chore: remplace SmartTags PA par Piano Analytics

(cherry picked from commit b9ba32f4d6)
pull/3254/head
Alice Dahan 2024-11-08 12:17:00 +01:00 committed by liliced
parent a017419086
commit c5b59d3074
15 changed files with 178 additions and 147 deletions

View File

@ -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

View File

@ -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"

View File

@ -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 \
"""

View File

@ -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

View File

@ -1,2 +1,3 @@
ignorePatterns:
- smarttag.js
- piano-analytics.js

View File

@ -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<string, string | number>) {
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<string, unknown>): 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
}

View File

@ -1,8 +1,8 @@
import React, { createContext, useContext, useEffect } from 'react'
import { ATTracker, Log } from './Tracker'
import { ATTracker } from './Tracker'
export const TrackingContext = createContext<ATTracker>(new Log())
export const TrackingContext = createContext<ATTracker | null>(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({

View File

@ -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,
})

View File

@ -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 (
<HelmetProvider>
<TrackingContext.Provider
value={
new ATTracker({
language: i18next.language as 'fr' | 'en',
})
}
>
<TrackingProvider>
<BrowserRouter
basename={import.meta.env.MODE === 'production' ? '' : basename}
future={{ v7_startTransition: true }}
>
{children}
</BrowserRouter>
</TrackingContext.Provider>
</TrackingProvider>
</HelmetProvider>
)
}
function TrackingProvider({ children }: { children: React.ReactNode }) {
const [tracker, setTracker] = useState<ATTracker | null>(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 (
<TrackingContext.Provider value={tracker}>
{children}
</TrackingContext.Provider>
)
}

View File

@ -52,7 +52,7 @@ export function ShareSimulationPopup({ url }: { url: string }) {
<Button
size="XS"
onPress={() => {
tracker.events.send('click.action', {
tracker?.sendEvent('click.action', {
click_chapter1: 'feature:partage',
click: 'lien copié',
})

View File

@ -102,7 +102,7 @@ export default function ShareOrSaveSimulationBanner({
light
size="XS"
onPress={(e) => {
tracker.events.send('click.action', {
tracker?.sendEvent('click.action', {
click_chapter1: 'feature:partage',
click: 'démarré',
})

View File

@ -42,15 +42,15 @@ export default function PrivacyPolicy({
const handleChange = useCallback(
(checked: boolean) => {
if (checked) {
tracker.privacy.setVisitorOptout()
tracker?.consent.setMode('optout')
safeLocalStorage.setItem('tracking:do_not_track', '1')
} else {
tracker.privacy.setVisitorMode('cnil', 'exempt')
tracker?.consent.setMode('exempt')
safeLocalStorage.setItem('tracking:do_not_track', '0')
}
setValueChanged(true)
},
[setValueChanged, tracker.privacy]
[setValueChanged, tracker?.consent]
)
return (
@ -273,9 +273,10 @@ export default function PrivacyPolicy({
<Body>
<Trans i18nKey="privacyPolicy.tracking.content">
mon-entreprise.urssaf.fr ne dépose pas de cookies ou de traceurs.
Cependant, la plateforme utilise Matomo, une solution de mesure
d'audience, configurée en mode « exempté » et ne nécessitant pas le
recueil du consentement des personnes concernées conformément aux{' '}
Cependant, la plateforme utilise Piano Analytics, une solution de
mesure d'audience, configurée en mode « exempté » et ne nécessitant
pas le recueil du consentement des personnes concernées conformément
aux{' '}
<StyledLink
href="https://www.cnil.fr/fr/solutions-pour-les-cookies-de-mesure-daudience"
aria-label={t(
@ -301,7 +302,7 @@ export default function PrivacyPolicy({
<Checkbox
name="opt-out mesure audience"
onChange={handleChange}
defaultSelected={tracker.privacy.getVisitorMode().name === 'optout'}
defaultSelected={tracker?.consent.getMode().name === 'optout'}
>
{t(
'privacyPolicy.tracking.optOut.checkboxLabel',

View File

@ -1686,10 +1686,10 @@ privacyPolicy:
tracking:
ariaLabel: CNIL recommendations, see more information on the CNIL website, new window
content: mon-entreprise.urssaf.fr does not use cookies or other tracking
devices. However, the platform uses Matomo, an audience measurement
solution, configured in “exempted” mode and not requiring the consent of
the persons concerned in accordance with the <2>recommendations of the
CNIL</2>.
devices. However, the platform uses Piano Analytics, an audience
measurement solution configured in "exempted" mode, which does not require
the consent of the persons concerned, in accordance with the
<2>recommendations of the CNIL</2>.
optOut:
checkboxLabel: I do not want to send anonymous data about my use of the platform
for audience measurement purposes.

View File

@ -1794,7 +1794,7 @@ privacyPolicy:
ariaLabel: recommandations de la CNIL, voir plus d'informations à ce sujet sur
le site de la CNIL, nouvelle fenêtre
content: mon-entreprise.urssaf.fr ne dépose pas de cookies ou de traceurs.
Cependant, la plateforme utilise Matomo, une solution de mesure
Cependant, la plateforme utilise Piano Analytics, une solution de mesure
d'audience, configurée en mode « exempté » et ne nécessitant pas le
recueil du consentement des personnes concernées conformément aux
<2>recommandations de la CNIL</2>.

View File

@ -214,13 +214,9 @@ export default function EndBlock({ fields, missingValues }: EndBlockProps) {
href={url}
size="XL"
onPress={() => {
tracker.setProp(
'evenement_type',
'telechargement',
false
)
tracker.events.send('demarche.document', {
tracker?.sendEvent('demarche.document', {
click: 'demande_formulaire_a1',
evenement_type: 'telechargement',
})
}}
download="demande-mobilité-internationale.pdf"