0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

Merge pull request #3905 from logto-io/gao-add-google-tag

refactor: add google tag conversion report
This commit is contained in:
Gao Sun 2023-05-30 13:27:21 +08:00 committed by GitHub
commit f1d73af537
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 2 deletions

View file

@ -37,6 +37,12 @@ export default function withSecurityHeaders<InputContext extends RequestContext>
const coreOrigins = urlSet.origins; const coreOrigins = urlSet.origins;
const developmentOrigins = conditionalArray(!isProduction && 'ws:'); const developmentOrigins = conditionalArray(!isProduction && 'ws:');
const appInsightsOrigins = ['https://*.applicationinsights.azure.com']; const appInsightsOrigins = ['https://*.applicationinsights.azure.com'];
// Gtag will load by `<script />`
const gtagOrigins = [
'https://*.googletagmanager.com',
'https://*.doubleclick.net',
'https://*.googleadservices.com',
];
return async ( return async (
context: InputContext, context: InputContext,
@ -63,6 +69,7 @@ export default function withSecurityHeaders<InputContext extends RequestContext>
const basicSecurityHeaderSettings: HelmetOptions = { const basicSecurityHeaderSettings: HelmetOptions = {
contentSecurityPolicy: false, // Exclusively set for console app only contentSecurityPolicy: false, // Exclusively set for console app only
crossOriginEmbedderPolicy: { policy: 'credentialless' },
expectCt: false, // Not recommended, will be deprecated by modern browsers expectCt: false, // Not recommended, will be deprecated by modern browsers
dnsPrefetchControl: false, dnsPrefetchControl: false,
referrerPolicy: { referrerPolicy: {
@ -89,7 +96,7 @@ export default function withSecurityHeaders<InputContext extends RequestContext>
directives: { directives: {
'upgrade-insecure-requests': null, 'upgrade-insecure-requests': null,
imgSrc: ["'self'", 'data:', 'https:'], imgSrc: ["'self'", 'data:', 'https:'],
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"], scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'", ...gtagOrigins],
connectSrc: [ connectSrc: [
"'self'", "'self'",
...adminOrigins, ...adminOrigins,
@ -97,6 +104,7 @@ export default function withSecurityHeaders<InputContext extends RequestContext>
...coreOrigins, ...coreOrigins,
...developmentOrigins, ...developmentOrigins,
...appInsightsOrigins, ...appInsightsOrigins,
...gtagOrigins,
], ],
frameSrc: ["'self'", ...coreOrigins, ...adminOrigins], frameSrc: ["'self'", ...coreOrigins, ...adminOrigins],
}, },

View file

@ -0,0 +1,3 @@
declare interface Window {
dataLayer?: unknown[];
}

View file

@ -2,6 +2,7 @@ import { Component, ConsoleEvent } from '@logto/app-insights/custom-event';
import { TrackOnce } from '@logto/app-insights/react'; import { TrackOnce } from '@logto/app-insights/react';
import { Theme } from '@logto/schemas'; import { Theme } from '@logto/schemas';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
@ -14,6 +15,7 @@ import useSwrOptions from '@/hooks/use-swr-options';
import NotFound from '@/pages/NotFound'; import NotFound from '@/pages/NotFound';
import * as styles from './App.module.scss'; import * as styles from './App.module.scss';
import { gtagAwTrackingId, gtagSignUpConversionId, logtoProductionHostname } from './constants';
import AppContent from './containers/AppContent'; import AppContent from './containers/AppContent';
import useUserOnboardingData from './hooks/use-user-onboarding-data'; import useUserOnboardingData from './hooks/use-user-onboarding-data';
import About from './pages/About'; import About from './pages/About';
@ -21,9 +23,14 @@ import Congrats from './pages/Congrats';
import SignInExperience from './pages/SignInExperience'; import SignInExperience from './pages/SignInExperience';
import Welcome from './pages/Welcome'; import Welcome from './pages/Welcome';
import { OnboardingPage, OnboardingRoute } from './types'; import { OnboardingPage, OnboardingRoute } from './types';
import { getOnboardingPage } from './utils'; import { getOnboardingPage, gtag } from './utils';
const welcomePathname = getOnboardingPage(OnboardingPage.Welcome); 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 App() { function App() {
const swrOptions = useSwrOptions(); const swrOptions = useSwrOptions();
@ -37,6 +44,27 @@ function App() {
}; };
}, [setThemeOverride]); }, [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 { const {
data: { questionnaire }, data: { questionnaire },
isLoaded, isLoaded,
@ -52,6 +80,15 @@ function App() {
<div className={styles.app}> <div className={styles.app}>
<SWRConfig value={swrOptions}> <SWRConfig value={swrOptions}>
<AppBoundary> <AppBoundary>
{shouldReportToGtag && (
<Helmet>
<script
async
crossOrigin="anonymous"
src={`https://www.googletagmanager.com/gtag/js?id=${gtagAwTrackingId}`}
/>
</Helmet>
)}
<Toast /> <Toast />
<Routes> <Routes>
<Route index element={<Navigate replace to={welcomePathname} />} /> <Route index element={<Navigate replace to={welcomePathname} />} />

View file

@ -11,3 +11,8 @@ export const emailUsLink = buildUrl(contactEmailLink, {
export const logtoBlogLink = 'https://docs.logto.io/blog?utm_source=console'; export const logtoBlogLink = 'https://docs.logto.io/blog?utm_source=console';
export const aboutCloudPreviewLink = 'https://docs.logto.io/about/cloud-preview?utm_source=console'; export const aboutCloudPreviewLink = 'https://docs.logto.io/about/cloud-preview?utm_source=console';
export const gtagAwTrackingId = 'AW-11124811245';
/** This ID indicates a user has truly signed up for Logto Cloud. */
export const gtagSignUpConversionId = `${gtagAwTrackingId}/RVejCKC65qMYEO3L3Lgp`;
export const logtoProductionHostname = 'logto.io';

View file

@ -5,3 +5,15 @@ import { OnboardingRoute } from './types';
export const getOnboardingPage = (page: OnboardingPage) => export const getOnboardingPage = (page: OnboardingPage) =>
joinPath(OnboardingRoute.Onboarding, page); 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);
}