Compare commits
1
Commits
master
...
11b5734812
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11b5734812 |
@@ -227,6 +227,8 @@ See [server-infra IMPLEMENTATION.md](https://git.aimloperations.com/ai_ml_operat
|
|||||||
- Script: `https://tianji.aimloperations.com/tracker.js`
|
- Script: `https://tianji.aimloperations.com/tracker.js`
|
||||||
- **Page views** always load on prod/beta (no consent gate)
|
- **Page views** always load on prod/beta (no consent gate)
|
||||||
- **Custom events / identify** require analytics consent (`AnalyticsConsentBanner` + `trackEvent` / `identifyUser`)
|
- **Custom events / identify** require analytics consent (`AnalyticsConsentBanner` + `trackEvent` / `identifyUser`)
|
||||||
|
- Consent choice stored in `localStorage` key `hesychia_analytics_consent_v1` (`granted` / `denied`)
|
||||||
|
- Banner links to `/terms_of_service/#analytics` for the always-on vs optional split
|
||||||
|
|
||||||
## Related repos
|
## Related repos
|
||||||
|
|
||||||
|
|||||||
@@ -29,19 +29,23 @@ describe('AnalyticsConsentBanner', () => {
|
|||||||
renderBanner();
|
renderBanner();
|
||||||
expect(screen.getByRole('dialog', { name: /analytics consent/i })).toBeInTheDocument();
|
expect(screen.getByRole('dialog', { name: /analytics consent/i })).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Help us improve Hesychia/i)).toBeInTheDocument();
|
expect(screen.getByText(/Help us improve Hesychia/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link', { name: /Analytics and Cookies/i })).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/terms_of_service/#analytics',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides after accept and persists granted', () => {
|
it('hides after accept and persists granted', () => {
|
||||||
renderBanner();
|
renderBanner();
|
||||||
fireEvent.click(screen.getByRole('button', { name: /accept analytics/i }));
|
fireEvent.click(screen.getByRole('button', { name: /accept analytics/i }));
|
||||||
expect(screen.queryByRole('dialog', { name: /analytics consent/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog', { name: /analytics consent/i })).not.toBeInTheDocument();
|
||||||
expect(localStorage.getItem('hesychia_analytics_consent')).toBe('granted');
|
expect(localStorage.getItem('hesychia_analytics_consent_v1')).toBe('granted');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides after decline and persists denied', () => {
|
it('hides after decline and persists denied', () => {
|
||||||
renderBanner();
|
renderBanner();
|
||||||
fireEvent.click(screen.getByRole('button', { name: /decline/i }));
|
fireEvent.click(screen.getByRole('button', { name: /decline/i }));
|
||||||
expect(screen.queryByRole('dialog', { name: /analytics consent/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog', { name: /analytics consent/i })).not.toBeInTheDocument();
|
||||||
expect(localStorage.getItem('hesychia_analytics_consent')).toBe('denied');
|
expect(localStorage.getItem('hesychia_analytics_consent_v1')).toBe('denied');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,11 +51,15 @@ const AnalyticsConsentBanner = (): JSX.Element | null => {
|
|||||||
We always collect anonymous page views and referral data to understand traffic.
|
We always collect anonymous page views and referral data to understand traffic.
|
||||||
With your consent, we also collect how you use features (sign-in, checkout, chat
|
With your consent, we also collect how you use features (sign-in, checkout, chat
|
||||||
actions, and similar). We do not use advertising cookies. When you accept and are
|
actions, and similar). We do not use advertising cookies. When you accept and are
|
||||||
signed in, we may associate that activity with your account. See our{' '}
|
signed in, we may associate that activity with your account. See the{' '}
|
||||||
<Link component={RouterLink} to="/terms_of_service/" underline="hover">
|
<Link
|
||||||
Terms of Service
|
component={RouterLink}
|
||||||
|
to="/terms_of_service/#analytics"
|
||||||
|
underline="hover"
|
||||||
|
>
|
||||||
|
Analytics and Cookies
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
for details.
|
section of our Terms of Service for details.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ flexShrink: 0 }}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ flexShrink: 0 }}>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('analytics event helpers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('tracks custom events when consent granted', () => {
|
it('tracks custom events when consent granted', () => {
|
||||||
localStorage.setItem('hesychia_analytics_consent', 'granted');
|
localStorage.setItem('hesychia_analytics_consent_v1', 'granted');
|
||||||
const track = jest.fn();
|
const track = jest.fn();
|
||||||
window.tianji = { track, identify: jest.fn() };
|
window.tianji = { track, identify: jest.fn() };
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ describe('analytics event helpers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('queues events until tianji is ready then flushes after consent', () => {
|
it('queues events until tianji is ready then flushes after consent', () => {
|
||||||
localStorage.setItem('hesychia_analytics_consent', 'granted');
|
localStorage.setItem('hesychia_analytics_consent_v1', 'granted');
|
||||||
|
|
||||||
trackEvent('Checkout Started');
|
trackEvent('Checkout Started');
|
||||||
const track = jest.fn();
|
const track = jest.fn();
|
||||||
@@ -49,7 +49,7 @@ describe('analytics event helpers', () => {
|
|||||||
identifyUser({ userId: 'u1' });
|
identifyUser({ userId: 'u1' });
|
||||||
expect(identify).not.toHaveBeenCalled();
|
expect(identify).not.toHaveBeenCalled();
|
||||||
|
|
||||||
localStorage.setItem('hesychia_analytics_consent', 'granted');
|
localStorage.setItem('hesychia_analytics_consent_v1', 'granted');
|
||||||
identifyUser({ userId: 'u1' });
|
identifyUser({ userId: 'u1' });
|
||||||
expect(identify).toHaveBeenCalledWith({ userId: 'u1' });
|
expect(identify).toHaveBeenCalledWith({ userId: 'u1' });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import {
|
|||||||
isAnalyticsEnvironment,
|
isAnalyticsEnvironment,
|
||||||
setAnalyticsConsent,
|
setAnalyticsConsent,
|
||||||
ANALYTICS_CONSENT_CHANGED_EVENT,
|
ANALYTICS_CONSENT_CHANGED_EVENT,
|
||||||
|
CONSENT_STORAGE_KEY,
|
||||||
} from './analyticsConsent';
|
} from './analyticsConsent';
|
||||||
|
|
||||||
const CONSENT_KEY = 'hesychia_analytics_consent';
|
|
||||||
|
|
||||||
describe('isAnalyticsEnvironment', () => {
|
describe('isAnalyticsEnvironment', () => {
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
|
|
||||||
@@ -64,26 +63,33 @@ describe('analytics consent storage', () => {
|
|||||||
expect(hasAnalyticsConsent()).toBe(true);
|
expect(hasAnalyticsConsent()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reads granted and denied from localStorage', () => {
|
it('reads granted and denied from versioned localStorage key', () => {
|
||||||
localStorage.setItem(CONSENT_KEY, 'granted');
|
localStorage.setItem(CONSENT_STORAGE_KEY, 'granted');
|
||||||
expect(getAnalyticsConsent()).toBe('granted');
|
expect(getAnalyticsConsent()).toBe('granted');
|
||||||
expect(isAnalyticsConsentResolved()).toBe(true);
|
expect(isAnalyticsConsentResolved()).toBe(true);
|
||||||
|
|
||||||
localStorage.setItem(CONSENT_KEY, 'denied');
|
localStorage.setItem(CONSENT_STORAGE_KEY, 'denied');
|
||||||
expect(getAnalyticsConsent()).toBe('denied');
|
expect(getAnalyticsConsent()).toBe('denied');
|
||||||
expect(hasAnalyticsConsent()).toBe(false);
|
expect(hasAnalyticsConsent()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('migrates legacy consent key to v1', () => {
|
||||||
|
localStorage.setItem('hesychia_analytics_consent', 'granted');
|
||||||
|
expect(getAnalyticsConsent()).toBe('granted');
|
||||||
|
expect(localStorage.getItem(CONSENT_STORAGE_KEY)).toBe('granted');
|
||||||
|
expect(localStorage.getItem('hesychia_analytics_consent')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('persists consent and dispatches change event', () => {
|
it('persists consent and dispatches change event', () => {
|
||||||
const listener = jest.fn();
|
const listener = jest.fn();
|
||||||
window.addEventListener(ANALYTICS_CONSENT_CHANGED_EVENT, listener);
|
window.addEventListener(ANALYTICS_CONSENT_CHANGED_EVENT, listener);
|
||||||
|
|
||||||
setAnalyticsConsent('denied');
|
setAnalyticsConsent('denied');
|
||||||
expect(localStorage.getItem(CONSENT_KEY)).toBe('denied');
|
expect(localStorage.getItem(CONSENT_STORAGE_KEY)).toBe('denied');
|
||||||
expect(listener).toHaveBeenCalled();
|
expect(listener).toHaveBeenCalled();
|
||||||
|
|
||||||
setAnalyticsConsent('granted');
|
setAnalyticsConsent('granted');
|
||||||
expect(localStorage.getItem(CONSENT_KEY)).toBe('granted');
|
expect(localStorage.getItem(CONSENT_STORAGE_KEY)).toBe('granted');
|
||||||
|
|
||||||
window.removeEventListener(ANALYTICS_CONSENT_CHANGED_EVENT, listener);
|
window.removeEventListener(ANALYTICS_CONSENT_CHANGED_EVENT, listener);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
export type AnalyticsConsentStatus = 'granted' | 'denied' | 'pending';
|
export type AnalyticsConsentStatus = 'granted' | 'denied' | 'pending';
|
||||||
|
|
||||||
const CONSENT_STORAGE_KEY = 'hesychia_analytics_consent';
|
/** Versioned so policy/copy changes can force a fresh choice later. */
|
||||||
|
export const CONSENT_STORAGE_KEY = 'hesychia_analytics_consent_v1';
|
||||||
|
/** Pre-#37 key from beta Tianji baseline (#42); migrated once on read. */
|
||||||
|
const LEGACY_CONSENT_STORAGE_KEY = 'hesychia_analytics_consent';
|
||||||
|
|
||||||
export const ANALYTICS_CONSENT_CHANGED_EVENT = 'hesychia-analytics-consent-changed';
|
export const ANALYTICS_CONSENT_CHANGED_EVENT = 'hesychia-analytics-consent-changed';
|
||||||
|
|
||||||
/** Prod + beta builds only (CRA NODE_ENV is production for both). */
|
/** Prod + beta builds only (CRA NODE_ENV is production for both). */
|
||||||
@@ -14,17 +18,27 @@ export const isAnalyticsEnvironment = (): boolean => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAnalyticsConsent = (): AnalyticsConsentStatus => {
|
const readStoredConsent = (): AnalyticsConsentStatus | null => {
|
||||||
if (!isAnalyticsEnvironment()) return 'granted';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value = localStorage.getItem(CONSENT_STORAGE_KEY);
|
const current = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||||
if (value === 'granted' || value === 'denied') return value;
|
if (current === 'granted' || current === 'denied') return current;
|
||||||
|
|
||||||
|
const legacy = localStorage.getItem(LEGACY_CONSENT_STORAGE_KEY);
|
||||||
|
if (legacy === 'granted' || legacy === 'denied') {
|
||||||
|
localStorage.setItem(CONSENT_STORAGE_KEY, legacy);
|
||||||
|
localStorage.removeItem(LEGACY_CONSENT_STORAGE_KEY);
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* localStorage unavailable */
|
/* localStorage unavailable */
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'pending';
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAnalyticsConsent = (): AnalyticsConsentStatus => {
|
||||||
|
if (!isAnalyticsEnvironment()) return 'granted';
|
||||||
|
return readStoredConsent() ?? 'pending';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasAnalyticsConsent = (): boolean => getAnalyticsConsent() === 'granted';
|
export const hasAnalyticsConsent = (): boolean => getAnalyticsConsent() === 'granted';
|
||||||
@@ -34,6 +48,7 @@ export const isAnalyticsConsentResolved = (): boolean => getAnalyticsConsent() !
|
|||||||
export const setAnalyticsConsent = (status: 'granted' | 'denied'): void => {
|
export const setAnalyticsConsent = (status: 'granted' | 'denied'): void => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(CONSENT_STORAGE_KEY, status);
|
localStorage.setItem(CONSENT_STORAGE_KEY, status);
|
||||||
|
localStorage.removeItem(LEGACY_CONSENT_STORAGE_KEY);
|
||||||
} catch {
|
} catch {
|
||||||
/* localStorage unavailable */
|
/* localStorage unavailable */
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user