Files
Aleksi ImmonenandSean Oliver 66e6cd1639 feat: capture Freebuff ad click ids (#50181)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature: ad attribution capture.

## What is the current behavior?

Freebuff ad clicks arrive on supabase.com with a signed click id in
`?bfcid=`. Nothing captures it, so those signups are unattributed.

## What is the new behavior?

This PR captures `bfcid` and writes it to a cookie that, in production,
is scoped so the management API receives it. The conversion is reported
server-side on profile creation, in a separate change tracked in
GROWTH-1217.

Start with `enforceConsentDecision` in
`packages/common/consented-url-cookie.ts`. It is the rule everything
else hangs off, and `consented-url-cookie.test.ts` covers the state
matrix.

Capture:

- `bfcid` is read on landing and held in `sessionStorage` until the
consent decision resolves. Memory alone loses it when someone navigates
before answering the banner.
- Once consent is granted it goes into a cookie. In production on
`*.supabase.com` that cookie is scoped to `domain=supabase.com`, and it
is host-only elsewhere. It is written only after consent, which is the
signal GROWTH-1217 relies on.
- Values are validated with `/^bfc_[A-Za-z0-9._-]{1,508}$/`, the
validator Freebuff publishes in their tag, so we never store a value
their tag would reject.
- `bfcid` is added to the first-touch attribution props, which feed
pageview telemetry and are already consent-gated.

Consent:

- `enforceConsentDecision` reduces the decision to two states. Undecided
and declined both clear the cookie, since neither has consent to point
at. They differ in the retained value: an undecided visitor may still
accept, so it waits for them.
- `clearConsentedUrlCookie` drops the cookie. `discardConsentedUrlValue`
also drops the retained value.
- A module-level valtio subscription registers on import, guarded on
`window` so it is inert during SSR.

`packages/common/consent-state.ts` gains a generic `isResolved` flag and
no vendor knowledge. A consumer acting on a decision needs to tell "not
decided yet" from "decided against", which `hasConsented` cannot express
alone. `applyPriorDecisionToSDK` now returns its promise chains, so its
signature becomes `void | Promise<void>` and initialization awaits
settlement before marking the decision resolved. Worth checking the call
sites.

## Additional context

160 tests pass in `packages/common`. Typecheck and Prettier are clean
locally on the changed files. CI is still running on the latest commit.

Unverified: the clearing paths are covered by unit tests only. The
consent SDK is short-circuited in local and preview builds, so they
cannot be exercised outside production. An end-to-end conversion
recorded by Freebuff is also unverified, since it needs the server-side
change deployed.

GROWTH-1216


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added consent-aware handling for Freebuff Ads click identifiers,
retaining valid URL values until consent is resolved and storing them in
a cookie after approval.
- Added automatic cleanup when consent is denied or withdrawn, while
preserving unrelated cookies.
- Added support for capturing the click identifier in first-touch
attribution data.
- **Bug Fixes**
- Improved consent initialization tracking so completion is reported
after successful or failed resolution.
- Added safeguards for restricted browser storage, cookies, and
server-rendered environments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Sean Oliver <882952+seanoliver@users.noreply.github.com>
2026-09-11 12:14:08 -07:00
..
2026-07-08 23:21:48 +08:00
2022-04-26 12:44:07 +02:00