DataTalksClub / DataTalksClub/website
Wire GA4 acquisition-only analytics behind the existing consent preference center
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Parent decision: #27 (closed)
Existing consent foundation: #125 (closed)
Privacy authority prerequisite: #254
Product outcome
Anonymous visitors to eligible public production pages may opt into one deliberately narrow Google Analytics 4 acquisition stream. Before an affirmative choice, after rejection, and after withdrawal, the browser makes no Google Analytics request and creates no optional analytics cookie. Allowing analytics sends only an explicit public-page view with bounded acquisition fields; it never joins analytics to an account, registration, learner record, editorial Person, credential, or internal identifier.
The default remains disabled. Code delivery does not activate a production property and does not constitute legal/privacy approval.
Normative authority
_docs/specs/open-decisions.md, decision 16: acquisition-only GA4 is approved behind the existing preference center; enhanced measurement is off; development/previews never send production analytics._docs/specs/01-platform-architecture.md, Configuration: environment variables are bootstrap-only; safe operational configuration uses typed validation, explicit defaults, and audit history._docs/specs/02-url-link-seo-compatibility.md: query/tracking values do not create public cache variants; previews remain noindex and must not leak preview tokens._docs/specs/06-studio-and-admin-api.md: manageable integration configuration has shared service, permission, validation, audit, Studio, admin API, and OpenAPI parity._docs/specs/07-security-privacy-operations.md: no secrets, tokens, account/profile/registration data, or direct identifiers in analytics; processor, region, transfer, purpose, lawful-basis, notice, retention, and rights facts require explicit authority._docs/specs/10-verification-strategy.mdand_docs/PROCESS.md: deterministic network-negative coverage, current-base verification planning, independent testing, and rendered evidence.- #125: reuse the existing
dtc_analytics_consentversion-1 allow/deny preference, reopen/withdraw behavior, optional-cookie cleanup, legal footer control, and accessible dialog. Do not introduce a second consent store or UI.
Current-state correction
At current remote main (face8e4808d65afbf0374d1ced7a88079950d663), the consent preference exists but there is no GA4 loader or measurement setting. The Privacy page and source-policy guard correctly describe/enforce provider absence.
The earlier staged candidate and its evidence were based on d24808c374b7deb220cb57e365760382ad66d06b; it is not present on current main. Its environment-variable configuration conflicts with specification 01 and #125's typed Studio/admin parity requirement. Its split-string provider hostname also bypassed rather than reconciled core/source_policy.py. Earlier tester/PM reports therefore cannot be reused as current acceptance.
Scope
Typed, disabled-by-default configuration
Add exactly two public-safe operational settings through the existing registry/service and its Studio/admin API/OpenAPI adapters:
site.analytics.ga4_enabled: boolean, defaultfalse;site.analytics.ga4_measurement_id: string, default empty, accepting only the completeG-[A-Z0-9]{8,}grammar with no whitespace or hidden/control characters.
The pair is necessary but not sufficient to run: analytics is eligible only when both settings are valid, runtime is production, the request is an anonymous eligible public document, and consent is v1.allow. Missing, partial, malformed, conflicting, unavailable, or stale configuration fails closed. The measurement ID is public configuration, not a secret, but writes still use the existing settings permission, service, revision/idempotency behavior, and redacted audit shape. Do not add GA4_MEASUREMENT_ID or another non-bootstrap environment variable.
Non-production runtimes remain network-disabled even if test data contains enabled settings. No test or implementation action configures Google, AWS, DNS, a deployed task, or a production database.
Eligible request boundary
Expose the public measurement configuration only on anonymous public HTML documents. Omit it from authenticated requests; account, Studio, admin, API, learner, registration, preview, token-bearing, and other private/no-store surfaces; unsafe methods; and any request carrying Authorization, session/auth/CSRF, preview/management, signed, or unknown credential-shaped state. The consent preference cookie alone does not make an otherwise anonymous request private.
No analytics value enters a public cache key. A cached anonymous document must not contain account-specific state. Error, denial, or settings-read failure must leave analytics absent without breaking the public page.
Consent-controlled local loader
Add one repository-owned loader. It may expose the configured public measurement ID in the eligible document, but it must not create a Google element, request, data-layer command, or optional cookie until the existing preference is affirmatively v1.allow.
- Allowing from the open dialog starts the eligible loader without requiring a reload.
- A persisted valid allow choice may start it on the next eligible page load.
- Rejecting or withdrawing prevents all later sends, expires recognized first-party analytics cookies using #125's domain/path behavior, and leaves ordinary site functionality intact. It cannot claim to erase data already sent to the processor.
- Malformed/unknown/future consent values are unset and fail closed.
- JavaScript disabled means no analytics request and no loss of page content/functionality.
Do not use Google Consent Mode, a pre-consent ping, Google Tag Manager container, inline executable administrator input, or a remote tag-manager editor.
Acquisition-only payload
After consent, initialize GA4 with automatic page views, Google signals, ad-personalization signals, and every repository-controlled behavioral listener disabled, then issue exactly one explicit page_view per eligible document load.
The event may contain only:
- public document title;
- production origin plus canonical public path;
- origin-only HTTP(S) referrer; and
- the first value of these exact UTM keys:
utm_source,utm_medium,utm_campaign,utm_term,utm_content,utm_id,utm_source_platform,utm_creative_format,utm_marketing_tactic, each decoded safely, control-character-free, and capped at 128 characters.
Drop all other query parameters, duplicate values after the first, fragments, userinfo, referrer path/query/fragment, and malformed or non-HTTP(S) referrers. Never send email, name, profile/country value, account/database/editorial ID, registration/enrollment/submission value, session/CSRF/consent/token/cookie value, preview secret, Slack secret, complete arbitrary URL, or custom user ID.
GA4 may create its standard consented session/client cookies and necessarily receives ordinary network transport metadata. Public/legal copy and the privacy authority register must describe that truthfully; do not claim the processor receives no IP/network metadata.
CSP and source-policy reconciliation
Update CSP only for the exact GA4 script and collection origins required by the owned loader; preserve all other directives and deny unrelated Google/provider origins. Reconcile core/source_policy.py explicitly:
- allow the reviewed provider origins only in the owned GA4 loader and exact CSP configuration;
- continue rejecting hard-coded measurement IDs, analytics hosts in unrelated templates/static/runtime configuration, extra analytics cookies, and any unowned loader or provider path;
- do not evade the guard with string concatenation, encoding, aliases, or generated fragments.
Privacy inventory and public copy
After #254's canonical source-only register is accepted, merged, and green, update that canonical register and its deterministic projection for this planned/disabled-by-default GA4 boundary. Preserve every unresolved human_required processor, region, transfer, safeguard, lawful-basis, notice, retention, rights, and review field. Update /privacy to describe the optional processor, allowed acquisition fields, standard consented cookies/network metadata, withdrawal limit, and non-production boundary without representing HUMAN facts as approved.
Non-goals
- No enhanced-measurement event, scroll, click, outbound-link, site-search, video, download, form, conversion, advertising, remarketing, cross-site, fingerprinting, demographic, profile, or account-join tracking.
- No server-side analytics, Measurement Protocol, persistent attribution model, CRM/newsletter integration, custom event taxonomy, or analytics dashboard.
- No changes to necessary cookies, marketing consent, course/event registration evidence, or account email preferences.
- No production measurement ID/property creation, provider API call, deployment/configuration mutation, legal inference, or production activation.
- No weakening of private/no-store, cache, query-poisoning, CSP, source-policy, or development noindex boundaries.
Dependencies and sequencing
- #27 and #125 are closed prerequisites.
- #254's source-only privacy authority register is a required interface dependency. It must be accepted, merged, and green before #225 engineering because this issue must update that canonical register rather than recreate the obsolete hand-maintained inventory.
- Operational sequencing, not a product dependency: reconstruct only from a clean, green, current
mainafter the active P0 release-recovery chain. Do not rebase, revive, or accept thed24808ccandidate or its verification artifacts. - Production activation remains a separate HUMAN gate after code/test/PM acceptance. Until then, both operational settings stay at disabled defaults.
Acceptance criteria — automated implementation
- The exact two typed settings, shared service, Studio/admin API/OpenAPI parity, permissions, validation, revisions/idempotency, audit behavior, and disabled defaults pass positive and negative tests; no analytics environment variable exists.
- Analytics is absent for non-production, disabled/partial/invalid/unavailable settings, unset/deny/malformed consent, JavaScript-off, and every private/authenticated/credential-bearing surface.
- An eligible anonymous public production page sends no Google request/cookie before consent; allow starts one loader and one explicit sanitized page view; reload with persisted allow behaves once; reject/withdraw prevents future sends and clears recognized optional cookies.
- The payload contains only the exact acquisition allowlist and safely handles duplicate, empty, malformed, encoded, control-character, overlong, arbitrary-query, fragment, userinfo, and non-HTTP(S) referrer cases.
- No behavioral listener/event, automatic page view, Google signals, ad personalization, Consent Mode ping, tag-manager container, user ID, direct identifier, credential, or private value is present.
- Exact CSP origins work for the owned loader and deny near-match/unrelated origins; the source guard permits only the reviewed owned files and still rejects hard-coded IDs, unowned analytics sources/cookies/loaders, and obfuscation.
- #254's canonical register/projection and
/privacyare updated consistently without elevating any HUMAN fact; deterministic privacy/source validators pass. - Focused Django/settings/security/privacy/OpenAPI tests, migration drift, repository quality checks, and the current graph-selected versioned verification plan pass using
uv/Make. - The independent tester runs the graph-selected full Playwright tier with all provider traffic locally intercepted (no real external hit), independently validates the plan/fingerprint, and reports no required skip or pending evidence.
- Independent desktop 1440×900 and mobile 390×844 screenshots show
/privacy, the public consent dialog before and after allow/withdraw, and the authorized Studio settings state; inspected images contain the expected pages, readable copy, visible focus, no debug/error page, and no overflow at 320/390 px or 200% zoom.
Browser/network scenarios
- On an eligible anonymous public route with unset consent, open/reopen/close the dialog by keyboard and pointer; assert zero Google requests, commands, or optional cookies.
- Allow analytics; assert one exact script request and one sanitized
page_viewwithout reload, correct focus return, and no behavioral requests while scrolling, searching, clicking links, playing media, or submitting unrelated public controls. - Reload with persisted allow; assert one page view. Reopen and withdraw; assert optional-cookie cleanup and zero later analytics requests while public navigation still works.
- Repeat deny, malformed-cookie, JavaScript-off, invalid/partial setting, non-production, authenticated, credential-bearing, account/Studio/API/registration/preview, and safe error states; assert fail-closed absence and intact content.
- Exercise the exact UTM/referrer positive and adversarial matrix and inspect captured request/data-layer payloads rather than relying on script presence.
- Verify Studio read/write and admin API parity for authorized, read-only, denied, stale revision, replay, malformed ID, partial batch, and settings-read failure cases without using a real property ID.
HUMAN production gate
These checks do not block source/code construction but do block activation and final issue closure:
- [HUMAN] The authorized privacy/legal owner accepts the exact #254 register version and
/privacywording for Google Analytics processor role, regions/transfers/safeguards, purpose/lawful basis, consent evidence, cookies/network metadata, retention, rights, withdrawal, and review date. - [HUMAN] An authorized analytics owner supplies the intended production GA4 property/stream and confirms in the provider console that enhanced measurement and advertising/personalization features are off and the property settings match the acquisition-only contract.
- [HUMAN] After an accepted code release is deployed with settings still disabled, an authorized operator records the normal audited settings change and confirms, with synthetic/no-identity traffic only, that deny/unset stays silent and allow reaches only the intended production property.
If automated implementation is otherwise accepted while these remain, commit with Refs #225, add human, keep the issue open, and leave both settings disabled. Only a later complete lifecycle may activate and close it.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
First wait for #254, then work from a clean current main and inspect #125's consent implementation, the listed _docs/specs authorities, and core/source_policy.py. Use the existing settings, Studio/admin API, OpenAPI, privacy, and verification entry points, with uv/Make tests as the first validation path. Done means the typed defaults, consent-gated acquisition-only behavior, privacy updates, CSP/source-policy checks, and independent local-interception verification all pass without production provider traffic.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, openapi, playwright, python
- Domain
- analytics, api, backend, documentation, security, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100