Améliore le tracking

fix #1824
pull/2083/head
Johan Girod 2022-04-07 11:25:10 +02:00
parent d325c095c8
commit 1a27fcd65e
16 changed files with 751 additions and 3930 deletions

View File

@ -10,42 +10,50 @@ export const INDICATOR = {
PAGE: {},
} as const
type CustomSiteIndicator = {
[INDICATOR.SITE.LANGAGE]: '[fr]' | '[en]' // langage du site (mycompanyinfrance ou mon-entreprise)
[INDICATOR.SITE.EMBARQUÉ]: '1' | '0' // intégration externe
}
type CustomPageIndicator = Record<string, string>
type CustomVars = {
site?: Partial<CustomSiteIndicator>
page?: Partial<CustomPageIndicator>
}
type ATHit = {
type PageHit = {
name?: string
chapter1?: string
chapter2?: string
chapter3?: string
level2?: string
customVars?: CustomVars
}
type ClickHit = {
click?: string
click_chapter1?: string
click_chapter2?: string
click_chapter3?: string
}
export interface ATTracker {
page: {
set(infos: ATHit): void
}
click: {
set(
infos: ATHit & { type: 'exit' | 'download' | 'action' | 'navigation' }
setProp(prop: 'env_language', value: 'fr' | 'en', persistant: true): void
setProp(prop: 'simulateur_embarque', value: boolean, persistant: true): void
setProp(
prop: 'evenement_type',
value: 'telechargement',
persistant: false
): void
events: {
send(type: 'page.display', data: PageHit): void
send(
type: 'demarche.document',
data: { click: 'demande_formulaire_a1' }
): void
send(
type:
| 'click.action'
| 'click.navigation'
| 'click.download'
| 'click.exit',
data: ClickHit
): void
}
customVars: {
set(variables: CustomVars): void
}
privacy: {
setVisitorMode(authority: 'cnil', type: 'exempt'): void
setVisitorOptout(): void
getVisitorMode(): { name: 'exempt' | 'optout' }
}
dispatch(): void
}
type ATTrackerClass = { new (options: { site: number }): ATTracker }
@ -61,29 +69,25 @@ export function createTracker(siteId?: string, doNotTrack = false) {
if (Number.isNaN(site)) {
throw new Error('expect string siteId to be of number form')
}
const BaseTracker: ATTrackerClass = siteId ? ATInternet?.Tracker.Tag : Log
const BaseTracker: ATTrackerClass =
siteId && !import.meta.env.SSR ? ATInternet?.Tracker.Tag : Log
class Tag extends BaseTracker {
site: CustomSiteIndicator = {
[INDICATOR.SITE.LANGAGE]: '[fr]',
[INDICATOR.SITE.EMBARQUÉ]:
!import.meta.env.SSR && document.location.pathname.includes('/iframes/')
? '1'
: '0',
}
constructor(options: { language: 'fr' | 'en' }) {
super({ site })
this.site[INDICATOR.SITE.LANGAGE] = `[${options.language}]`
this.setProp('env_language', options.language, true)
this.setProp(
'simulateur_embarque',
document.location.pathname.includes('/iframes/'),
true
)
if (import.meta.env.MODE === 'production' && doNotTrack) {
this.privacy.setVisitorOptout()
} else {
this.privacy.setVisitorMode('cnil', 'exempt')
}
}
dispatch() {
this.customVars.set({ site: this.site })
super.dispatch()
}
}
return Tag
@ -93,19 +97,13 @@ export class Log implements ATTracker {
constructor(options?: Record<string, string | number>) {
console.debug('ATTracker::new', options)
}
page = {
set(infos: ATHit): void {
console.debug('ATTracker::page.set', infos)
},
setProp(name: string, value: boolean | string, persistent: boolean): void {
console.debug('ATTracker::setProp', { name, value, persistent })
}
click = {
set(infos: ATHit): void {
console.debug('ATTracker::click.set', infos)
},
}
customVars: ATTracker['customVars'] = {
set(variables) {
console.debug('ATTracker::customVars.set', variables)
events = {
send(name: string, data: Record<string, unknown>): void {
console.debug('ATTracker::events.send', name, data)
},
}
privacy: ATTracker['privacy'] = {

View File

@ -1,4 +1,4 @@
import { createContext, useContext, useEffect } from 'react'
import React, { createContext, useContext, useEffect } from 'react'
import { ATTracker, Log } from './Tracker'
export const TrackingContext = createContext<ATTracker>(new Log())
@ -27,57 +27,66 @@ type Chapter1 =
| 'documentation'
| 'integration'
let chapters: {
type Chapters = {
chapter1?: Chapter1
chapter2?: string
chapter3?: string
} = {}
}
export function TrackChapter(props: {
chapter1?: Chapter1
chapter2?: string
chapter3?: string
}) {
const PageChapterContext = createContext<Chapters>({})
function useChapters(props: Chapters): Chapters {
let chapters = useContext(PageChapterContext)
if (props.chapter1) {
chapters = { chapter2: '', chapter3: '', ...props }
return null
}
if (props.chapter2) {
chapters = { ...chapters, chapter3: '', ...props }
return null
}
if (props.chapter3) {
chapters = { ...chapters, ...props }
}
chapters = { ...chapters, ...props }
return null
return chapters
}
export function TrackPage(props: {
name?: string
export function TrackChapter({
children,
...chaptersProps
}: {
chapter1?: Chapter1
chapter2?: string
chapter3?: string
children: React.ReactNode
}) {
const tag = useContext(TrackingContext)
TrackChapter(props)
const propsFormatted = Object.fromEntries(
Object.entries({ ...chapters, name: props.name }).map(([k, v]) => [
k,
v && toAtString(v),
])
const chapters = useChapters(chaptersProps)
return (
<PageChapterContext.Provider value={chapters}>
{children}
</PageChapterContext.Provider>
)
}
export function TrackPage({
name,
...chapters
}: {
name?: string
} & Chapters) {
const { chapter1, chapter2, chapter3 } = useChapters(chapters)
const tag = useContext(TrackingContext)
useEffect(() => {
tag.page.set(propsFormatted)
tag.dispatch()
}, [
tag,
propsFormatted.name,
propsFormatted.chapter1,
propsFormatted.chapter2,
propsFormatted.chapter3,
])
tag.events.send(
'page.display',
Object.fromEntries(
Object.entries({ chapter1, chapter2, chapter3, name }).map(([k, v]) => [
k,
v && toAtString(v),
])
)
)
}, [tag, name, chapter1, chapter2, chapter3])
return null
}

File diff suppressed because it is too large Load Diff

View File

@ -53,24 +53,22 @@ export default function PageFeedback({ customMessage }: PageFeedbackProps) {
showForm: false,
showThanks: false,
})
const ATTracker = useContext(TrackingContext)
const tag = useContext(TrackingContext)
const handleFeedback = useCallback(
(rating: 'mauvais' | 'moyen' | 'bien' | 'très bien') => {
setFeedbackGivenForUrl(url)
ATTracker.click.set({
chapter1: 'satisfaction',
type: 'action',
name: rating,
tag.events.send('click.action', {
click_chapter1: 'satisfaction',
click: rating,
})
ATTracker.dispatch()
const askDetails = ['mauvais', 'moyen'].includes(rating)
setState({
showThanks: !askDetails,
showForm: askDetails,
})
},
[ATTracker, url]
[tag, url]
)
const openSuggestionForm = useCallback(() => {

View File

@ -50,12 +50,10 @@ export function ShareSimulationPopup({ url }: { url: string }) {
<Button
size="XS"
onPress={() => {
tracker.click.set({
chapter1: 'feature:partage',
type: 'action',
name: 'lien copié',
tracker.events.send('click.action', {
click_chapter1: 'feature:partage',
click: 'lien copié',
})
tracker.dispatch()
navigator.clipboard.writeText(url).catch((err) =>
// eslint-disable-next-line no-console
console.error(err)

View File

@ -78,12 +78,10 @@ export default function ShareOrSaveSimulationBanner() {
light
size="XS"
onPress={(e) => {
tracker.click.set({
chapter1: 'feature:partage',
type: 'action',
name: 'démarré',
tracker.events.send('click.action', {
click_chapter1: 'feature:partage',
click: 'démarré',
})
tracker.dispatch()
startSharing().catch(
// eslint-disable-next-line no-console
(err) => console.error(err)

View File

@ -1,3 +1,4 @@
import { TrackingContext } from '@/ATInternetTracking'
import React, {
createContext,
ReactNode,

View File

@ -70,14 +70,14 @@ export default function CreateCompany({ statut }: CreateCompanyProps) {
'entreprise.page.autoEntrepreneur.description',
'La liste complète des démarches à faire pour devenir {{autoEntrepreneur}}.',
],
{ autoEntrepreneur: t(statut) }
{ autoEntrepreneur: statut }
)
: t(
[
'entreprise.page.description',
"La liste complète des démarches à faire pour créer une {{statut}} auprès de l'administration française.",
],
{ statut: t(statut) }
{ statut: statut }
)
}
/>

View File

@ -57,33 +57,36 @@ export default function Créer() {
<Link to={sitePaths.créer.index}>
<Trans>Retour</Trans>
</Link>
<TrackChapter chapter2="guide" />
<H1>
<Trans i18nKey="formeJuridique.titre">Choix du statut juridique</Trans>
</H1>
<PreviousAnswers />
<FromBottom key={location.pathname}>
<Switch>
<Route path={sitePaths.créer.guideStatut.soleProprietorship}>
<SoleProprietorship />
</Route>
<Route path={sitePaths.créer.guideStatut.directorStatus}>
<DirectorStatus />
</Route>
<Route path={sitePaths.créer.guideStatut.autoEntrepreneur}>
<AutoEntrepreneur />
</Route>
<Route path={sitePaths.créer.guideStatut.multipleAssociates}>
<NumberOfAssociate />
</Route>
<Route path={sitePaths.créer.guideStatut.minorityDirector}>
<MinorityDirector />
</Route>
<Route path={sitePaths.créer.guideStatut.liste}>
<PickLegalStatus />
</Route>
</Switch>
</FromBottom>
<TrackChapter chapter2="guide">
<H1>
<Trans i18nKey="formeJuridique.titre">
Choix du statut juridique
</Trans>
</H1>
<PreviousAnswers />
<FromBottom key={location.pathname}>
<Switch>
<Route path={sitePaths.créer.guideStatut.soleProprietorship}>
<SoleProprietorship />
</Route>
<Route path={sitePaths.créer.guideStatut.directorStatus}>
<DirectorStatus />
</Route>
<Route path={sitePaths.créer.guideStatut.autoEntrepreneur}>
<AutoEntrepreneur />
</Route>
<Route path={sitePaths.créer.guideStatut.multipleAssociates}>
<NumberOfAssociate />
</Route>
<Route path={sitePaths.créer.guideStatut.minorityDirector}>
<MinorityDirector />
</Route>
<Route path={sitePaths.créer.guideStatut.liste}>
<PickLegalStatus />
</Route>
</Switch>
</FromBottom>
</TrackChapter>
</>
)
}

View File

@ -16,20 +16,21 @@ export default function CreateMyCompany() {
return (
<>
<ScrollToTop key={location.pathname} />
<TrackChapter chapter1="creer" />
<Switch>
<Route exact path={sitePaths.créer.index} component={Home} />
{LANDING_LEGAL_STATUS_LIST.map((statut) => (
<Route path={sitePaths.créer[statut]} key={statut}>
<CreationChecklist statut={statut} />
</Route>
))}
<Route path={sitePaths.créer.après} component={AfterRegistration} />
<Route
path={sitePaths.créer.guideStatut.index}
component={GuideStatut}
/>
</Switch>
<TrackChapter chapter1="creer">
<Switch>
<Route exact path={sitePaths.créer.index} component={Home} />
{LANDING_LEGAL_STATUS_LIST.map((statut) => (
<Route path={sitePaths.créer[statut]} key={statut}>
<CreationChecklist statut={statut} />
</Route>
))}
<Route path={sitePaths.créer.après} component={AfterRegistration} />
<Route
path={sitePaths.créer.guideStatut.index}
component={GuideStatut}
/>
</Switch>
</TrackChapter>
</>
)
}

View File

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

View File

@ -27,26 +27,27 @@ export default function Gérer() {
</Link>
)}
<TrackChapter chapter1="gerer" />
<Switch>
<Route exact path={sitePaths.gérer.index} component={Home} />
<Route
path={sitePaths.gérer.sécuritéSociale}
component={SécuritéSociale}
/>
<Route path={sitePaths.gérer.embaucher} component={Embaucher} />
{[
simulateurs['déclaration-charges-sociales-indépendant'],
simulateurs['demande-mobilité'],
].map((p) => (
<TrackChapter chapter1="gerer">
<Switch>
<Route exact path={sitePaths.gérer.index} component={Home} />
<Route
key={p.shortName}
exact
path={p.path}
render={() => <PageData {...p} />}
path={sitePaths.gérer.sécuritéSociale}
component={SécuritéSociale}
/>
))}
</Switch>
<Route path={sitePaths.gérer.embaucher} component={Embaucher} />
{[
simulateurs['déclaration-charges-sociales-indépendant'],
simulateurs['demande-mobilité'],
].map((p) => (
<Route
key={p.shortName}
exact
path={p.path}
render={() => <PageData {...p} />}
/>
))}
</Switch>
</TrackChapter>
</>
)
}

View File

@ -20,8 +20,7 @@ export default function ÉconomieCollaborative() {
: sitePaths.simulateurs.économieCollaborative.index
return (
<>
<TrackChapter chapter1="simulateurs" chapter2="economie_collaborative" />
<TrackChapter chapter1="simulateurs" chapter2="economie_collaborative">
<div css="transform: translateY(2rem)">
<Link exact activeStyle={{ display: 'none' }} to={indexPath}>
{' '}
@ -43,6 +42,6 @@ export default function ÉconomieCollaborative() {
/>
</Switch>
</StoreProvider>
</>
</TrackChapter>
)
}

View File

@ -85,32 +85,33 @@ export default function PageData(props: PageDataProps) {
return (
<CurrentSimulatorDataProvider value={props}>
<TrackChapter {...trackInfo} />
{meta && <Meta page={`simulateur.${title ?? ''}`} {...meta} />}
{title && !inIframe && (
<>
<H1>
{title}
{year}
</H1>
{tooltip && <Intro>{tooltip}</Intro>}
</>
)}
{description && !inIframe && description}
<TrackChapter {...trackInfo}>
{meta && <Meta page={`simulateur.${title ?? ''}`} {...meta} />}
{title && !inIframe && (
<>
<H1>
{title}
{year}
</H1>
{tooltip && <Intro>{tooltip}</Intro>}
</>
)}
{description && !inIframe && description}
<Component />
<Component />
{!inIframe && (
<>
<section>{seoExplanations}</section>
<NextSteps
iframePath={privateIframe ? undefined : iframePath}
nextSteps={nextSteps}
/>
{!inIframe && (
<>
<section>{seoExplanations}</section>
<NextSteps
iframePath={privateIframe ? undefined : iframePath}
nextSteps={nextSteps}
/>
<Spacing lg />
</>
)}
<Spacing lg />
</>
)}
</TrackChapter>
</CurrentSimulatorDataProvider>
)
}

View File

@ -16,7 +16,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { Route } from 'react-router'
import { MemoryRouter, useHistory, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import { TrackPage } from '../../ATInternetTracking'
import { TrackingContext, TrackPage } from '../../ATInternetTracking'
import { hexToHSL } from '../../hexToHSL'
import Créer from '../Creer'
import Documentation from '../Documentation'
@ -28,6 +28,7 @@ import apecLogo from './images/apec.png'
import cciLogo from './images/cci.png'
import minTraLogo from './images/min-tra.jpg'
import poleEmploiLogo from './images/pole-emploi.png'
import { Log } from '@/ATInternetTracking/Tracker'
const LazyColorPicker = lazy(() => import('../Dev/ColorPicker'))
@ -137,33 +138,36 @@ function IntegrationCustomizer() {
<Trans>Prévisualisation</Trans>
</H3>
<PrevisualisationContainer columns={10}>
<MemoryRouter
key={currentModule}
initialEntries={[`/iframes/${currentIframePath}`]}
>
<ThemeColorsProvider
color={color == null ? color : hexToHSL(color)}
<TrackingContext.Provider value={DummyTracker}>
<MemoryRouter
key={currentModule}
initialEntries={[`/iframes/${currentIframePath}`]}
>
<IsEmbeddedProvider isEmbeded>
<Route
path={sitePaths.simulateurs.index}
component={Simulateurs}
/>
<Route path={sitePaths.créer.index} component={Créer} />
<Route
path={sitePaths.documentation.index}
component={Documentation}
/>
<Iframes />
</IsEmbeddedProvider>
</ThemeColorsProvider>
</MemoryRouter>
<ThemeColorsProvider
color={color == null ? color : hexToHSL(color)}
>
<IsEmbeddedProvider isEmbeded>
<Route
path={sitePaths.simulateurs.index}
component={Simulateurs}
/>
<Route path={sitePaths.créer.index} component={Créer} />
<Route
path={sitePaths.documentation.index}
component={Documentation}
/>
<Iframes />
</IsEmbeddedProvider>
</ThemeColorsProvider>
</MemoryRouter>
</TrackingContext.Provider>
</PrevisualisationContainer>
</Grid>
</Grid>
</>
)
}
const DummyTracker = new Log()
const PrevisualisationContainer = styled(Grid)`
background-color: white;

View File

@ -24,8 +24,7 @@ export default function Integration() {
const openJobOffer = (jobOffers as Array<JobOffer>)[0]
return (
<>
<TrackChapter chapter1="integration" />
<TrackChapter chapter1="integration">
<ScrollToTop />
{pathname !== sitePaths.integration.index && (
@ -54,6 +53,6 @@ export default function Integration() {
<Route path={sitePaths.integration.iframe} component={Iframe} />
<Route path={sitePaths.integration.library} component={Library} />
</Switch>
</>
</TrackChapter>
)
}