feat(help-center): add more analytics integrations to portal settings
Adds Google Analytics 4, Hotjar, Plausible, Amplitude, Microsoft Clarity, and Meta Pixel alongside Google Tag Manager. Provider IDs are stored in portal config, validated against safe formats, and rendered as official embed snippets on the public help center.
This commit is contained in:
+92
-22
@@ -20,8 +20,51 @@ const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
// Mirrors Portal::GTM_CONTAINER_ID_FORMAT on the backend.
|
||||
const GTM_CONTAINER_ID_FORMAT = /^GTM-[A-Z0-9]+$/;
|
||||
// Mirrors Portal::ANALYTICS_CONFIG_FORMATS on the backend.
|
||||
const ANALYTICS_PROVIDERS = [
|
||||
{
|
||||
key: 'gtm_container_id',
|
||||
i18nKey: 'GTM',
|
||||
icon: 'i-logos-google-tag-manager',
|
||||
format: /^GTM-[A-Z0-9]+$/,
|
||||
},
|
||||
{
|
||||
key: 'ga4_measurement_id',
|
||||
i18nKey: 'GA4',
|
||||
icon: 'i-logos-google-analytics',
|
||||
format: /^G-[A-Z0-9]+$/,
|
||||
},
|
||||
{
|
||||
key: 'hotjar_site_id',
|
||||
i18nKey: 'HOTJAR',
|
||||
icon: 'i-logos-hotjar-icon',
|
||||
format: /^\d+$/,
|
||||
},
|
||||
{
|
||||
key: 'plausible_domain',
|
||||
i18nKey: 'PLAUSIBLE',
|
||||
icon: 'i-woot-plausible',
|
||||
format: /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i,
|
||||
},
|
||||
{
|
||||
key: 'amplitude_api_key',
|
||||
i18nKey: 'AMPLITUDE',
|
||||
icon: 'i-logos-amplitude-icon',
|
||||
format: /^[a-z0-9]+$/i,
|
||||
},
|
||||
{
|
||||
key: 'clarity_project_id',
|
||||
i18nKey: 'CLARITY',
|
||||
icon: 'i-woot-microsoft-clarity',
|
||||
format: /^[a-z0-9]+$/i,
|
||||
},
|
||||
{
|
||||
key: 'meta_pixel_id',
|
||||
i18nKey: 'META_PIXEL',
|
||||
icon: 'i-logos-meta-icon',
|
||||
format: /^\d+$/,
|
||||
},
|
||||
];
|
||||
|
||||
const portalConfig = computed(() => props.activePortal?.config || {});
|
||||
|
||||
@@ -43,13 +86,15 @@ const liveChatWidgets = computed(() => {
|
||||
|
||||
const state = reactive({
|
||||
liveChatWidgetInboxId: '',
|
||||
gtmContainerId: '',
|
||||
...Object.fromEntries(ANALYTICS_PROVIDERS.map(({ key }) => [key, ''])),
|
||||
});
|
||||
const originalState = reactive({ ...state });
|
||||
|
||||
const resetFromPortal = () => {
|
||||
state.liveChatWidgetInboxId = props.activePortal?.inbox?.id || '';
|
||||
state.gtmContainerId = portalConfig.value.gtm_container_id || '';
|
||||
ANALYTICS_PROVIDERS.forEach(({ key }) => {
|
||||
state[key] = portalConfig.value[key] || '';
|
||||
});
|
||||
Object.assign(originalState, state);
|
||||
};
|
||||
|
||||
@@ -64,18 +109,27 @@ const liveChatTitle = computed(() =>
|
||||
)
|
||||
);
|
||||
|
||||
const trimmedGtmId = computed(() => state.gtmContainerId.trim());
|
||||
|
||||
const isGtmInvalid = computed(
|
||||
() =>
|
||||
trimmedGtmId.value !== '' &&
|
||||
!GTM_CONTAINER_ID_FORMAT.test(trimmedGtmId.value)
|
||||
const trimmedAnalyticsValues = computed(() =>
|
||||
Object.fromEntries(
|
||||
ANALYTICS_PROVIDERS.map(({ key }) => [key, state[key].trim()])
|
||||
)
|
||||
);
|
||||
|
||||
const invalidAnalyticsKeys = computed(() =>
|
||||
ANALYTICS_PROVIDERS.filter(({ key, format }) => {
|
||||
const value = trimmedAnalyticsValues.value[key];
|
||||
return value !== '' && !format.test(value);
|
||||
}).map(({ key }) => key)
|
||||
);
|
||||
|
||||
const isInvalid = key => invalidAnalyticsKeys.value.includes(key);
|
||||
|
||||
const hasChanges = computed(
|
||||
() =>
|
||||
state.liveChatWidgetInboxId !== originalState.liveChatWidgetInboxId ||
|
||||
trimmedGtmId.value !== originalState.gtmContainerId
|
||||
ANALYTICS_PROVIDERS.some(
|
||||
({ key }) => trimmedAnalyticsValues.value[key] !== originalState[key]
|
||||
)
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
@@ -83,7 +137,7 @@ const handleSave = () => {
|
||||
id: props.activePortal.id,
|
||||
slug: props.activePortal.slug,
|
||||
inbox_id: state.liveChatWidgetInboxId,
|
||||
config: { gtm_container_id: trimmedGtmId.value },
|
||||
config: { ...trimmedAnalyticsValues.value },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -132,41 +186,57 @@ const handleSave = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="provider in ANALYTICS_PROVIDERS"
|
||||
:key="provider.key"
|
||||
class="flex flex-col gap-4 p-4 rounded-xl outline outline-1 outline-n-weak"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg size-9 shrink-0 bg-n-alpha-2 text-n-slate-12"
|
||||
>
|
||||
<Icon icon="i-ri-google-fill" class="size-5" />
|
||||
<Icon :icon="provider.icon" class="size-5" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h6 class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.TITLE') }}
|
||||
{{
|
||||
t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.TITLE`
|
||||
)
|
||||
}}
|
||||
</h6>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.DESCRIPTION') }}
|
||||
{{
|
||||
t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.DESCRIPTION`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
v-model="state.gtmContainerId"
|
||||
v-model="state[provider.key]"
|
||||
:placeholder="
|
||||
t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.PLACEHOLDER')
|
||||
t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.PLACEHOLDER`
|
||||
)
|
||||
"
|
||||
:message="
|
||||
isGtmInvalid
|
||||
? t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.INVALID')
|
||||
: t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.GTM.HELP')
|
||||
isInvalid(provider.key)
|
||||
? t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.INVALID`
|
||||
)
|
||||
: t(
|
||||
`HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.${provider.i18nKey}.HELP`
|
||||
)
|
||||
"
|
||||
:message-type="isGtmInvalid ? 'error' : 'info'"
|
||||
:message-type="isInvalid(provider.key) ? 'error' : 'info'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
:label="t('HELP_CENTER.PORTAL_SETTINGS.INTEGRATIONS.SAVE')"
|
||||
:disabled="!hasChanges || isGtmInvalid || isFetching"
|
||||
:disabled="!hasChanges || invalidAnalyticsKeys.length > 0 || isFetching"
|
||||
@click="handleSave"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -899,11 +899,53 @@
|
||||
},
|
||||
"GTM": {
|
||||
"TITLE": "Google Tag Manager",
|
||||
"DESCRIPTION": "Add analytics and marketing tags, like Google Analytics, by connecting your Tag Manager container.",
|
||||
"DESCRIPTION": "Add analytics and marketing tags by connecting your Tag Manager container.",
|
||||
"PLACEHOLDER": "GTM-XXXXXXX",
|
||||
"HELP": "Find this in your Google Tag Manager workspace. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid container ID, for example GTM-XXXXXXX."
|
||||
},
|
||||
"GA4": {
|
||||
"TITLE": "Google Analytics 4",
|
||||
"DESCRIPTION": "Measure traffic and visitor behavior on your help center with Google Analytics.",
|
||||
"PLACEHOLDER": "G-XXXXXXXXXX",
|
||||
"HELP": "Find the measurement ID under Admin → Data streams in Google Analytics. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid measurement ID, for example G-XXXXXXXXXX."
|
||||
},
|
||||
"HOTJAR": {
|
||||
"TITLE": "Hotjar",
|
||||
"DESCRIPTION": "Understand how visitors use your help center with heatmaps and session recordings.",
|
||||
"PLACEHOLDER": "1234567",
|
||||
"HELP": "Find your site ID under Sites & organizations in Hotjar. Leave empty to disable.",
|
||||
"INVALID": "Enter a numeric Hotjar site ID."
|
||||
},
|
||||
"PLAUSIBLE": {
|
||||
"TITLE": "Plausible",
|
||||
"DESCRIPTION": "Track help center traffic with privacy-friendly Plausible Analytics.",
|
||||
"PLACEHOLDER": "example.com",
|
||||
"HELP": "Enter the domain configured in your Plausible site settings. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid domain, for example example.com."
|
||||
},
|
||||
"AMPLITUDE": {
|
||||
"TITLE": "Amplitude",
|
||||
"DESCRIPTION": "Analyze visitor behavior on your help center with Amplitude.",
|
||||
"PLACEHOLDER": "0123456789abcdef0123456789abcdef",
|
||||
"HELP": "Find the API key in your Amplitude project settings. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid Amplitude API key."
|
||||
},
|
||||
"CLARITY": {
|
||||
"TITLE": "Microsoft Clarity",
|
||||
"DESCRIPTION": "Capture heatmaps and session recordings with Microsoft Clarity.",
|
||||
"PLACEHOLDER": "abcd1234ef",
|
||||
"HELP": "Find the project ID in your Clarity project settings. Leave empty to disable.",
|
||||
"INVALID": "Enter a valid Clarity project ID."
|
||||
},
|
||||
"META_PIXEL": {
|
||||
"TITLE": "Meta Pixel",
|
||||
"DESCRIPTION": "Measure ad conversions and build audiences from help center visits with Meta Pixel.",
|
||||
"PLACEHOLDER": "123456789012345",
|
||||
"HELP": "Find the pixel ID in Meta Events Manager. Leave empty to disable.",
|
||||
"INVALID": "Enter a numeric pixel ID."
|
||||
},
|
||||
"SAVE": "Save changes"
|
||||
},
|
||||
"NAV": {
|
||||
|
||||
Reference in New Issue
Block a user