From c5b59d3074a272061dff677d0446a9cf0a671f20 Mon Sep 17 00:00:00 2001 From: Alice Dahan Date: Fri, 8 Nov 2024 12:17:00 +0100 Subject: [PATCH] chore: remplace SmartTags PA par Piano Analytics (cherry picked from commit b9ba32f4d6df44d3fd61d7c76145820ead5436e9) --- .prettierignore | 1 + site/.env.template | 1 + site/netlify.base.toml | 2 +- site/scripts/i18n/parser.config.js | 1 + .../ATInternetTracking/.eslintrc.yaml | 1 + .../components/ATInternetTracking/Tracker.ts | 201 +++++++++--------- .../components/ATInternetTracking/index.tsx | 6 +- site/source/components/Feedback/Feedback.tsx | 2 +- site/source/components/Provider.tsx | 73 +++++-- .../ShareSimulationPopup.tsx | 2 +- .../ShareSimulationBanner/index.tsx | 2 +- .../layout/Footer/PrivacyPolicy.tsx | 15 +- site/source/locales/ui-en.yaml | 8 +- site/source/locales/ui-fr.yaml | 2 +- .../assistants/demande-mobilité/EndBlock.tsx | 8 +- 15 files changed, 178 insertions(+), 147 deletions(-) diff --git a/.prettierignore b/.prettierignore index 86e8b8b22..c53fce762 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,5 +3,6 @@ dist storybook-static site/source/locales/*.yaml site/source/components/ATInternetTracking/smarttag.js +site/source/components/ATInternetTracking/piano-analytics.js *.yaml.d.ts .yarnrc.yml \ No newline at end of file diff --git a/site/.env.template b/site/.env.template index 2723f7002..409e6c59e 100644 --- a/site/.env.template +++ b/site/.env.template @@ -16,6 +16,7 @@ VITE_ALGOLIA_APP_ID= VITE_ALGOLIA_SEARCH_KEY= VITE_ALGOLIA_INDEX_PREFIX=monentreprise-master- VITE_AT_INTERNET_SITE_ID= +VITE_AT_INTERNET_DEV_SITE_ID= VITE_FR_BASE_URL="http://localhost:3000/mon-entreprise" VITE_EN_BASE_URL="http://localhost:3000/infrance" diff --git a/site/netlify.base.toml b/site/netlify.base.toml index 4cbc2bba1..616e82b05 100644 --- a/site/netlify.base.toml +++ b/site/netlify.base.toml @@ -6,7 +6,7 @@ Content-Security-Policy = """\ style-src 'self' 'unsafe-inline'; \ connect-src 'self' *.incubateur.net raw.githubusercontent.com github.com tm.urssaf.fr recherche-entreprises.api.gouv.fr api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; \ form-action 'self' *.sibforms.com *.incubateur.net; \ - script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com polyfill.io; \ + script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr tag.aticdn.net *.incubateur.net stonly.com code.jquery.com polyfill.io; \ img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com github.com *.s3.amazonaws.com jedonnemonavis.numerique.gouv.fr; \ frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com https://conseillers-entreprises.service-public.fr \ """ diff --git a/site/scripts/i18n/parser.config.js b/site/scripts/i18n/parser.config.js index 1368defe9..9a053bc29 100644 --- a/site/scripts/i18n/parser.config.js +++ b/site/scripts/i18n/parser.config.js @@ -79,6 +79,7 @@ export default { input: [ '../../source/**/*.{jsx,tsx,js,ts}', '!../../source/components/ATInternetTracking/smarttag.js', + '!../../source/components/ATInternetTracking/piano-analytics.js', ], // An array of globs that describe where to look for source files // relative to the location of the configuration file diff --git a/site/source/components/ATInternetTracking/.eslintrc.yaml b/site/source/components/ATInternetTracking/.eslintrc.yaml index bfbe2db19..d3999dc4b 100644 --- a/site/source/components/ATInternetTracking/.eslintrc.yaml +++ b/site/source/components/ATInternetTracking/.eslintrc.yaml @@ -1,2 +1,3 @@ ignorePatterns: - smarttag.js + - piano-analytics.js diff --git a/site/source/components/ATInternetTracking/Tracker.ts b/site/source/components/ATInternetTracking/Tracker.ts index 538ea2fe4..8b86f93d0 100644 --- a/site/source/components/ATInternetTracking/Tracker.ts +++ b/site/source/components/ATInternetTracking/Tracker.ts @@ -1,15 +1,9 @@ -/* eslint-disable no-console */ -import './smarttag.js' - -// Ci-dessous les indicateurs personnalisés de site et de page -// https://developers.atinternet-solutions.com/javascript-fr/contenus-javascript-fr/indicateurs-de-site-et-de-page-javascript-fr/ -export const INDICATOR = { - SITE: { - LANGAGE: 1, - EMBARQUÉ: 2, - }, - PAGE: {}, -} as const +// Ajoute la propriété 'pa' à Window pour que Typescript accepte window.pa +declare global { + interface Window { + pa: ATTracker + } +} type PageHit = { page?: string @@ -17,128 +11,123 @@ type PageHit = { page_chapter2?: string page_chapter3?: string } - type ClickHit = { click?: string click_chapter1?: string click_chapter2?: string click_chapter3?: string + evenement_type?: 'telechargement' } export interface ATTracker { - setProp(prop: 'env_language', value: 'fr' | 'en', persistant: true): void - setProp(prop: 'n:simulateur_embarque', value: 1 | 0, persistant: true): void - setProp( - prop: 'evenement_type', - value: 'telechargement', - persistant: false + setConfigurations(options: { + site: number + collectDomain: string + privacyDefaultMode: 'optout' | 'exempt' + }): void + + setProperties( + propertiesObject: { + env_language: 'fr' | 'en' + 'n:simulateur_embarque': 1 | 0 + }, + options: { + persistent: true + } ): void - events: { - send(type: 'page.display', data: PageHit): void - send( - type: - | 'demarche.document' - | 'click.action' - | 'click.navigation' - | 'click.download' - | 'click.exit', - data: ClickHit & PageHit - ): void - } + sendEvent(type: 'page.display', data: PageHit): void + sendEvent( + type: + | 'demarche.document' + | 'click.action' + | 'click.navigation' + | 'click.download' + | 'click.exit', + data: ClickHit & PageHit + ): void - privacy: { - setVisitorMode(authority: 'cnil', type: 'exempt'): void - setVisitorOptout(): void - getVisitorMode(): { name: 'exempt' | 'optout' } - } -} - -type ATTrackerClass = { new (options: { site: number }): ATTracker } - -declare global { - const ATInternet: { - Tracker: { Tag: ATTrackerClass } + consent: { + setMode(type: 'exempt' | 'optout'): void + getMode(): { name: 'exempt' | 'optout' } } } export function createTracker(siteId?: string, doNotTrack = false) { const site = siteId ? +siteId : 0 if (Number.isNaN(site)) { - throw new Error('expect string siteId to be of number form') + throw new Error('Expect string siteId to be of number form') } - const BaseTracker: ATTrackerClass = - siteId && !import.meta.env.SSR ? ATInternet?.Tracker.Tag : Log - class Tag extends BaseTracker implements ATTracker { - #send: ATTracker['events']['send'] + if (typeof window === 'undefined' || !window.pa) { + throw new Error('Piano Analytics script not loaded') + } + class PianoTracker implements ATTracker { constructor(options: { language: 'fr' | 'en' }) { - super({ site }) - this.#send = this.events.send.bind(this) - this.events.send = (type, data) => { - if (type === 'page.display') { - this.#currentPageInfo = data - this.#send(type, data) + window.pa.setConfigurations({ + site, + collectDomain: 'https://tm.urssaf.fr', + privacyDefaultMode: doNotTrack ? 'optout' : 'exempt', + }) - return - } - if (!('click' in data)) { - throw new Error('invalid argument error') - } - this.#send(type, { ...this.#currentPageInfo, ...data }) - } - - this.setProp('env_language', options.language, true) - this.setProp( - 'n:simulateur_embarque', - document.location.pathname.includes('/iframes/') ? 1 : 0, - true + window.pa.setProperties( + { + env_language: options.language, + 'n:simulateur_embarque': document.location.pathname.includes( + '/iframes/' + ) + ? 1 + : 0, + }, + { persistent: true } ) + } - if (import.meta.env.MODE === 'production' && doNotTrack) { - this.privacy.setVisitorOptout() + setConfigurations(options: { + site: number + collectDomain: string + privacyDefaultMode: 'exempt' + }): void { + window.pa.setConfigurations(options) + } + + setProperties( + propertiesObject: { + env_language: 'fr' | 'en' + 'n:simulateur_embarque': 1 | 0 + }, + options: { persistent: true } + ): void { + window.pa.setProperties(propertiesObject, options) + } + + sendEvent( + type: + | 'page.display' + | 'demarche.document' + | 'click.action' + | 'click.navigation' + | 'click.download' + | 'click.exit', + data: PageHit | (ClickHit & PageHit) + ): void { + if (type === 'page.display') { + window.pa.sendEvent(type, data as PageHit) } else { - this.privacy.setVisitorMode('cnil', 'exempt') + window.pa.sendEvent(type, data as ClickHit & PageHit) } } - #currentPageInfo: PageHit = {} + consent = { + setMode(type: 'exempt' | 'optout'): void { + window.pa.consent.setMode(type) + }, + getMode(): { name: 'exempt' | 'optout' } { + return window.pa.consent.getMode() + }, + } } - return Tag -} - -export class Log implements ATTracker { - constructor(options?: Record) { - console.debug('ATTracker::new', options) - } - - setProp(name: string, value: unknown, persistent: boolean): void { - console.debug('ATTracker::setProp', { name, value, persistent }) - } - - events = { - send(name: string, data: Record): void { - console.debug('ATTracker::events.send', name, data) - }, - } - - privacy: ATTracker['privacy'] = { - setVisitorMode(...args) { - console.debug('ATTracker::privacy.setVisitorMode', ...args) - }, - setVisitorOptout() { - console.debug('ATTracker::setVisitorOptout') - }, - getVisitorMode() { - console.debug('ATTracker::privacy.getVisitorMode') - - return { name: 'exempt' } - }, - } - - dispatch(): void { - console.debug('ATTracker::dispatch') - } + return PianoTracker } diff --git a/site/source/components/ATInternetTracking/index.tsx b/site/source/components/ATInternetTracking/index.tsx index 39f807004..12b5adc83 100644 --- a/site/source/components/ATInternetTracking/index.tsx +++ b/site/source/components/ATInternetTracking/index.tsx @@ -1,8 +1,8 @@ import React, { createContext, useContext, useEffect } from 'react' -import { ATTracker, Log } from './Tracker' +import { ATTracker } from './Tracker' -export const TrackingContext = createContext(new Log()) +export const TrackingContext = createContext(null) // From https://github.com/nclsmitchell/at-internet export function toAtString(string: string): string { @@ -80,7 +80,7 @@ export function TrackPage({ const { chapter1, chapter2, chapter3 } = useChapters(chapters) const tag = useContext(TrackingContext) useEffect(() => { - tag.events.send( + tag?.sendEvent( 'page.display', Object.fromEntries( Object.entries({ diff --git a/site/source/components/Feedback/Feedback.tsx b/site/source/components/Feedback/Feedback.tsx index ef5364bbf..422b9a112 100644 --- a/site/source/components/Feedback/Feedback.tsx +++ b/site/source/components/Feedback/Feedback.tsx @@ -66,7 +66,7 @@ export function Feedback({ const submitFeedback = useCallback( (rating: FeedbackT) => { setFeedbackGivenForUrl(url) - tag.events.send('click.action', { + tag?.sendEvent('click.action', { click_chapter1: 'satisfaction', click: rating, }) diff --git a/site/source/components/Provider.tsx b/site/source/components/Provider.tsx index d1babd6de..25c396135 100644 --- a/site/source/components/Provider.tsx +++ b/site/source/components/Provider.tsx @@ -2,7 +2,7 @@ import { OverlayProvider } from '@react-aria/overlays' import { ErrorBoundary } from '@sentry/react' import i18next from 'i18next' import Engine from 'publicodes' -import { createContext, ReactNode } from 'react' +import { createContext, ReactNode, useEffect, useState } from 'react' import { HelmetProvider } from 'react-helmet-async' import { I18nextProvider } from 'react-i18next' import { Provider as ReduxProvider } from 'react-redux' @@ -16,7 +16,7 @@ import { EmbededContextProvider } from '@/hooks/useIsEmbedded' import * as safeLocalStorage from '../storage/safeLocalStorage' import { makeStore } from '../store/store' import { TrackingContext } from './ATInternetTracking' -import { createTracker } from './ATInternetTracking/Tracker' +import { ATTracker, createTracker } from './ATInternetTracking/Tracker' import { ErrorFallback } from './ErrorPage' import { IframeResizer } from './IframeResizer' import { ServiceWorker } from './ServiceWorker' @@ -92,28 +92,69 @@ function BrowserRouterProvider({ return <>{children} } - const ATTracker = createTracker( - import.meta.env.VITE_AT_INTERNET_SITE_ID, - safeLocalStorage.getItem('tracking:do_not_track') === '1' || - navigator.doNotTrack === '1' - ) - return ( - + {children} - + ) } + +function TrackingProvider({ children }: { children: React.ReactNode }) { + const [tracker, setTracker] = useState(null) + + useEffect(() => { + const script = document.createElement('script') + script.src = 'https://tag.aticdn.net/piano-analytics.js' + script.type = 'text/javascript' + script.crossOrigin = 'anonymous' + script.async = true + + script.onload = () => { + const siteId = ( + !import.meta.env.SSR + ? import.meta.env.VITE_AT_INTERNET_SITE_ID + : import.meta.env.VITE_AT_INTERNET_DEV_SITE_ID + ) as string + + const ATTrackerClass = createTracker( + siteId, + safeLocalStorage.getItem('tracking:do_not_track') === '1' || + navigator.doNotTrack === '1' + ) + + const instance = new ATTrackerClass({ + language: i18next.language as 'fr' | 'en', + }) + + setTracker(instance) + } + + script.onerror = () => { + // eslint-disable-next-line no-console + console.error('Failed to load Piano Analytics script') + } + + document.body.appendChild(script) + + return () => { + document.body.removeChild(script) + } + }, []) + + if (!tracker) { + return <>{children} + } + + return ( + + {children} + + ) +} diff --git a/site/source/components/ShareSimulationBanner/ShareSimulationPopup.tsx b/site/source/components/ShareSimulationBanner/ShareSimulationPopup.tsx index fd12dfec1..33f8d8828 100644 --- a/site/source/components/ShareSimulationBanner/ShareSimulationPopup.tsx +++ b/site/source/components/ShareSimulationBanner/ShareSimulationPopup.tsx @@ -52,7 +52,7 @@ export function ShareSimulationPopup({ url }: { url: string }) {