diff --git a/docs/pages/_app.js b/docs/pages/_app.js index 27343682d9161b..abc350a468e75c 100644 --- a/docs/pages/_app.js +++ b/docs/pages/_app.js @@ -19,6 +19,7 @@ import GoogleAnalytics from 'docs/src/modules/components/GoogleAnalytics'; import { CodeCopyProvider } from '@mui/docs/CodeCopy'; import { ThemeProvider } from 'docs/src/modules/components/ThemeContext'; import { CodeVariantProvider } from 'docs/src/modules/utils/codeVariant'; +import { AnalyticsProvider } from 'docs/src/modules/components/AnalyticsProvider'; import DocsStyledEngineProvider from 'docs/src/modules/utils/StyledEngineProvider'; import createEmotionCache from 'docs/src/createEmotionCache'; import findActivePage from 'docs/src/modules/utils/findActivePage'; @@ -345,18 +346,20 @@ function AppWrapper(props) { defaultUserLanguage={pageProps.userLanguage} translations={pageProps.translations} > - - - - - - {children} - - - - - - + + + + + + + {children} + + + + + + + ); diff --git a/docs/pages/_document.js b/docs/pages/_document.js index 873bbba1fa2bd2..ccf73f6f8fa99c 100644 --- a/docs/pages/_document.js +++ b/docs/pages/_document.js @@ -15,6 +15,7 @@ const PRODUCTION_GA = process.env.DEPLOY_ENV === 'production' || process.env.DEPLOY_ENV === 'staging'; const GOOGLE_ANALYTICS_ID_V4 = PRODUCTION_GA ? 'G-5NXDQLC2ZK' : 'G-XJ83JQEK7J'; +const APOLLO_TRACKING_ID = PRODUCTION_GA ? '68efaf63a6b6a4001571da4a' : 'dev-id'; export default class MyDocument extends Document { render() { @@ -104,10 +105,47 @@ export default class MyDocument extends Document { window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} window.gtag = gtag; + +${/* Set default consent to denied (Google Consent Mode v2) */ ''} +gtag('consent', 'default', { + 'ad_storage': 'denied', + 'ad_user_data': 'denied', + 'ad_personalization': 'denied', + 'analytics_storage': 'denied', + 'wait_for_update': 500 +}); +gtag('set', 'ads_data_redaction', true); +gtag('set', 'url_passthrough', true); + gtag('js', new Date()); gtag('config', '${GOOGLE_ANALYTICS_ID_V4}', { send_page_view: false, }); + +${/* Apollo initialization - called by AnalyticsProvider when consent is granted */ ''} +window.initApollo = function() { + if (window.apolloInitialized) return; + window.apolloInitialized = true; + var n = Math.random().toString(36).substring(7), + o = document.createElement('script'); + o.src = 'https://assets.apollo.io/micro/website-tracker/tracker.iife.js?nocache=' + n; + o.async = true; + o.defer = true; + o.onload = function () { + window.trackingFunctions.onLoad({ appId: '${APOLLO_TRACKING_ID}' }); + }; + document.head.appendChild(o); +}; + +${/* Check localStorage for existing consent and initialize if already granted */ ''} +(function() { + try { + var consent = localStorage.getItem('docs-cookie-consent'); + if (consent === 'analytics') { + window.initApollo(); + } + } catch (e) {} +})(); `, }} /> diff --git a/docs/src/modules/components/AnalyticsProvider.tsx b/docs/src/modules/components/AnalyticsProvider.tsx new file mode 100644 index 00000000000000..5294e179bfe1b8 --- /dev/null +++ b/docs/src/modules/components/AnalyticsProvider.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import useLocalStorageState from '@mui/utils/useLocalStorageState'; + +const COOKIE_CONSENT_KEY = 'docs-cookie-consent'; + +type ConsentStatus = 'analytics' | 'essential' | null; + +function getDoNotTrack(): boolean { + if (typeof window === 'undefined') { + return false; + } + // Check for Do Not Track (DNT) + return navigator.doNotTrack === '1' || (window as { doNotTrack?: string }).doNotTrack === '1'; +} + +// DNT doesn't change during a session, so we can use a simple external store +const subscribeToNothing = () => () => {}; +const getDoNotTrackSnapshot = () => getDoNotTrack(); +const getDoNotTrackServerSnapshot = () => true; // Assume DNT until we know the actual value + +function useDoNotTrack(): boolean { + return React.useSyncExternalStore( + subscribeToNothing, + getDoNotTrackSnapshot, + getDoNotTrackServerSnapshot, + ); +} + +interface AnalyticsContextValue { + consentStatus: ConsentStatus; + hasAnalyticsConsent: boolean; +} + +const AnalyticsContext = React.createContext({ + consentStatus: null, + hasAnalyticsConsent: false, +}); + +export function useAnalyticsConsent() { + return React.useContext(AnalyticsContext); +} + +function CookieConsentDialog({ + open, + onAnalytics, + onEssential, +}: { + open: boolean; + onAnalytics: () => void; + onEssential: () => void; +}) { + if (!open) { + return null; + } + + return ( + + + Cookie Preferences + + + + We use cookies to understand site usage and improve our content. This includes third-party + analytics. + + + + + + + + ); +} + +function updateGoogleConsent(hasAnalytics: boolean) { + if (typeof window !== 'undefined' && typeof window.gtag === 'function') { + window.gtag('consent', 'update', { + ad_storage: 'denied', + ad_user_data: 'denied', + ad_personalization: 'denied', + analytics_storage: hasAnalytics ? 'granted' : 'denied', + }); + + // Initialize Apollo when analytics consent is granted + const win = window as typeof window & { initApollo?: () => void }; + if (hasAnalytics && typeof win.initApollo === 'function') { + win.initApollo(); + } + } +} + +export function AnalyticsProvider({ children }: { children: React.ReactNode }) { + const [consentStatus, setConsentStatus] = useLocalStorageState(COOKIE_CONSENT_KEY, null); + const doNotTrack = useDoNotTrack(); + + // Respect Do Not Track - don't show dialog and treat as essential only + const showDialog = consentStatus === null && !doNotTrack; + + // Update Google consent when status changes or on mount if already set + React.useEffect(() => { + if (doNotTrack) { + // DNT is enabled - always deny analytics + updateGoogleConsent(false); + } else if (consentStatus !== null) { + updateGoogleConsent(consentStatus === 'analytics'); + } + }, [consentStatus, doNotTrack]); + + const handleAnalytics = () => { + setConsentStatus('analytics'); + }; + + const handleEssential = () => { + setConsentStatus('essential'); + }; + + const contextValue = React.useMemo( + () => ({ + consentStatus: doNotTrack ? 'essential' : (consentStatus as ConsentStatus), + hasAnalyticsConsent: !doNotTrack && consentStatus === 'analytics', + }), + [consentStatus, doNotTrack], + ); + + return ( + + {children} + + + ); +}