Skip to content

feat(attribution): first-touch channel attribution on completed tests (GATED — do not merge)#91

Merged
miquelmatoses merged 4 commits into
mainfrom
feat/first-touch-attribution
Jun 17, 2026
Merged

feat(attribution): first-touch channel attribution on completed tests (GATED — do not merge)#91
miquelmatoses merged 4 commits into
mainfrom
feat/first-touch-attribution

Conversation

@miquelmatoses

Copy link
Copy Markdown
Collaborator

⚠️ GATED — do not merge / do not apply migration without sign-off

This captures a new data category (referrer + utm). Opened as draft.

Ordering matters: migration 028 must be applied before this code goes live, or the extended INSERT INTO results fails on the missing columns (POST /results would 500 and drop the result). Recommended sequence on approval: apply 028 via apply-migrations.yml → then merge.

What

Minimal first-touch attribution so a completed test traces to its source.

  • Client src/utils/attribution.js: getFirstTouch() captures document.referrer (external only) + utm_source/medium/campaign on first load, persists once to localStorage['cercol_attribution'] (same pattern as cercol_anon). First-party, never linked to identity.
  • logResult 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 5 columns (idempotent). NOT applied.
  • Privacy draft added to privacy.collected in 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 build green; frontend 245 tests pass; backend tests do not exercise the live INSERT, so CI is unaffected. The weekly_digest source-split placeholder (PR #86) is ready to consume this once it lands.

🤖 Generated with Claude Code

… (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>
miquelmatoses and others added 2 commits June 17, 2026 10:27
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>
@miquelmatoses miquelmatoses marked this pull request as ready for review June 17, 2026 08:31
…lated)

Match the ADR format guarded by api/tests/test_infra.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@miquelmatoses miquelmatoses merged commit 8243a83 into main Jun 17, 2026
7 checks passed
@miquelmatoses miquelmatoses deleted the feat/first-touch-attribution branch June 17, 2026 08:34
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant