Share the Django site's analytics consent instead of asking twice (same-origin tchatou-consent)

This commit is contained in:
Jean Chevronnet 2026-07-10 05:28:23 +00:00
parent 56ae05f56d
commit 900859acdf
No known key found for this signature in database
3 changed files with 24 additions and 15 deletions

View file

@ -4,9 +4,12 @@ import { getConfig } from '../core/config';
import { getConsent, setConsent } from '../core/consent';
import { initGa } from '../core/ga';
// First-visit analytics consent. Only shown when Google Analytics is configured
// (config.analytics.gaId) and the visitor hasn't chosen yet. Accepting loads gtag;
// rejecting means it never loads. Reject is as prominent as Accept (CNIL).
// Analytics consent — a FALLBACK. The Django site (tchatou.fr) already asks and
// stores the choice in the shared `tchatou-consent` entry (same origin), so anyone
// arriving from there is never asked again. This only appears when GA is configured
// AND no choice exists yet — i.e. a direct /app/ visit (installed PWA, bookmark) that
// never hit the marketing site. Accepting loads gtag; rejecting means it never loads;
// reject is as prominent as Accept (CNIL). The choice writes the same shared entry.
export function ConsentBanner() {
const { t } = useTranslation();
const [open, setOpen] = useState(() => !!getConfig().analytics?.gaId && getConsent() === 'unset');

View file

@ -1,19 +1,22 @@
// Analytics consent (opt-in). Google Analytics sets cookies, so on an EU/French
// site it must not load until the visitor agrees — gtag is gated on this. Stored in
// localStorage; 'unset' means "not asked yet" (the banner shows). Withdrawable from
// Settings. The first-party cookieless collector does NOT depend on this (no cookie).
const KEY = 'tchatou:analytics-consent';
// Analytics consent (opt-in) for Google Analytics. SHARED with the Django site:
// tchatou.fr and tchatou.fr/app are the same origin, so we read/write the exact same
// localStorage entry the marketing site's static/js/consent.js uses — key
// `tchatou-consent`, value `{v:'granted'|'denied', t:epochMs}`, re-asked after ~6
// months. A visitor who already chose on tchatou.fr is never asked again in the app;
// the app's own banner only appears for someone who opens /app/ directly (e.g. the
// installed PWA) and writes the same shared entry so the choice stays in sync.
const KEY = 'tchatou-consent';
const TTL = 1000 * 60 * 60 * 24 * 180; // re-ask after ~6 months (matches consent.js)
export type Consent = 'granted' | 'denied' | 'unset';
export function getConsent(): Consent {
try {
const v = localStorage.getItem(KEY);
return v === 'granted' || v === 'denied' ? v : 'unset';
} catch {
return 'unset';
}
const o = JSON.parse(localStorage.getItem(KEY) || 'null');
if (o && o.t && Date.now() - o.t < TTL && (o.v === 'granted' || o.v === 'denied')) return o.v;
} catch { /* bad JSON / no storage */ }
return 'unset';
}
export function setConsent(v: 'granted' | 'denied'): void {
try { localStorage.setItem(KEY, v); } catch { /* private mode / quota */ }
try { localStorage.setItem(KEY, JSON.stringify({ v, t: Date.now() })); } catch { /* private mode / quota */ }
}

View file

@ -38,7 +38,10 @@ export function initGa(): void {
s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(id);
document.head.appendChild(s);
w.gtag('js', new Date());
w.gtag('config', id);
// Match the Django site's config: no Google Signals / ad-personalisation, so GA
// sets only the first-party _ga cookies the consent banner declares (and doesn't
// use the region1.analytics.google.com signals endpoint).
w.gtag('config', id, { allow_google_signals: false, allow_ad_personalization_signals: false });
bus.on('connected', () => w.gtag('event', 'login', { method: 'irc' }));
bus.on('buffer.active', (name: unknown) => {
const n = String(name ?? '');