feat(attribution): first-touch channel attribution on completed tests (GATED — do not merge)#91
Merged
Merged
Conversation
… (GATED) Minimal first-touch attribution so a completed test can be traced to its source. - Client: src/utils/attribution.js getFirstTouch() captures document.referrer (external only) + utm_source/medium/campaign on first load and persists them once to localStorage (cercol_attribution), same pattern as cercol_anon. - logResult (src/lib/api.js) now sends anon_id + the first-touch fields. - Backend: LogResultBody accepts the new optional fields; POST /results INSERT writes them. - Migration db/migrations/028_results_attribution.sql adds the columns (anon_id, utm_source, utm_medium, utm_campaign, referrer). NOT APPLIED. - Privacy: draft "Visit source" clause added to privacy.collected in all six locales + rendered on /privacy. DO NOT MERGE until sign-off: this captures a NEW data category (referrer + utm). Migration 028 must be applied BEFORE this code is live, or the extended INSERT fails on the missing columns. A short ADR is advisable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the new first-party data category landed in this PR: referrer + utm captured client-side, stored on results, anon_id never linked to identity. Rejected third-party analytics. Migration 028 references the ADR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
miquelmatoses
added a commit
that referenced
this pull request
Jun 17, 2026
…ode (#93) Lands migration 028 on main on its own so it can be applied via apply-migrations.yml BEFORE PR #91's extended INSERT goes live, avoiding any window where POST /results would fail on missing columns. Idempotent; columns only, no code. See ADR 0014. Co-authored-by: miquelmatoses <miquelmatoses@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lated) Match the ADR format guarded by api/tests/test_infra.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
miquelmatoses
added a commit
that referenced
this pull request
Jun 17, 2026
The first-party visit-source attribution decision is made and landed (PR #91, migration 028 applied). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: miquelmatoses <miquelmatoses@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This captures a new data category (referrer + utm). Opened as draft.
Ordering matters: migration
028must be applied before this code goes live, or the extendedINSERT INTO resultsfails on the missing columns (POST /results would 500 and drop the result). Recommended sequence on approval: apply028viaapply-migrations.yml→ then merge.What
Minimal first-touch attribution so a completed test traces to its source.
src/utils/attribution.js:getFirstTouch()capturesdocument.referrer(external only) +utm_source/medium/campaignon first load, persists once tolocalStorage['cercol_attribution'](same pattern ascercol_anon). First-party, never linked to identity.logResultnow sendsanon_id+ the first-touch fields.LogResultBodyaccepts the new optional fields;POST /resultsINSERT writes them.db/migrations/028_results_attribution.sqladds 5 columns (idempotent). NOT applied.privacy.collectedin all six locales + rendered on/privacy.Exact privacy wording (for your review)
en: "Visit source — the referring website and any campaign tags (utm) from the link you arrived through, stored with a completed test. First-party only, never shared, and never linked to your identity."
ca: "Origen de la visita — el lloc web que t'ha referit i qualsevol etiqueta de campanya (utm) de l'enllaç pel qual has arribat, desat amb un test completat. Només de primera part, mai compartit i mai vinculat a la teva identitat."
es: "Origen de la visita — el sitio web que te refirió y cualquier etiqueta de campaña (utm) del enlace por el que llegaste, almacenado con un test completado. Solo de origen propio, nunca compartido y nunca vinculado a tu identidad."
fr: "Source de la visite — le site web qui vous a référé et les éventuelles étiquettes de campagne (utm) du lien par lequel vous êtes arrivé, enregistrées avec un test complété. Première partie uniquement, jamais partagé et jamais lié à votre identité."
de: "Besuchsquelle — die verweisende Website und etwaige Kampagnen-Tags (utm) aus dem Link, über den du gekommen bist, gespeichert mit einem abgeschlossenen Test. Nur Erstanbieter, niemals geteilt und niemals mit deiner Identität verknüpft."
da: "Besøgskilde — det henvisende websted og eventuelle kampagne-tags (utm) fra linket, du kom ind via, gemt sammen med en gennemført test. Kun førstepart, aldrig delt og aldrig knyttet til din identitet."
ADR
Recommended: a short ADR documenting the new data category and the first-party/unlinked-to-identity stance. Happy to add it if you want before merge.
Verification
npm run buildgreen; frontend 245 tests pass; backend tests do not exercise the live INSERT, so CI is unaffected. Theweekly_digestsource-split placeholder (PR #86) is ready to consume this once it lands.🤖 Generated with Claude Code