diff --git a/packages/console/src/components/ReportConversion/index.tsx b/packages/console/src/components/ReportConversion/index.tsx
new file mode 100644
index 000000000..2ed523127
--- /dev/null
+++ b/packages/console/src/components/ReportConversion/index.tsx
@@ -0,0 +1,57 @@
+import { useEffect } from 'react';
+import { Helmet } from 'react-helmet';
+
+import {
+ shouldReport,
+ lintrk,
+ gtagAwTrackingId,
+ linkedInConversionId,
+ gtag,
+ gtagSignUpConversionId,
+} from './utils';
+
+/**
+ * In cloud production environment, this component initiates gtag.js and LinkedIn
+ * Insight Tag, then reports a sign-up conversion to them.
+ */
+export default function ReportConversion() {
+ /**
+ * This `useEffect()` initiates Google Tag and report a sign-up conversion to it.
+ * It may run multiple times (e.g. a user visit multiple times to finish the onboarding process,
+ * which rarely happens), but it'll be okay since we've set conversion's "Count" to "One"
+ * which means only the first interaction is valuable.
+ *
+ * Track this conversion in the backend has been considered, but Google does not provide
+ * a clear guideline for it and marks the [Node library](https://developers.google.com/tag-platform/tag-manager/api/v2/libraries)
+ * as "alpha" which looks unreliable.
+ */
+ useEffect(() => {
+ if (shouldReport) {
+ gtag('js', new Date());
+ gtag('config', gtagAwTrackingId);
+ gtag('event', 'conversion', {
+ send_to: gtagSignUpConversionId,
+ });
+
+ lintrk('track', { conversion_id: linkedInConversionId });
+ console.debug('Have a good day!');
+ } else {
+ console.debug("Not reporting conversion because it's not production");
+ }
+ }, []);
+
+ if (shouldReport) {
+ return (
+
+
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/packages/console/src/components/ReportConversion/utils.ts b/packages/console/src/components/ReportConversion/utils.ts
new file mode 100644
index 000000000..1c6afb05a
--- /dev/null
+++ b/packages/console/src/components/ReportConversion/utils.ts
@@ -0,0 +1,44 @@
+export const gtagAwTrackingId = 'AW-11124811245';
+/** This ID indicates a user has truly signed up for Logto Cloud. */
+export const gtagSignUpConversionId = `AW-11192640559/ZuqUCLvNpasYEK_IiNkp`;
+const logtoProductionHostname = 'logto.io';
+const linkedInPartnerId = '5096172';
+export const linkedInConversionId = '13374828';
+
+/**
+ * Due to the special of conversion reporting, it should be `true` only in the
+ * Logto Cloud production environment.
+ * Add the leading '.' to make it safer (ignore hostnames like "foologto.io").
+ */
+export const shouldReport = window.location.hostname.endsWith('.' + logtoProductionHostname);
+
+/* eslint-disable @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods */
+
+/** This function is edited from the Google Tag official code snippet. */
+export function gtag(..._: unknown[]) {
+ if (!window.dataLayer) {
+ window.dataLayer = [];
+ }
+
+ // We cannot use rest params here since gtag has some internal logic about `arguments` for data transpiling
+ // eslint-disable-next-line prefer-rest-params
+ window.dataLayer.push(arguments);
+}
+
+/** This function is edited from the LinkedIn Tag official code snippet. */
+export function lintrk(..._: unknown[]) {
+ // Init LinkedIn tag if needed
+ if (!window._linkedin_data_partner_ids) {
+ window._linkedin_data_partner_ids = [];
+ window._linkedin_data_partner_ids.push(linkedInPartnerId);
+ }
+
+ if (!window.lintrk) {
+ window.lintrk = { q: [] };
+ }
+
+ // eslint-disable-next-line prefer-rest-params
+ window.lintrk.q.push(arguments);
+}
+
+/* eslint-enable @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods */
diff --git a/packages/console/src/include.d/gtag.d.ts b/packages/console/src/include.d/gtag.d.ts
deleted file mode 100644
index 977669c09..000000000
--- a/packages/console/src/include.d/gtag.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-declare interface Window {
- dataLayer?: unknown[];
-}
diff --git a/packages/console/src/include.d/tags.d.ts b/packages/console/src/include.d/tags.d.ts
new file mode 100644
index 000000000..5dc207007
--- /dev/null
+++ b/packages/console/src/include.d/tags.d.ts
@@ -0,0 +1,9 @@
+declare interface Window {
+ // `gtag.js`
+ dataLayer?: unknown[];
+ // LinkedIn
+ _linkedin_data_partner_ids?: unknown[];
+ lintrk?: {
+ q: unknown[];
+ };
+}
diff --git a/packages/console/src/onboarding/constants/index.ts b/packages/console/src/onboarding/constants/index.ts
deleted file mode 100644
index 260976397..000000000
--- a/packages/console/src/onboarding/constants/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const gtagAwTrackingId = 'AW-11124811245';
-/** This ID indicates a user has truly signed up for Logto Cloud. */
-export const gtagSignUpConversionId = `AW-11192640559/ZuqUCLvNpasYEK_IiNkp`;
-export const logtoProductionHostname = 'logto.io';
diff --git a/packages/console/src/onboarding/index.tsx b/packages/console/src/onboarding/index.tsx
index 946f06ea6..aab3cceb0 100644
--- a/packages/console/src/onboarding/index.tsx
+++ b/packages/console/src/onboarding/index.tsx
@@ -2,10 +2,10 @@ import { Component, ConsoleEvent } from '@logto/app-insights/custom-event';
import { TrackOnce } from '@logto/app-insights/react';
import { Theme } from '@logto/schemas';
import { useContext, useEffect } from 'react';
-import { Helmet } from 'react-helmet';
import { Route, Navigate, Outlet, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
+import ReportConversion from '@/components/ReportConversion';
import AppBoundary from '@/containers/AppBoundary';
import ProtectedRoutes from '@/containers/ProtectedRoutes';
import TenantAccess from '@/containers/TenantAccess';
@@ -15,21 +15,15 @@ import useSwrOptions from '@/hooks/use-swr-options';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import NotFound from '@/pages/NotFound';
-import { gtagAwTrackingId, gtagSignUpConversionId, logtoProductionHostname } from './constants';
import AppContent from './containers/AppContent';
import useUserOnboardingData from './hooks/use-user-onboarding-data';
import * as styles from './index.module.scss';
import SignInExperience from './pages/SignInExperience';
import Welcome from './pages/Welcome';
import { OnboardingPage, OnboardingRoute } from './types';
-import { getOnboardingPage, gtag } from './utils';
+import { getOnboardingPage } from './utils';
const welcomePathname = getOnboardingPage(OnboardingPage.Welcome);
-/**
- * Due to the special of Google Tag, it should be `true` only in the Logto Cloud production environment.
- * Add the leading '.' to make it safer (ignore hostnames like "foologto.io").
- */
-const shouldReportToGtag = window.location.hostname.endsWith('.' + logtoProductionHostname);
function Layout() {
const swrOptions = useSwrOptions();
@@ -44,27 +38,6 @@ function Layout() {
};
}, [setThemeOverride]);
- /**
- * This `useEffect()` initiates Google Tag and report a sign-up conversion to it.
- * It may run multiple times (e.g. a user visit multiple times to finish the onboarding process,
- * which rarely happens), but it'll be okay since we've set conversion's "Count" to "One"
- * which means only the first interaction is valuable.
- *
- * Track this conversion in the backend has been considered, but Google does not provide
- * a clear guideline for it and marks the [Node library](https://developers.google.com/tag-platform/tag-manager/api/v2/libraries)
- * as "alpha" which looks unreliable.
- */
- useEffect(() => {
- if (shouldReportToGtag) {
- gtag('js', new Date());
- gtag('config', gtagAwTrackingId);
- gtag('event', 'conversion', {
- send_to: gtagSignUpConversionId,
- });
- console.debug('Google Tag event fires');
- }
- }, []);
-
const {
data: { questionnaire },
} = useUserOnboardingData();
@@ -80,15 +53,7 @@ function Layout() {
- {shouldReportToGtag && (
-
-
-
- )}
+
diff --git a/packages/console/src/onboarding/utils.ts b/packages/console/src/onboarding/utils.ts
index b77302852..d219a9d4f 100644
--- a/packages/console/src/onboarding/utils.ts
+++ b/packages/console/src/onboarding/utils.ts
@@ -5,15 +5,3 @@ import { OnboardingRoute } from './types';
export const getOnboardingPage = (page: OnboardingPage) =>
joinPath(OnboardingRoute.Onboarding, page);
-
-/** This function is updated from the Google Tag official code snippet. */
-export function gtag(..._: unknown[]) {
- if (!window.dataLayer) {
- // eslint-disable-next-line @silverhand/fp/no-mutation
- window.dataLayer = [];
- }
-
- // We cannot use rest params here since gtag has some internal logic about `arguments` for data transpiling
- // eslint-disable-next-line @silverhand/fp/no-mutating-methods, prefer-rest-params
- window.dataLayer.push(arguments);
-}