Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions docs/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -345,18 +346,20 @@ function AppWrapper(props) {
defaultUserLanguage={pageProps.userLanguage}
translations={pageProps.translations}
>
<CodeCopyProvider>
<CodeVariantProvider>
<PageContext.Provider value={pageContextValue}>
<ThemeProvider>
<DocsStyledEngineProvider cacheLtr={emotionCache}>
{children}
<GoogleAnalytics />
</DocsStyledEngineProvider>
</ThemeProvider>
</PageContext.Provider>
</CodeVariantProvider>
</CodeCopyProvider>
<AnalyticsProvider>
<CodeCopyProvider>
<CodeVariantProvider>
<PageContext.Provider value={pageContextValue}>
<ThemeProvider>
<DocsStyledEngineProvider cacheLtr={emotionCache}>
{children}
<GoogleAnalytics />
</DocsStyledEngineProvider>
</ThemeProvider>
</PageContext.Provider>
</CodeVariantProvider>
</CodeCopyProvider>
</AnalyticsProvider>
</DocsProvider>
</React.Fragment>
);
Expand Down
38 changes: 38 additions & 0 deletions docs/pages/_document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {}
})();
`,
}}
/>
Expand Down
164 changes: 164 additions & 0 deletions docs/src/modules/components/AnalyticsProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<AnalyticsContextValue>({
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 (
<Dialog
open={open}
hideBackdrop
disableScrollLock
disableEscapeKeyDown
sx={{
pointerEvents: 'none',
'& .MuiDialog-container': {
justifyContent: 'flex-end',
alignItems: 'flex-end',
},
'& .MuiPaper-root': {
pointerEvents: 'auto',
m: 2,
maxWidth: 340,
},
}}
aria-labelledby="cookie-consent-dialog-title"
aria-describedby="cookie-consent-dialog-description"
>
<DialogTitle id="cookie-consent-dialog-title" sx={{ pb: 1 }}>
Cookie Preferences
</DialogTitle>
<DialogContent sx={{ pb: 1 }}>
<DialogContentText id="cookie-consent-dialog-description" variant="body2">
We use cookies to understand site usage and improve our content. This includes third-party
analytics.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onEssential} color="inherit" size="small">
Essential only
</Button>
<Button onClick={onAnalytics} variant="contained" size="small">
Allow analytics
</Button>
</DialogActions>
</Dialog>
);
}

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<AnalyticsContextValue>(
() => ({
consentStatus: doNotTrack ? 'essential' : (consentStatus as ConsentStatus),
hasAnalyticsConsent: !doNotTrack && consentStatus === 'analytics',
}),
[consentStatus, doNotTrack],
);

return (
<AnalyticsContext.Provider value={contextValue}>
{children}
<CookieConsentDialog
open={showDialog}
onAnalytics={handleAnalytics}
onEssential={handleEssential}
/>
</AnalyticsContext.Provider>
);
}
Loading