diff --git a/site/netlify.base.toml b/site/netlify.base.toml index 616e82b053..e3ba42ab9d 100644 --- a/site/netlify.base.toml +++ b/site/netlify.base.toml @@ -4,7 +4,7 @@ for = "/*" Content-Security-Policy = """\ default-src 'self' mon-entreprise.fr; \ 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; \ + connect-src 'self' *.incubateur.net raw.githubusercontent.com github.com tag.aticdn.net 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; \ img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com github.com *.s3.amazonaws.com jedonnemonavis.numerique.gouv.fr; \ diff --git a/site/source/components/Provider.tsx b/site/source/components/Provider.tsx index 5704ac5cc3..b8cbef980f 100644 --- a/site/source/components/Provider.tsx +++ b/site/source/components/Provider.tsx @@ -2,21 +2,19 @@ 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' import { BrowserRouter } from 'react-router-dom' +import { TrackingProvider } from '@/components/TrackingProvider' import { ThemeColorsProvider } from '@/components/utils/colors' import { DisableAnimationOnPrintProvider } from '@/components/utils/DisableAnimationContext' import DesignSystemThemeProvider from '@/design-system/root' 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 { ErrorFallback } from './ErrorPage' import { IframeResizer } from './IframeResizer' import { ServiceWorker } from './ServiceWorker' @@ -105,52 +103,3 @@ function BrowserRouterProvider({ ) } - -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.VITE_AT_INTERNET_SITE_ID - - 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/TrackingProvider.tsx b/site/source/components/TrackingProvider.tsx new file mode 100644 index 0000000000..307ed6a48d --- /dev/null +++ b/site/source/components/TrackingProvider.tsx @@ -0,0 +1,85 @@ +import i18next from 'i18next' +import { useEffect, useState } from 'react' + +import { TrackingContext } from '@/components/ATInternetTracking' +import { + ATTracker, + createTracker, +} from '@/components/ATInternetTracking/Tracker' +import * as safeLocalStorage from '@/storage/safeLocalStorage' + +export function TrackingProvider({ children }: { children: React.ReactNode }) { + const [tracker, setTracker] = useState(null) + const [script, setScript] = useState(null) + const [injected, setInjected] = useState(false) + + 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.VITE_AT_INTERNET_SITE_ID + + 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') + } + + setScript(script) + }, []) + + useEffect(() => { + if (script) { + if (injected) { + return () => { + document.body.removeChild(script) + } + } + + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then(() => { + requestIdleCallback(() => { + document.body.appendChild(script) + setInjected(true) + }) + }) + .catch((error) => { + console.error( + 'Impossible d’initialiser le suivi car le service worker n’a pas démarré', + error + ) + }) + } else { + document.body.appendChild(script) + setInjected(true) + } + } + }, [script, injected]) + + if (!tracker) { + return <>{children} + } + + return ( + + {children} + + ) +} diff --git a/site/source/entries/entry-sw.ts b/site/source/entries/entry-sw.ts index b5776da6b3..3bf4a1b9a0 100644 --- a/site/source/entries/entry-sw.ts +++ b/site/source/entries/entry-sw.ts @@ -56,9 +56,7 @@ setDefaultHandler( ) const networkFirstJS = new Route( - ({ sameOrigin, url }) => { - return sameOrigin && /assets\/.*\.js$/.test(url.pathname) - }, + ({ sameOrigin, url }) => sameOrigin && /assets\/.*\.js$/.test(url.pathname), new NetworkFirst({ cacheName: 'js-cache', plugins: [ @@ -72,6 +70,24 @@ const networkFirstJS = new Route( registerRoute(networkFirstJS) +const networkFirstPiano = new Route( + ({ url }) => url.hostname === 'tag.aticdn.net', + new NetworkFirst({ + fetchOptions: { + mode: 'cors', + }, + cacheName: 'piano-cache', + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 1 * MONTH, + maxEntries: 40, + }), + ], + }) +) + +registerRoute(networkFirstPiano) + const staleWhileRevalidate = new Route( ({ request, sameOrigin, url }) => { return (