From 2442d2f2184d39cfc4ab22a11102dc7c53792ee4 Mon Sep 17 00:00:00 2001 From: Dhenain Ambroise Date: Tue, 22 Jun 2021 11:43:37 +0200 Subject: [PATCH] Refactor current usage of Amplitude (browser) --- .../workflows/deploy-vercel-production.yml | 4 +- .github/workflows/deploy-vercel-staging.yml | 4 +- src/app/components/BrowserPageBootstrap.tsx | 9 +- src/app/components/ServerPageBootstrap.tsx | 4 +- src/layouts/demo/components/DemoNav.tsx | 13 +- .../demo/components/IntroductionSection.tsx | 9 +- src/layouts/public/pagePublicTemplateSSG.tsx | 2 +- src/layouts/public/pagePublicTemplateSSR.tsx | 2 +- src/modules/core/amplitude/amplitude.ts | 299 +++++++++--------- src/modules/core/amplitude/events.ts | 37 +++ .../types/GetAmplitudeInstanceProps.ts | 20 ++ src/modules/core/sentry/sentry.ts | 28 +- src/modules/core/userConsent/cookieConsent.ts | 3 + .../demo/built-in-features/analytics.tsx | 9 +- src/pages/[locale]/demo/index.tsx | 2 +- src/pages/[locale]/demo/privacy.tsx | 2 +- .../demo/quick-preview/preview-product.tsx | 2 +- src/pages/[locale]/demo/terms.tsx | 2 +- src/pages/[locale]/public/index.tsx | 2 +- 19 files changed, 275 insertions(+), 178 deletions(-) create mode 100644 src/modules/core/amplitude/events.ts create mode 100644 src/modules/core/amplitude/types/GetAmplitudeInstanceProps.ts diff --git a/.github/workflows/deploy-vercel-production.yml b/.github/workflows/deploy-vercel-production.yml index b1430734f..21332c651 100644 --- a/.github/workflows/deploy-vercel-production.yml +++ b/.github/workflows/deploy-vercel-production.yml @@ -84,8 +84,8 @@ jobs: with: node-version: '14.x' # Use the same node.js version as the one Vercel's uses (currently node14.x) - # Starts a Vercel deployment, using the production configuration file of the default institution - # The default institution is the one defined in the `vercel.json` file (which is a symlink to the actual file) + # Starts a Vercel deployment, using the production configuration file of the default customer + # The default customer is the one defined in the `vercel.json` file (which is a symlink to the actual file) # N.B: It's Vercel that will perform the actual deployment start-production-deployment: name: Starts Vercel deployment (production) (Ubuntu 18.04) diff --git a/.github/workflows/deploy-vercel-staging.yml b/.github/workflows/deploy-vercel-staging.yml index 98b39fd8d..e3c15e8de 100644 --- a/.github/workflows/deploy-vercel-staging.yml +++ b/.github/workflows/deploy-vercel-staging.yml @@ -84,8 +84,8 @@ jobs: with: node-version: '14.x' # Use the same node.js version as the one Vercel's uses (currently node14.x) - # Starts a Vercel deployment, using the staging configuration file of the default institution - # The default institution is the one defined in the `vercel.json` file (which is a symlink to the actual file) + # Starts a Vercel deployment, using the staging configuration file of the default customer + # The default customer is the one defined in the `vercel.json` file (which is a symlink to the actual file) # N.B: It's Vercel that will perform the actual deployment start-staging-deployment: name: Starts Vercel deployment (staging) (Ubuntu 18.04) diff --git a/src/app/components/BrowserPageBootstrap.tsx b/src/app/components/BrowserPageBootstrap.tsx index 4710c68c9..4e59ea983 100644 --- a/src/app/components/BrowserPageBootstrap.tsx +++ b/src/app/components/BrowserPageBootstrap.tsx @@ -14,7 +14,10 @@ import { getClientNetworkConnectionType, getClientNetworkInformationSpeed, } from '@/modules/core/networkInformation/networkInformation'; -import { configureSentryUser } from '@/modules/core/sentry/sentry'; +import { + configureSentryBrowserMetadata, + configureSentryUserMetadata, +} from '@/modules/core/sentry/sentry'; import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; import { CYPRESS_WINDOW_NS, @@ -91,7 +94,9 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => const networkConnectionType: ClientNetworkConnectionType = getClientNetworkConnectionType(); // Configure Sentry user and track navigation through breadcrumb - configureSentryUser(userSession); + configureSentryUserMetadata(userSession); + configureSentryBrowserMetadata(networkSpeed, networkConnectionType, isInIframe, iframeReferrer); + Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs category: fileLabel, message: `Rendering ${fileLabel}`, diff --git a/src/app/components/ServerPageBootstrap.tsx b/src/app/components/ServerPageBootstrap.tsx index 4f5f06e09..032e9a23b 100644 --- a/src/app/components/ServerPageBootstrap.tsx +++ b/src/app/components/ServerPageBootstrap.tsx @@ -1,7 +1,7 @@ import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; import { OnlyServerPageProps } from '@/layouts/core/types/OnlyServerPageProps'; import { createLogger } from '@/modules/core/logging/logger'; -import { configureSentryUser } from '@/modules/core/sentry/sentry'; +import { configureSentryUserMetadata } from '@/modules/core/sentry/sentry'; import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; import * as Sentry from '@sentry/node'; import React from 'react'; @@ -35,7 +35,7 @@ const ServerPageBootstrap = (props: ServerPageBootstrapProps): JSX.Element => { } = pageProps; // Configure Sentry user and track navigation through breadcrumb - configureSentryUser(userSession); + configureSentryUserMetadata(userSession); Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs category: fileLabel, message: `Rendering ${fileLabel}`, diff --git a/src/layouts/demo/components/DemoNav.tsx b/src/layouts/demo/components/DemoNav.tsx index 88b8b0e29..9d44fd2fa 100644 --- a/src/layouts/demo/components/DemoNav.tsx +++ b/src/layouts/demo/components/DemoNav.tsx @@ -1,5 +1,6 @@ import Tooltip from '@/components/dataDisplay/Tooltip'; import AirtableAsset from '@/modules/core/airtable/components/AirtableAsset'; +import { AMPLITUDE_ACTIONS } from '@/modules/core/amplitude/events'; import { LogEvent } from '@/modules/core/amplitude/types/Amplitude'; import { AirtableAttachment } from '@/modules/core/data/types/AirtableAttachment'; import { Asset } from '@/modules/core/data/types/Asset'; @@ -284,7 +285,9 @@ const DemoNav: React.FunctionComponent = () => { target={'_blank'} rel={'noopener'} onClick={(): void => { - logEvent('open-github-doc'); + logEvent('open-github-doc', { + action: AMPLITUDE_ACTIONS.CLICK, + }); }} > @@ -307,7 +310,9 @@ const DemoNav: React.FunctionComponent = () => { target={'_blank'} rel={'noopener'} onClick={(): void => { - logEvent('open-github'); + logEvent('open-github', { + action: AMPLITUDE_ACTIONS.CLICK, + }); }} title={''} > @@ -338,7 +343,9 @@ const DemoNav: React.FunctionComponent = () => { target={'_blank'} rel={'noopener'} onClick={(): void => { - logEvent('open-admin-site'); + logEvent('open-admin-site', { + action: AMPLITUDE_ACTIONS.CLICK, + }); }} > diff --git a/src/layouts/demo/components/IntroductionSection.tsx b/src/layouts/demo/components/IntroductionSection.tsx index 70f24beea..cdcf3a4e8 100644 --- a/src/layouts/demo/components/IntroductionSection.tsx +++ b/src/layouts/demo/components/IntroductionSection.tsx @@ -1,4 +1,5 @@ import ExternalLink from '@/components/dataDisplay/ExternalLink'; +import { AMPLITUDE_ACTIONS } from '@/modules/core/amplitude/events'; import { LogEvent } from '@/modules/core/amplitude/types/Amplitude'; import I18nLink from '@/modules/core/i18n/components/I18nLink'; import { css } from '@emotion/react'; @@ -41,14 +42,18 @@ const IntroductionSection: React.FunctionComponent = (props): JSX.Element logEvent('open-what-is-preset-doc')} + onClick={(): number => logEvent('open-what-is-preset-doc', { + action: AMPLITUDE_ACTIONS.CLICK, + })} > What is a preset?  -  logEvent('open-see-all-presets-doc')} + onClick={(): number => logEvent('open-see-all-presets-doc', { + action: AMPLITUDE_ACTIONS.CLICK, + })} > See all presets diff --git a/src/layouts/public/pagePublicTemplateSSG.tsx b/src/layouts/public/pagePublicTemplateSSG.tsx index e55ac1530..802d32b9b 100644 --- a/src/layouts/public/pagePublicTemplateSSG.tsx +++ b/src/layouts/public/pagePublicTemplateSSG.tsx @@ -6,7 +6,7 @@ import { getPublicLayoutStaticPaths, getPublicLayoutStaticProps, } from '@/layouts/public/publicLayoutSSG'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { Customer } from '@/modules/core/data/types/Customer'; import { createLogger } from '@/modules/core/logging/logger'; diff --git a/src/layouts/public/pagePublicTemplateSSR.tsx b/src/layouts/public/pagePublicTemplateSSR.tsx index 92cfbdf94..c9226a8f4 100644 --- a/src/layouts/public/pagePublicTemplateSSR.tsx +++ b/src/layouts/public/pagePublicTemplateSSR.tsx @@ -7,7 +7,7 @@ import { getPublicLayoutServerSideProps, GetPublicLayoutServerSidePropsResults, } from '@/layouts/public/publicLayoutSSR'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { Customer } from '@/modules/core/data/types/Customer'; import { createLogger } from '@/modules/core/logging/logger'; diff --git a/src/modules/core/amplitude/amplitude.ts b/src/modules/core/amplitude/amplitude.ts index e50fe4c89..9595ebbfc 100644 --- a/src/modules/core/amplitude/amplitude.ts +++ b/src/modules/core/amplitude/amplitude.ts @@ -1,3 +1,6 @@ +import { AMPLITUDE_ACTIONS } from '@/modules/core/amplitude/events'; +import { GetAmplitudeInstanceProps } from '@/modules/core/amplitude/types/GetAmplitudeInstanceProps'; +import { GenericObject } from '@/modules/core/data/types/GenericObject'; import { createLogger } from '@/modules/core/logging/logger'; import { ClientNetworkConnectionType, @@ -12,7 +15,6 @@ import { Identify, } from 'amplitude-js'; import UniversalCookiesManager from '../cookiesManager/UniversalCookiesManager'; -import { UserConsent } from '../userConsent/types/UserConsent'; import { UserSemiPersistentSession } from '../userSession/types/UserSemiPersistentSession'; import { NextWebVitalsMetricsReport } from '../webVitals/types/NextWebVitalsMetricsReport'; @@ -22,142 +24,148 @@ const logger = createLogger({ }); /** - * Event actions. + * Initializes an existing amplitude instance with base configuration shared by all Amplitude events. * - * All actions must use action verb (imperative form). - * - * DA Usefulness: Avoids using anonymous constants that will likely duplicate each other. - * Using constants ensures strict usage with a proper definition for the analytics team and the developers. - * Example: Using both "remove" and "delete" could lead to misunderstanding or errors when configuring charts. + * @param amplitudeInstance + * @param options */ -export enum AMPLITUDE_ACTIONS { - CLICK = 'click', // When an element is clicked (mouse) or tapped (screen, mobile) - SELECT = 'select', // When an element is selected (checkbox, select input, multi choices) - REMOVE = 'remove', // When an element is removed/delete - OPEN = 'open', // When an element is opened - CLOSE = 'close', // When an element is closed -} +export const initAmplitudeInstance = (amplitudeInstance: AmplitudeClient, options: GetAmplitudeInstanceProps): void => { + const { + customerRef, + iframeReferrer, + isInIframe, + lang, + locale, + userId, + userConsent, + networkSpeed, + networkConnectionType, + } = options; + const { + isUserOptedOutOfAnalytics, + hasUserGivenAnyCookieConsent, + } = userConsent; + + // See https://help.amplitude.com/hc/en-us/articles/115001361248#settings-configuration-options + // See all JS SDK options https://github.com/amplitude/Amplitude-JavaScript/blob/master/src/options.js + amplitudeInstance.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY, null, { + userId, + logLevel: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? 'DISABLE' : 'WARN', + includeGclid: false, // GDPR Enabling this is not GDPR compliant and must not be enabled without explicit user consent - See https://croud.com/blog/news/10-point-gdpr-checklist-digital-advertising/ + includeReferrer: true, // See https://help.amplitude.com/hc/en-us/articles/215131888#track-referrers + includeUtm: true, + // @ts-ignore XXX onError should be allowed, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42005 + onError: (error): void => { + Sentry.captureException(error); + console.error(error); // eslint-disable-line no-console + }, + sameSiteCookie: 'Strict', // 'Strict' | 'Lax' | 'None' - See https://web.dev/samesite-cookies-explained/ + cookieExpiration: 365, // Expires in 1 year (would fallback to 10 years by default, which isn't GDPR compliant) + }); + + // Disable analytics tracking entirely if the user has opted-out + if (isUserOptedOutOfAnalytics) { + amplitudeInstance.setOptOut(true); // If true, then no events will be logged or sent. + + if (process.env.STORYBOOK !== 'true') { + logger.info('User has opted-out of analytics tracking.'); // eslint-disable-line no-console + } + } else { + // Re-enable tracking (necessary if it was previously disabled!) + amplitudeInstance.setOptOut(false); -/** - * Pages names used by Amplitude - * - * Each page within the /src/pages directory should use a different page name as "pageName". - * This is used to track events happening within the pages, to know on which page they occurred. - */ -export enum AMPLITUDE_PAGES { - DEMO_HOME_PAGE = 'demo', - PREVIEW_PRODUCT_PAGE = 'demo/preview-product', - TERMS_PAGE = 'demo/terms', - PRIVACY_PAGE = 'demo/privacy', - TEMPLATE_SSG_PAGE = 'template-ssg', - TEMPLATE_SSR_PAGE = 'template-ssr', -} - -type GetAmplitudeInstanceProps = { - customerRef: string; - iframeReferrer: string; - isInIframe: boolean; - lang: string; - locale: string; - userId: string; - userConsent: UserConsent; - networkSpeed: ClientNetworkInformationSpeed; - networkConnectionType: ClientNetworkConnectionType; -} + if (process.env.STORYBOOK !== 'true') { + logger.info(`User has opted-in into analytics tracking. (Thank you! This helps us make our product better, and we don't track any personal/identifiable data.`); // eslint-disable-line no-console + } + } -export const getAmplitudeInstance = (props: GetAmplitudeInstanceProps): AmplitudeClient | null => { - // XXX Amplitude is disabled on the server side, it's only used on the client side - // (avoids duplicated events, and amplitude-js isn't server-side compatible anyway) - if (isBrowser()) { - const { - customerRef, - iframeReferrer, - isInIframe, - lang, - locale, - userId, - userConsent, - networkSpeed, - networkConnectionType, - } = props; - const { - isUserOptedOutOfAnalytics, - hasUserGivenAnyCookieConsent, - } = userConsent; - - Sentry.configureScope((scope) => { // See https://www.npmjs.com/package/@sentry/node - scope.setTag('networkSpeed', networkSpeed); - scope.setTag('networkConnectionType', networkConnectionType); - scope.setTag('iframe', `${isInIframe}`); - scope.setExtra('iframe', isInIframe); - scope.setExtra('iframeReferrer', iframeReferrer); - }); + amplitudeInstance.setVersionName(process.env.NEXT_PUBLIC_APP_VERSION_RELEASE); // e.g: v1.0.0 + + /** + * Initializes the Amplitude user session. + * + * We must set all "must-have" properties here (instead of doing it in the "AmplitudeProvider", as userProperties), + * because "react-amplitude" would send the next "page-displayed" event BEFORE sending the $identify event, + * which would lead to events not containing the user's session. + * + * We're only doing this when detecting a new session, as it won't be executed multiple times for the same session anyway, and it avoids noise. + * + * @see https://github.com/amplitude/Amplitude-JavaScript/issues/223 Learn more about "setOnce" + */ + if (amplitudeInstance.isNewSession()) { + const visitor: Identify = new amplitudeInstance.Identify(); + visitor.setOnce('customer.ref', customerRef); + + if (lang) { + // DA Helps figuring out if the initial language (auto-detected) is changed afterwards + visitor.setOnce('initial_lang', lang); + visitor.setOnce('lang', lang); + } - const amplitude = require('amplitude-js'); // eslint-disable-line @typescript-eslint/no-var-requires - const amplitudeInstance: AmplitudeClient = amplitude.getInstance(); + if (locale) { + visitor.setOnce('initial_locale', locale); + visitor.setOnce('locale', locale); + } - // See https://help.amplitude.com/hc/en-us/articles/115001361248#settings-configuration-options - // See all JS SDK options https://github.com/amplitude/Amplitude-JavaScript/blob/master/src/options.js - amplitudeInstance.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY, null, { - userId, - logLevel: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? 'DISABLE' : 'WARN', - includeGclid: true, - includeReferrer: true, // See https://help.amplitude.com/hc/en-us/articles/215131888#track-referrers - includeUtm: true, - // @ts-ignore XXX onError should be allowed, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42005 - onError: (error): void => { - Sentry.captureException(error); - console.error(error); // eslint-disable-line no-console - }, - sameSiteCookie: 'Strict', // 'Strict' | 'Lax' | 'None' - See https://web.dev/samesite-cookies-explained/ - cookieExpiration: 365, // Expires in 1 year (would fallback to 10 years by default, which isn't GDPR compliant) - }); + if (isInIframe) { + // DA This will help track down the users who discovered our platform because of an iframe + visitor.setOnce('initial_iframe', isInIframe); + visitor.setOnce('iframe', isInIframe); + } - // Disable analytics tracking entirely if the user has opted-out - if (isUserOptedOutOfAnalytics) { - amplitudeInstance.setOptOut(true); // If true, then no events will be logged or sent. + if (iframeReferrer) { + visitor.setOnce('initial_iframeReferrer', iframeReferrer); + visitor.setOnce('iframeReferrer', iframeReferrer); + } - if (process.env.STORYBOOK !== 'true') { - logger.info('User has opted-out of analytics tracking.'); // eslint-disable-line no-console - } - } else { - // Re-enable tracking (necessary if it was previously disabled!) - amplitudeInstance.setOptOut(false); + visitor.setOnce('initial_networkSpeed', networkSpeed); + visitor.setOnce('initial_networkConnectionType', networkConnectionType); - if (process.env.STORYBOOK !== 'true') { - logger.info(`User has opted-in into analytics tracking. (Thank you! This helps us make our product better, and we don't track any personal/identifiable data.`); // eslint-disable-line no-console - } - } + visitor.setOnce('networkSpeed', networkSpeed); + visitor.setOnce('networkConnectionType', networkConnectionType); - amplitudeInstance.setVersionName(process.env.NEXT_PUBLIC_APP_VERSION_RELEASE); // e.g: v1.0.0 + visitor.set('isUserOptedOutOfAnalytics', isUserOptedOutOfAnalytics); + visitor.set('hasUserGivenAnyCookieConsent', hasUserGivenAnyCookieConsent); - // We're only doing this when detecting a new session, as it won't be executed multiple times for the same session anyway, and it avoids noise - if (amplitudeInstance.isNewSession()) { - // Store whether the visitor originally came from an iframe (and from where) - const visitor: Identify = new amplitudeInstance.Identify(); - // XXX Learn more about "setOnce" at https://github.com/amplitude/Amplitude-JavaScript/issues/223 - visitor.setOnce('initial_lang', lang); // DA Helps figuring out if the initial language (auto-detected) is changed afterwards - visitor.setOnce('initial_locale', locale); - visitor.setOnce('initial_networkSpeed', networkSpeed); - visitor.setOnce('initial_networkConnectionType', networkConnectionType); - // DA This will help track down the users who discovered our platform because of an iframe - visitor.setOnce('initial_iframe', isInIframe); - visitor.setOnce('initial_iframeReferrer', iframeReferrer); + amplitudeInstance.identify(visitor); // Send the new identify event to amplitude (updates the user's identity) + } +}; - // XXX We set all "must-have" properties here (instead of doing it in the "AmplitudeProvider", as userProperties), because react-amplitude will send the next "page-displayed" event BEFORE sending the $identify event - visitor.setOnce('customer.ref', customerRef); - visitor.setOnce('lang', lang); - visitor.setOnce('locale', locale); - visitor.setOnce('networkSpeed', networkSpeed); - visitor.setOnce('networkConnectionType', networkConnectionType); - visitor.setOnce('iframe', isInIframe); - visitor.setOnce('iframeReferrer', iframeReferrer); +/** + * Base properties shared by all events. + */ +export const getDefaultEventProperties = (): GenericObject => { + const customerRef = process.env.NEXT_PUBLIC_CUSTOMER_REF; + const customerRefWithoutVersion = customerRef?.replace('-v4', ''); // Hack removing the version number from the customer ref + + return { + app: { + name: process.env.NEXT_PUBLIC_APP_NAME, + release: process.env.NEXT_PUBLIC_APP_VERSION_RELEASE, + stage: process.env.NEXT_PUBLIC_APP_STAGE, + }, + page: { + url: location.href, + path: location.pathname, + origin: location.origin, + name: null, // XXX Will be set by the page (usually through its layout) + }, + customer: { + ref: customerRefWithoutVersion, + }, + }; +}; - visitor.set('isUserOptedOutOfAnalytics', isUserOptedOutOfAnalytics); - visitor.set('hasUserGivenAnyCookieConsent', hasUserGivenAnyCookieConsent); +/** + * Returns a browser-compatible Amplitude instance + * @param props + */ +export const getAmplitudeInstance = (props: GetAmplitudeInstanceProps): AmplitudeClient | null => { + if (isBrowser()) { + const amplitude = require('amplitude-js'); // eslint-disable-line @typescript-eslint/no-var-requires + const amplitudeInstance: AmplitudeClient = amplitude.getInstance(); - amplitudeInstance.identify(visitor); // Send the new identify event to amplitude (updates user's identity) - } + initAmplitudeInstance(amplitudeInstance, props); return amplitudeInstance; @@ -181,47 +189,30 @@ export const sendWebVitals = (report: NextWebVitalsMetricsReport): void => { const userData: UserSemiPersistentSession = universalCookiesManager.getUserData(); const networkSpeed: ClientNetworkInformationSpeed = getClientNetworkInformationSpeed(); const networkConnectionType: ClientNetworkConnectionType = getClientNetworkConnectionType(); + const customerRef = process.env.NEXT_PUBLIC_CUSTOMER_REF; - // https://help.amplitude.com/hc/en-us/articles/115001361248#settings-configuration-options - // See all JS SDK options https://github.com/amplitude/Amplitude-JavaScript/blob/master/src/options.js - amplitudeInstance.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY, null, { - // userId: null, + initAmplitudeInstance(amplitudeInstance, { + customerRef: customerRef, userId: userData?.id, - logLevel: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? 'DISABLE' : 'WARN', - includeGclid: false, // GDPR Enabling this is not GDPR compliant and must not be enabled without explicit user consent - See https://croud.com/blog/news/10-point-gdpr-checklist-digital-advertising/ - includeReferrer: true, // https://help.amplitude.com/hc/en-us/articles/215131888#track-referrers - includeUtm: true, - // @ts-ignore XXX onError should be allowed, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42005 - onError: (error): void => { - Sentry.captureException(error); - console.error(error); // eslint-disable-line no-console + userConsent: { + isUserOptedOutOfAnalytics: false, + hasUserGivenAnyCookieConsent: false, }, - sameSiteCookie: 'Strict', // 'Strict' | 'Lax' | 'None' - See https://web.dev/samesite-cookies-explained/ - cookieExpiration: 365, // Expires in 1 year (would fallback to 10 years by default, which isn't GDPR compliant) + locale: null, + lang: null, + isInIframe: null, + iframeReferrer: null, + networkSpeed, + networkConnectionType, }); - amplitudeInstance.setVersionName(process.env.NEXT_PUBLIC_APP_VERSION_RELEASE); // e.g: v1.0.0 - // Send metrics to our analytics service amplitudeInstance.logEvent(`report-web-vitals`, { - app: { - name: process.env.NEXT_PUBLIC_APP_NAME, - release: process.env.NEXT_PUBLIC_APP_VERSION_RELEASE, - stage: process.env.NEXT_PUBLIC_APP_STAGE, - preset: process.env.NEXT_PUBLIC_NRN_PRESET, - }, - page: { - url: location.href, - path: location.pathname, - origin: location.origin, - name: null, - }, - customer: { - ref: process.env.NEXT_PUBLIC_CUSTOMER_REF, - }, + ...getDefaultEventProperties(), report, networkSpeed, networkConnectionType, + action: AMPLITUDE_ACTIONS.AUTO, }); // eslint-disable-next-line no-console console.debug('report-web-vitals report sent to Amplitude'); diff --git a/src/modules/core/amplitude/events.ts b/src/modules/core/amplitude/events.ts new file mode 100644 index 000000000..cb1d1fc46 --- /dev/null +++ b/src/modules/core/amplitude/events.ts @@ -0,0 +1,37 @@ +/** + * Event actions. + * + * We use an "action" property to track the event's trigger. + * It's especially useful when the same event can be triggered by different actions, + * as sometimes it's easier to keep a single event with different properties. (it really depends how you want to use the data) + * + * Best practice: All actions must use action verb (imperative form). + * This is a NRN internal rule (recommandation) about how to track which action led to triggering the event. + * + * DA Usefulness: Avoids using anonymous constants that will likely end up being duplicated. + * Using constants ensures strict usage with a proper definition for the analytics team and the developers. + * Example: Using both "remove" and "delete" could lead to misunderstanding or errors when configuring charts. + */ +export enum AMPLITUDE_ACTIONS { + CLICK = 'click', // When an element is clicked (mouse) or tapped (screen, mobile) + SELECT = 'select', // When an element is selected (checkbox, select input, multi choices) + REMOVE = 'remove', // When an element is removed/delete + OPEN = 'open', // When an element is opened + CLOSE = 'close', // When an element is closed + AUTO = 'auto', // When an event is triggered automatically instead of a user action +} + +/** + * Pages names used within Amplitude. + * + * Each page within the /src/pages directory should use a different page name as "pageName". + * This is used to track events happening within the pages, to know on which page they occurred. + */ +export enum AMPLITUDE_PAGES { + DEMO_HOME_PAGE = 'demo', + PREVIEW_PRODUCT_PAGE = 'demo/preview-product', + TERMS_PAGE = 'demo/terms', + PRIVACY_PAGE = 'demo/privacy', + TEMPLATE_SSG_PAGE = 'template-ssg', + TEMPLATE_SSR_PAGE = 'template-ssr', +} diff --git a/src/modules/core/amplitude/types/GetAmplitudeInstanceProps.ts b/src/modules/core/amplitude/types/GetAmplitudeInstanceProps.ts new file mode 100644 index 000000000..12297fd9e --- /dev/null +++ b/src/modules/core/amplitude/types/GetAmplitudeInstanceProps.ts @@ -0,0 +1,20 @@ +import { + ClientNetworkConnectionType, + ClientNetworkInformationSpeed, +} from '@/modules/core/networkInformation/networkInformation'; +import { UserConsent } from '@/modules/core/userConsent/types/UserConsent'; + +/** + * Properties necessary to initialize a new Amplitude instance. + */ +export type GetAmplitudeInstanceProps = { + customerRef: string; + iframeReferrer: string; + isInIframe?: boolean; + lang?: string; + locale?: string; + userId?: string; + userConsent: UserConsent; + networkSpeed: ClientNetworkInformationSpeed; + networkConnectionType: ClientNetworkConnectionType; +} diff --git a/src/modules/core/sentry/sentry.ts b/src/modules/core/sentry/sentry.ts index e1cf604e3..6ea0c7b19 100644 --- a/src/modules/core/sentry/sentry.ts +++ b/src/modules/core/sentry/sentry.ts @@ -1,3 +1,7 @@ +import { + ClientNetworkConnectionType, + ClientNetworkInformationSpeed, +} from '@/modules/core/networkInformation/networkInformation'; import * as Sentry from '@sentry/node'; import { isBrowser } from '@unly/utils'; import map from 'lodash.map'; @@ -64,7 +68,7 @@ export const ALERT_TYPES = { }; /** - * Configure Sentry tags for the current user. + * Configure Sentry tags related to the current user. * * Allows to track all Sentry events related to a particular user. * The tracking remains anonymous, there are no personal information being tracked, only internal ids. @@ -72,7 +76,7 @@ export const ALERT_TYPES = { * @param userSession * @see https://www.npmjs.com/package/@sentry/node */ -export const configureSentryUser = (userSession: UserSession): void => { +export const configureSentryUserMetadata = (userSession: UserSession): void => { if (process.env.SENTRY_DSN) { Sentry.configureScope((scope) => { scope.setTag('userId', userSession?.id); @@ -82,6 +86,26 @@ export const configureSentryUser = (userSession: UserSession): void => { } }; +/** + * Configure Sentry tags related to the browser metadata. + * + * @param networkSpeed + * @param networkConnectionType + * @param isInIframe + * @param iframeReferrer + */ +export const configureSentryBrowserMetadata = (networkSpeed: ClientNetworkInformationSpeed, networkConnectionType: ClientNetworkConnectionType, isInIframe: boolean, iframeReferrer: string): void => { + if (process.env.SENTRY_DSN) { + Sentry.configureScope((scope) => { + scope.setTag('networkSpeed', networkSpeed); + scope.setTag('networkConnectionType', networkConnectionType); + scope.setTag('iframe', `${isInIframe}`); + scope.setExtra('iframe', isInIframe); + scope.setExtra('iframeReferrer', iframeReferrer); + }); + } +}; + /** * Configure Sentry tags for the currently used lang/locale. * diff --git a/src/modules/core/userConsent/cookieConsent.ts b/src/modules/core/userConsent/cookieConsent.ts index 08f8488fc..ea21c37b5 100644 --- a/src/modules/core/userConsent/cookieConsent.ts +++ b/src/modules/core/userConsent/cookieConsent.ts @@ -1,3 +1,4 @@ +import { AMPLITUDE_ACTIONS } from '@/modules/core/amplitude/events'; import { createLogger } from '@/modules/core/logging/logger'; import * as Sentry from '@sentry/node'; import { AmplitudeClient } from 'amplitude-js'; @@ -221,6 +222,7 @@ const initCookieConsent = (options: InitOptions): void => { if (status === 'deny') { // Store user choice, then disable analytics tracking amplitudeInstance.logEvent('user-consent-manually-given', { + action: AMPLITUDE_ACTIONS.CLICK, choice: 'deny', at: +new Date(), message, // Store the text that was displayed to the user at the time @@ -232,6 +234,7 @@ const initCookieConsent = (options: InitOptions): void => { // Enable analytics tracking, then store user choice amplitudeInstance.setOptOut(false); amplitudeInstance.logEvent('user-consent-manually-given', { + action: AMPLITUDE_ACTIONS.CLICK, choice: 'allow', at: +new Date(), message, // Store the text that was displayed to the user at the time diff --git a/src/pages/[locale]/demo/built-in-features/analytics.tsx b/src/pages/[locale]/demo/built-in-features/analytics.tsx index 7f5d7f364..ad61c92b0 100644 --- a/src/pages/[locale]/demo/built-in-features/analytics.tsx +++ b/src/pages/[locale]/demo/built-in-features/analytics.tsx @@ -11,6 +11,7 @@ import { getDemoLayoutStaticPaths, getDemoLayoutStaticProps, } from '@/layouts/demo/demoLayoutSSG'; +import { AMPLITUDE_ACTIONS } from '@/modules/core/amplitude/events'; import { LogEvent } from '@/modules/core/amplitude/types/Amplitude'; import { createLogger } from '@/modules/core/logging/logger'; import useUserConsent from '@/modules/core/userConsent/hooks/useUserConsent'; @@ -240,7 +241,9 @@ const ExampleAnalyticsPage: NextPage = (props): JSX.Element => { onClick={(): void => { // eslint-disable-next-line no-console console.log('Button click'); - logEvent('analytics-button-test-event'); + logEvent('analytics-button-test-event', { + action: AMPLITUDE_ACTIONS.CLICK, + }); }} > Click me @@ -256,7 +259,9 @@ const ExampleAnalyticsPage: NextPage = (props): JSX.Element => { onClick={(): void => { // eslint-disable-next-line no-console console.log('Button click'); - logEvent('analytics-button-test-event'); + logEvent('analytics-button-test-event', { + action: AMPLITUDE_ACTIONS.CLICK, + }); }} > Click me diff --git a/src/pages/[locale]/demo/index.tsx b/src/pages/[locale]/demo/index.tsx index 81b003470..225f48413 100644 --- a/src/pages/[locale]/demo/index.tsx +++ b/src/pages/[locale]/demo/index.tsx @@ -11,7 +11,7 @@ import { getDemoLayoutStaticPaths, getDemoLayoutStaticProps, } from '@/layouts/demo/demoLayoutSSG'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import { LogEvent } from '@/modules/core/amplitude/types/Amplitude'; import { createLogger } from '@/modules/core/logging/logger'; import { Amplitude } from '@amplitude/react-amplitude'; diff --git a/src/pages/[locale]/demo/privacy.tsx b/src/pages/[locale]/demo/privacy.tsx index 00e0c9aca..bda162fc1 100644 --- a/src/pages/[locale]/demo/privacy.tsx +++ b/src/pages/[locale]/demo/privacy.tsx @@ -7,7 +7,7 @@ import { getDemoLayoutStaticPaths, getDemoLayoutStaticProps, } from '@/layouts/demo/demoLayoutSSG'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { Customer } from '@/modules/core/data/types/Customer'; import { replaceAllOccurrences } from '@/modules/core/js/string'; diff --git a/src/pages/[locale]/demo/quick-preview/preview-product.tsx b/src/pages/[locale]/demo/quick-preview/preview-product.tsx index f36ea755e..676bbac3a 100644 --- a/src/pages/[locale]/demo/quick-preview/preview-product.tsx +++ b/src/pages/[locale]/demo/quick-preview/preview-product.tsx @@ -3,7 +3,7 @@ import { OnlyBrowserPageProps } from '@/layouts/core/types/OnlyBrowserPageProps' import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; import { getDemoLayoutServerSideProps } from '@/layouts/demo/demoLayoutSSR'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { AirtableRecord } from '@/modules/core/data/types/AirtableRecord'; import { Customer } from '@/modules/core/data/types/Customer'; diff --git a/src/pages/[locale]/demo/terms.tsx b/src/pages/[locale]/demo/terms.tsx index b4faf712e..8e3c5fd37 100644 --- a/src/pages/[locale]/demo/terms.tsx +++ b/src/pages/[locale]/demo/terms.tsx @@ -7,7 +7,7 @@ import { getDemoLayoutStaticPaths, getDemoLayoutStaticProps, } from '@/layouts/demo/demoLayoutSSG'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { Customer } from '@/modules/core/data/types/Customer'; import { replaceAllOccurrences } from '@/modules/core/js/string'; diff --git a/src/pages/[locale]/public/index.tsx b/src/pages/[locale]/public/index.tsx index b8f8682e9..27210773f 100644 --- a/src/pages/[locale]/public/index.tsx +++ b/src/pages/[locale]/public/index.tsx @@ -6,7 +6,7 @@ import { getPublicLayoutStaticPaths, getPublicLayoutStaticProps, } from '@/layouts/public/publicLayoutSSG'; -import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/amplitude'; +import { AMPLITUDE_PAGES } from '@/modules/core/amplitude/events'; import useCustomer from '@/modules/core/data/hooks/useCustomer'; import { Customer } from '@/modules/core/data/types/Customer'; import { createLogger } from '@/modules/core/logging/logger';