diff --git a/.prettierignore b/.prettierignore index c53fce762..86e8b8b22 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,5 @@ 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 409e6c59e..2723f7002 100644 --- a/site/.env.template +++ b/site/.env.template @@ -16,7 +16,6 @@ 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 616e82b05..4cbc2bba1 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 tag.aticdn.net *.incubateur.net stonly.com code.jquery.com polyfill.io; \ + script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.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 9a053bc29..1368defe9 100644 --- a/site/scripts/i18n/parser.config.js +++ b/site/scripts/i18n/parser.config.js @@ -79,7 +79,6 @@ 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 d3999dc4b..bfbe2db19 100644 --- a/site/source/components/ATInternetTracking/.eslintrc.yaml +++ b/site/source/components/ATInternetTracking/.eslintrc.yaml @@ -1,3 +1,2 @@ ignorePatterns: - smarttag.js - - piano-analytics.js diff --git a/site/source/components/ATInternetTracking/Tracker.ts b/site/source/components/ATInternetTracking/Tracker.ts index 8b86f93d0..538ea2fe4 100644 --- a/site/source/components/ATInternetTracking/Tracker.ts +++ b/site/source/components/ATInternetTracking/Tracker.ts @@ -1,9 +1,15 @@ -// Ajoute la propriété 'pa' à Window pour que Typescript accepte window.pa -declare global { - interface Window { - pa: ATTracker - } -} +/* 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 type PageHit = { page?: string @@ -11,123 +17,128 @@ 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 { - setConfigurations(options: { - site: number - collectDomain: string - privacyDefaultMode: 'optout' | 'exempt' - }): void - - setProperties( - propertiesObject: { - env_language: 'fr' | 'en' - 'n:simulateur_embarque': 1 | 0 - }, - options: { - persistent: true - } + 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 ): void - sendEvent(type: 'page.display', data: PageHit): void - sendEvent( - type: - | 'demarche.document' - | 'click.action' - | 'click.navigation' - | 'click.download' - | 'click.exit', - data: ClickHit & PageHit - ): 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 + } - consent: { - setMode(type: 'exempt' | 'optout'): void - getMode(): { name: 'exempt' | 'optout' } + 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 } } } 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 - if (typeof window === 'undefined' || !window.pa) { - throw new Error('Piano Analytics script not loaded') - } + class Tag extends BaseTracker implements ATTracker { + #send: ATTracker['events']['send'] - class PianoTracker implements ATTracker { constructor(options: { language: 'fr' | 'en' }) { - window.pa.setConfigurations({ - site, - collectDomain: 'https://tm.urssaf.fr', - privacyDefaultMode: doNotTrack ? 'optout' : 'exempt', - }) + 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.setProperties( - { - env_language: options.language, - 'n:simulateur_embarque': document.location.pathname.includes( - '/iframes/' - ) - ? 1 - : 0, - }, - { persistent: true } + 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 ) - } - 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) + if (import.meta.env.MODE === 'production' && doNotTrack) { + this.privacy.setVisitorOptout() } else { - window.pa.sendEvent(type, data as ClickHit & PageHit) + this.privacy.setVisitorMode('cnil', 'exempt') } } - consent = { - setMode(type: 'exempt' | 'optout'): void { - window.pa.consent.setMode(type) - }, - getMode(): { name: 'exempt' | 'optout' } { - return window.pa.consent.getMode() - }, - } + #currentPageInfo: PageHit = {} } - return PianoTracker + 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') + } } diff --git a/site/source/components/ATInternetTracking/index.tsx b/site/source/components/ATInternetTracking/index.tsx index 12b5adc83..39f807004 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 } from './Tracker' +import { ATTracker, Log } from './Tracker' -export const TrackingContext = createContext(null) +export const TrackingContext = createContext(new Log()) // 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?.sendEvent( + tag.events.send( 'page.display', Object.fromEntries( Object.entries({ diff --git a/site/source/components/Feedback/Feedback.tsx b/site/source/components/Feedback/Feedback.tsx index 422b9a112..ef5364bbf 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?.sendEvent('click.action', { + tag.events.send('click.action', { click_chapter1: 'satisfaction', click: rating, }) diff --git a/site/source/components/Provider.tsx b/site/source/components/Provider.tsx index 25c396135..d1babd6de 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, useEffect, useState } from 'react' +import { createContext, ReactNode } 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 { ATTracker, createTracker } from './ATInternetTracking/Tracker' +import { createTracker } from './ATInternetTracking/Tracker' import { ErrorFallback } from './ErrorPage' import { IframeResizer } from './IframeResizer' import { ServiceWorker } from './ServiceWorker' @@ -92,69 +92,28 @@ 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 33f8d8828..fd12dfec1 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 }) {