Skip to content

Commit

Permalink
feat: added paradata tracking (#163)
Browse files Browse the repository at this point in the history
* feat: added paradata tracking

* chore: fixed yarn lock
  • Loading branch information
chloe-renaud authored Nov 21, 2024
1 parent 4c3ab3e commit 1c9c38c
Show file tree
Hide file tree
Showing 47 changed files with 1,660 additions and 283 deletions.
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ VITE_VISUALIZE_DISABLED=
# Temporary env var used only in vite.config.js to indicate base, in app we use the native import.meta.env.BASE_URL, by default there is no base path ("")
# Waiting vite-env support dynamic base path
VITE_BASE_PATH=
VITE_REVIEW_IDENTITY_PROVIDER=
VITE_REVIEW_IDENTITY_PROVIDER=
# When VITE_TELEMETRY_DISABLED equals true we disable telemetry. (default: enabled)
VITE_TELEMETRY_DISABLED=
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
// see https://typescript-eslint.netlify.app/rules/no-unused-vars/
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_$" }],
'@typescript-eslint/no-explicit-any': ['off'],
'@typescript-eslint/no-namespace': ['off'],
'react-refresh/only-export-components': [
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
"prebuild": "react-dsfr update-icons",
"generate-api-client": "tsx scripts/generate-api-client.ts",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@codegouvfr/react-dsfr": "^1.13.6",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@inseefr/lunatic": "^3.4.4",
"@inseefr/lunatic-dsfr": "^2.4.1",
"@inseefr/lunatic": "^3.4.7",
"@inseefr/lunatic-dsfr": "^2.4.2",
"@mui/material": "^5.16.7",
"@tanstack/react-query": "^5.52.0",
"@tanstack/react-router": "^1.49.2",
Expand All @@ -46,6 +47,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/he": "^1.2.3",
"@types/node": "^22.7.3",
"@types/react": "^18.3.9",
Expand Down
63 changes: 63 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { TelemetryProvider } from '@/contexts/TelemetryContext'
import { OidcProvider } from '@/oidc'
import { routeTree } from '@/router/router'
import { MuiDsfrThemeProvider } from '@codegouvfr/react-dsfr/mui'
import { startReactDsfr } from '@codegouvfr/react-dsfr/spa'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
Link,
RouterProvider,
createRouter,
type LinkProps,
} from '@tanstack/react-router'

startReactDsfr({
defaultColorScheme: 'system',
Link,
})

declare module '@codegouvfr/react-dsfr/spa' {
interface RegisterLink {
Link: (props: LinkProps) => JSX.Element
}
}

const queryClient = new QueryClient({
defaultOptions: {
mutations: {
networkMode: 'always',
},
},
})

const router = createRouter({
routeTree,
context: {
queryClient,
},
defaultPreloadStaleTime: 0,
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

/** Wraps and inits the providers used in the app */
export function App() {
return (
<MuiDsfrThemeProvider>
<QueryClientProvider client={queryClient}>
<OidcProvider>
<TelemetryProvider>
<RouterProvider
router={router}
basepath={import.meta.env.VITE_BASE_PATH}
/>
</TelemetryProvider>
</OidcProvider>
</QueryClientProvider>
</MuiDsfrThemeProvider>
)
}
5 changes: 5 additions & 0 deletions src/constants/mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum MODE_TYPE {
COLLECT = 'collect',
REVIEW = 'review',
VISUALIZE = 'visualize',
}
6 changes: 6 additions & 0 deletions src/constants/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum PAGE_TYPE {
WELCOME = 'welcomePage',
VALIDATION = 'validationPage',
END = 'endPage',
LUNATIC = 'lunaticPage',
}
13 changes: 13 additions & 0 deletions src/constants/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export enum TELEMETRY_EVENT_TYPE {
CONTACT_SUPPORT = 'contact-support',
CONTROL = 'control',
CONTROL_SKIP = 'control-skip',
EXIT = 'exit',
INIT = 'initialized',
INPUT = 'input',
NEW_PAGE = 'new-page',
}

export enum TELEMETRY_EVENT_EXIT_SOURCE {
LOGOUT = 'logout',
}
52 changes: 52 additions & 0 deletions src/contexts/TelemetryContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { computeInitEvent } from '@/utils/telemetry'
import '@testing-library/jest-dom'
import { renderHook } from '@testing-library/react'
import { TelemetryContext, useTelemetry } from './TelemetryContext'

describe('Telemetry context', () => {
test('push events', () => {
const mock = vi.fn()

const wrapper = ({ children }: { children: React.ReactNode }) => (
<TelemetryContext.Provider
value={{
isTelemetryDisabled: false,
pushEvent: mock,
setDefaultValues: () => {},
}}
>
{children}
</TelemetryContext.Provider>
)

const { result } = renderHook(() => useTelemetry(), { wrapper })

const myEvent = computeInitEvent()
result.current.pushEvent(myEvent)

expect(mock).toHaveBeenCalledWith(myEvent)
})

test('set default values', () => {
const mock = vi.fn()

const wrapper = ({ children }: { children: React.ReactNode }) => (
<TelemetryContext.Provider
value={{
isTelemetryDisabled: false,
pushEvent: () => {},
setDefaultValues: mock,
}}
>
{children}
</TelemetryContext.Provider>
)

const { result } = renderHook(() => useTelemetry(), { wrapper })

const myValues = { idSU: 'abc' }
result.current.setDefaultValues(myValues)

expect(mock).toHaveBeenCalledWith(myValues)
})
})
104 changes: 104 additions & 0 deletions src/contexts/TelemetryContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable react-refresh/only-export-components */
import { addParadata } from '@/api/07-paradata-events'
import { useBatch } from '@/shared/hooks/useBatch'
import type {
DefaultParadataValues,
TelemetryEvent,
TelemetryParadata,
} from '@/types/telemetry'
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react'

type TelemetryContextType = {
isTelemetryDisabled: boolean
pushEvent: (e: TelemetryParadata) => void | Promise<boolean>
setDefaultValues: (e: DefaultParadataValues) => void
triggerBatchTelemetryCallback?: () => Promise<void>
}

/** Mandatory values used as a context's last-resort fallback */
const defaultValues = {
isTelemetryDisabled: true,
pushEvent: (_: TelemetryParadata) => {},
setDefaultValues: (_: DefaultParadataValues) => {},
}

/**
* Exposes shared functions to handle telemetry events.
*
* Should be used with the useBatch hook to reduce external API load.
*/
export const TelemetryContext: React.Context<TelemetryContextType> =
createContext(defaultValues)

/**
* Returns the current telemetry context value.
*
* @version 1.2.4
* @see https://react.dev/reference/react/useContext
*/
export function useTelemetry() {
return useContext(TelemetryContext)
}

/** Initializes the telemetry context with a batch system */
export function TelemetryProvider({
children,
}: Readonly<{
children: React.ReactElement
}>) {
const isTelemetryDisabled = import.meta.env.VITE_TELEMETRY_DISABLED === 'true'

const [defaultValues, setDefaultValues] = useState<DefaultParadataValues>({
userAgent: navigator.userAgent,
})

/** Push events to telemetry API after an arbitrary number of events or a delay. */
const pushEvents = useCallback(async (events: Array<TelemetryEvent>) => {
if (events.length > 0) {
return addParadata({ idSU: events[0].idSU, events })
}
}, [])

const { addDatum, triggerTimeoutEvent } = useBatch(pushEvents)

/** Add the event to a batch mechanism. */
const pushEvent = useCallback(
(event: TelemetryParadata) => {
addDatum({ ...defaultValues, ...event })
},
[addDatum, defaultValues]
)

/** Add values that will be appended to every telemetry event (e.g. user id) */
const updateDefaultValues = useCallback(
(newDefaultValues: DefaultParadataValues) => {
setDefaultValues((oldValues: DefaultParadataValues) => ({
...oldValues,
...newDefaultValues,
}))
},
[]
)

const telemetryContextValues = useMemo(
() => ({
isTelemetryDisabled,
pushEvent,
setDefaultValues: updateDefaultValues,
triggerBatchTelemetryCallback: triggerTimeoutEvent,
}),
[isTelemetryDisabled, pushEvent, triggerTimeoutEvent, updateDefaultValues]
)

return (
<TelemetryContext.Provider value={telemetryContextValues}>
{children}
</TelemetryContext.Provider>
)
}
18 changes: 8 additions & 10 deletions src/i18n/resources/en.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PAGE_TYPE } from '@/constants/page'
import type { Translations } from '@/i18n/types'

export const translations: Translations<'en'> = {
Expand Down Expand Up @@ -90,23 +91,20 @@ export const translations: Translations<'en'> = {
SurveyContainer: {
'button continue label': ({ currentPage }) => {
switch (currentPage) {
case 'welcomePage':
case PAGE_TYPE.WELCOME:
return 'Start'
case 'lunaticPage':
case PAGE_TYPE.LUNATIC:
return 'Continue'
case 'endPage':
case PAGE_TYPE.END:
return 'Download the receipt'
case 'validationPage':
case PAGE_TYPE.VALIDATION:
return 'Submit my responses'
}
},
'button continue title': ({ currentPage }) => {
switch (currentPage) {
case 'endPage':
return 'Download the acknowledgment of receipt'
default:
return 'Proceed to the next step'
}
if (currentPage === PAGE_TYPE.END)
return 'Download the acknowledgment of receipt'
return 'Proceed to the next step'
},
'button download': 'Download data',
'button expand': 'Expand view',
Expand Down
18 changes: 8 additions & 10 deletions src/i18n/resources/fr.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PAGE_TYPE } from '@/constants/page'
import type { Translations } from '@/i18n/types'

export const translations: Translations<'fr'> = {
Expand Down Expand Up @@ -89,23 +90,20 @@ export const translations: Translations<'fr'> = {
SurveyContainer: {
'button continue label': ({ currentPage }) => {
switch (currentPage) {
case 'welcomePage':
case PAGE_TYPE.WELCOME:
return 'Commencer'
case 'lunaticPage':
case PAGE_TYPE.LUNATIC:
return 'Continuer'
case 'endPage':
case PAGE_TYPE.END:
return "Télécharger l'accusé de réception"
case 'validationPage':
case PAGE_TYPE.VALIDATION:
return 'Envoyer mes réponses'
}
},
'button continue title': ({ currentPage }) => {
switch (currentPage) {
case 'endPage':
return "Télécharger l'accusé de réception"
default:
return "Passer à l'étape suivante"
}
if (currentPage === PAGE_TYPE.END)
return "Télécharger l'accusé de réception"
return "Passer à l'étape suivante"
},
'button download': 'Télécharger les données',

Expand Down
Loading

0 comments on commit 1c9c38c

Please sign in to comment.