Implements the Brevo web tracker (#56), with consent built to EU and Quebec standards.
Two commits, and the second reverses an approach taken in the first.ac3f6e9 widened the marketing consent sentence so one checkbox covered both email and tracking. 955049e undoes that and separates them, for the reason below. Reviewing the branch as a whole is easier than reading the first commit on its own.
Why the consents are separate
Bundling was the problem. GDPR requires consent to be granular — email marketing and behavioural tracking are separate purposes with separate recipients, and current EDPB guidance treats bundling tracking consent with subscription consent as invalid, because the customer cannot accept one and refuse the other.
Quebec's Law 25 s.8.1 is stricter and applies to any Quebec visitor: profiling technology must be off until the person switches it on — no pre-ticked box, and no consent inherited from agreeing to something else. Penalties reach CAD $25M. Elsewhere in Canada PIPEDA is looser, but the OPC has moved to a GDPR-aligned position that profiling needs meaningful, not implied, consent.
The storefront is publicly reachable and anyone can register, so this is built to the strictest standard rather than to where customers happen to be today.
What that means concretely
The marketing sentence is restored to exactly what it was. Every existing email consent stays valid and untouched; nobody is re-asked for something they already agreed to.
Analytics has its own column, sentence, checkbox, account toggle and endpoint. A customer can hold either, both, or neither, and withdrawing one does not disturb the other.
The migration defaults analytics_consent to FALSE — the honest answer, since none of the existing customers was ever asked, and what Law 25 requires. Nothing here opts anybody in.
The sentence names Brevo rather than "our email provider". Informed consent means the customer can tell who receives their data; a description they cannot act on is not disclosure.
The account toggle is as easy to switch off as on, because withdrawal has to be as easy as consenting.
The analytics endpoint is separate from the marketing one, so a single call cannot change an answer the customer did not touch — the bundling problem moved from the form into the API.
The tracker itself
Loads only for a signed-in customer whose stored analytics wording matches the current constant. Anonymous visitors are never tracked and no script is injected.
QA isolation by construction: docker-compose.qa.yml sets an empty BREVO_TRACKER_KEY literal with no stack variable behind it, so nothing can inherit a value from the host or be pasted in from production's stack.
Events reported from the API layer, not UI call sites, so no caller can be missed. Each fires only after the response was accepted. Both checkout completions are instrumented and name their processor — a demo purchase charges nothing and counting it as a sale would overstate revenue.
Privacy policy rewritten to name Brevo, state that the two choices are independent, and state the limits honestly: turning it off stops further sharing, but anything already sent stays with Brevo, and stopBrevoTracking stops calls — it does not unload sa.js.
Deployment note
BREVO_TRACKER_KEY must be set as a Portainer stack variable for production, from https://app.brevo.com/automation/parameters. Unset means the tracker never loads and nothing is reported — the change is inert until that key exists, which is deliberate.
This adds a migration, so it runs at container start on the next deploy.
Verification
Backend tsc clean · both lint suites 0 errors with no new warnings · 478 unit tests across 33 suites · frontend production build green, including composeEnvironment.test.ts.
The unit tests assert the two consents stay apart in both directions, including that the marketing sentence still says nothing about tracking — re-bundling them would otherwise pass silently, and this branch made that mistake once already.
Not verified: the migration has not been run against a database; integration and e2e need a Node this machine does not have active; and no real Brevo key was exercised, so the tracker has never been observed reporting into an actual account. Registration copy changed, so the two consent sentences are worth reading on screen in QA.
None of this is legal advice. The wording is built against published guidance for GDPR, ePrivacy, Law 25 and PIPEDA, but it deserves a lawyer's eye before it ships — as the policy's own disclaimer already says.
Implements the Brevo web tracker (#56), with consent built to EU and Quebec standards.
**Two commits, and the second reverses an approach taken in the first.** `ac3f6e9` widened the marketing consent sentence so one checkbox covered both email and tracking. `955049e` undoes that and separates them, for the reason below. Reviewing the branch as a whole is easier than reading the first commit on its own.
## Why the consents are separate
Bundling was the problem. GDPR requires consent to be **granular** — email marketing and behavioural tracking are separate purposes with separate recipients, and current EDPB guidance treats bundling tracking consent with subscription consent as invalid, because the customer cannot accept one and refuse the other.
**Quebec's Law 25 s.8.1** is stricter and applies to any Quebec visitor: profiling technology must be **off until the person switches it on** — no pre-ticked box, and no consent inherited from agreeing to something else. Penalties reach CAD $25M. Elsewhere in Canada PIPEDA is looser, but the OPC has moved to a GDPR-aligned position that profiling needs meaningful, not implied, consent.
The storefront is publicly reachable and anyone can register, so this is built to the strictest standard rather than to where customers happen to be today.
## What that means concretely
- **The marketing sentence is restored to exactly what it was.** Every existing email consent stays valid and untouched; nobody is re-asked for something they already agreed to.
- **Analytics has its own column, sentence, checkbox, account toggle and endpoint.** A customer can hold either, both, or neither, and withdrawing one does not disturb the other.
- **The migration defaults `analytics_consent` to `FALSE`** — the honest answer, since none of the existing customers was ever asked, and what Law 25 requires. Nothing here opts anybody in.
- **The sentence names Brevo** rather than "our email provider". Informed consent means the customer can tell who receives their data; a description they cannot act on is not disclosure.
- **The account toggle is as easy to switch off as on**, because withdrawal has to be as easy as consenting.
- **The analytics endpoint is separate** from the marketing one, so a single call cannot change an answer the customer did not touch — the bundling problem moved from the form into the API.
## The tracker itself
- Loads only for a signed-in customer whose stored analytics wording matches the current constant. Anonymous visitors are never tracked and no script is injected.
- **QA isolation by construction**: `docker-compose.qa.yml` sets an empty `BREVO_TRACKER_KEY` literal with no stack variable behind it, so nothing can inherit a value from the host or be pasted in from production's stack.
- **Events reported from the API layer**, not UI call sites, so no caller can be missed. Each fires only after the response was accepted. Both checkout completions are instrumented and name their processor — a demo purchase charges nothing and counting it as a sale would overstate revenue.
- **Privacy policy** rewritten to name Brevo, state that the two choices are independent, and state the limits honestly: turning it off stops further sharing, but anything already sent stays with Brevo, and `stopBrevoTracking` stops calls — it does not unload `sa.js`.
## Deployment note
`BREVO_TRACKER_KEY` must be set as a Portainer stack variable for production, from https://app.brevo.com/automation/parameters. Unset means the tracker never loads and nothing is reported — the change is inert until that key exists, which is deliberate.
**This adds a migration**, so it runs at container start on the next deploy.
## Verification
Backend `tsc` clean · both lint suites 0 errors with no new warnings · **478 unit tests across 33 suites** · frontend production build green, including `composeEnvironment.test.ts`.
The unit tests assert the two consents stay apart **in both directions**, including that the marketing sentence still says nothing about tracking — re-bundling them would otherwise pass silently, and this branch made that mistake once already.
**Not verified:** the migration has not been run against a database; integration and e2e need a Node this machine does not have active; and no real Brevo key was exercised, so the tracker has never been observed reporting into an actual account. Registration copy changed, so the two consent sentences are worth reading on screen in QA.
**None of this is legal advice.** The wording is built against published guidance for GDPR, ePrivacy, Law 25 and PIPEDA, but it deserves a lawyer's eye before it ships — as the policy's own disclaimer already says.
Closes #56
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Loads Brevo's web tracker for a signed-in customer who has consented, reports route changes as page views, and tracks the three events the issue asked for: added_to_cart, favorited, and checkout_completed. The four design questions were settled on the issue in August and this implements those answers.
The consent gate is the part worth reading. The decision recorded on the issue was "gate it behind consent", but the sentence customers actually agreed to named only email: "I want to receive occasional emails about new one-of-a-kind items". Gating a tracker on `marketing_consent` while that was the stored wording would have treated "email me about new items" as authorisation to send someone's browsing to a third party, which it does not say — and this project stores the wording verbatim against each customer precisely so that a record says what the customer saw.
So the sentence is widened here, and the tracker is gated on `analytics_consent`, a field the server computes by comparing the wording stored against a customer with the current constant. Changing the sentence therefore does not retroactively widen anybody's consent: everyone who agreed to the old text keeps their email consent and is not tracked until they re-consent through the account page. A boolean alone could not tell those two populations apart, which is the whole reason the text is stored per customer. `analyticsConsent` is exported and has its own unit test, because "agreeing to the old wording does not authorise tracking" is the rule that silently tracks people if it regresses — their flag really is true.
QA stays out of the live Brevo account by construction rather than by remembering. The key is per-environment, the tracker never loads without one, and `docker-compose.qa.yml` sets an empty literal with no stack variable behind it, so nothing can inherit a value from the host or be pasted in from production's stack. Same reasoning as QA_DB_PASSWORD and the QA_SMTP_ names beside it.
Events are reported from the API layer rather than the UI call sites, so no caller can add to the cart or favorite an item without it being counted, and each fires only after the response was accepted — a refused add is not reported as one. The two checkout completions each name their processor, because a demo purchase charges nothing and counting it as a sale would overstate revenue.
The privacy policy gains an analytics section in this change rather than a follow-up, since the published policy previously described none of this and would otherwise have lagged the code. It is deliberate about the limits: withdrawing consent stops further reporting, but anything already sent stays with Brevo, and a script already injected cannot be un-injected — `stopBrevoTracking` stops calls, it does not unload sa.js. That is said in the code too, because "tracking stops" reads as a stronger promise than any web tracker can make.
Verified: backend tsc clean, both lint suites 0 errors with no new warnings, 474 unit tests passing across 33 suites, and the frontend production build green including the compose-environment guard. Not verified: integration and e2e, which need a database and a Node this machine does not have active, and no real Brevo key was exercised — the tracker has never been observed reporting to an actual account.
Closes#56
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit widened the marketing consent sentence to cover the Brevo tracker, so one checkbox carried both purposes. That is the specific pattern GDPR rejects: consent has to be granular, and current EDPB guidance treats bundling tracking consent with subscription consent as invalid because the customer cannot accept one purpose and refuse the other. Quebec's Law 25 s.8.1 is stricter again — profiling technology has to be off until the person switches it on, with no pre-ticked box and no consent inherited from agreeing to something else. Building to both standards was the decision, since the storefront is publicly reachable and anyone can register.
So the marketing sentence is restored to exactly what it was, which leaves every existing email consent valid and untouched, and analytics gets its own column, its own sentence, its own checkbox at registration, its own toggle in the account page and its own endpoint. A customer can now hold either, both, or neither, and withdrawing one does not disturb the other.
The migration defaults analytics_consent to false, which is both the honest answer — none of the existing customers was ever asked — and what Law 25 requires. Nothing about this change opts anybody in.
Two details that are compliance requirements rather than wording preferences. The sentence names Brevo instead of saying "our email provider", because informed consent means the customer can tell who receives their data and a description they cannot act on is not disclosure. And the account toggle is as prominent and as easy to switch off as it is to switch on, because withdrawal has to be as easy as consenting.
The analytics endpoint is separate from the marketing one rather than a second field on it, so that a single call cannot change an answer the customer did not touch — the bundling problem moved from the form into the API. The unit tests now assert the two consents stay apart in both directions, including that the marketing sentence still says nothing about tracking, because re-bundling them would otherwise pass silently and is the mistake this project already made once.
Verified: backend tsc clean, both lint suites 0 errors with no new warnings, 478 unit tests passing across 33 suites, frontend production build green. Not verified: the migration has not been run against a database, and integration and e2e need a Node this machine does not have active. None of this is legal advice and the wording is worth a lawyer's eye before it ships.
Refs #56
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the consent picture the rest of this branch builds. The claim worth being able to check is that no cookie needing permission is set before it is asked for, so the policy now lists everything rather than asserting it: the rd_session sign-in cookie, which is strictly necessary and therefore exempt, the two preferences kept in localStorage and never sent anywhere, and Brevo's cookie, which cannot exist unless analytics consent was given because the script that would set it is never loaded otherwise.
No cookie banner, and that is a finding rather than an omission. ePrivacy requires consent before storing anything non-essential, and this application does not store anything non-essential until the customer has asked for the feature that needs it. A banner would be asking permission for things that are either exempt or already separately consented to, which teaches people to dismiss the one consent that does matter.
Described in terms of what each thing does rather than by category, since a list of cookie names tells a customer nothing they can act on.
Refs #56
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The register route now returns analytics_consent, and customers.integration.test.ts asserts the exact key set the public customer shape may contain. That test failed in CI, which is the guard doing its job rather than a problem with it: its whole point is that the shape cannot quietly grow, and a field appearing without someone deciding it belongs there is what it exists to catch. This field does belong there, so the expected set gains it.
Three cases added while here, all of them properties the compliance work depends on and none of them observable from a unit test. Analytics consent is off for a registration that does not mention it, which is what Quebec's Law 25 s.8.1 requires and needs the column default, the register route and the stored wording to agree. Opting in to marketing alone leaves analytics off, which is the bundling GDPR treats as invalid and the mistake this branch already made once. And an analytics-only opt-in works with marketing left off, so the granularity holds in both directions rather than only the convenient one.
Found by CI rather than locally: the integration suite needs a database this machine has no Docker to run, which was called out as unverified when the change went up. Typechecked, linted and the 478 unit tests still pass, but the assertion itself is only proven by the next CI run.
Refs #56
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements the Brevo web tracker (#56), with consent built to EU and Quebec standards.
Two commits, and the second reverses an approach taken in the first.
ac3f6e9widened the marketing consent sentence so one checkbox covered both email and tracking.955049eundoes that and separates them, for the reason below. Reviewing the branch as a whole is easier than reading the first commit on its own.Why the consents are separate
Bundling was the problem. GDPR requires consent to be granular — email marketing and behavioural tracking are separate purposes with separate recipients, and current EDPB guidance treats bundling tracking consent with subscription consent as invalid, because the customer cannot accept one and refuse the other.
Quebec's Law 25 s.8.1 is stricter and applies to any Quebec visitor: profiling technology must be off until the person switches it on — no pre-ticked box, and no consent inherited from agreeing to something else. Penalties reach CAD $25M. Elsewhere in Canada PIPEDA is looser, but the OPC has moved to a GDPR-aligned position that profiling needs meaningful, not implied, consent.
The storefront is publicly reachable and anyone can register, so this is built to the strictest standard rather than to where customers happen to be today.
What that means concretely
analytics_consenttoFALSE— the honest answer, since none of the existing customers was ever asked, and what Law 25 requires. Nothing here opts anybody in.The tracker itself
docker-compose.qa.ymlsets an emptyBREVO_TRACKER_KEYliteral with no stack variable behind it, so nothing can inherit a value from the host or be pasted in from production's stack.stopBrevoTrackingstops calls — it does not unloadsa.js.Deployment note
BREVO_TRACKER_KEYmust be set as a Portainer stack variable for production, from https://app.brevo.com/automation/parameters. Unset means the tracker never loads and nothing is reported — the change is inert until that key exists, which is deliberate.This adds a migration, so it runs at container start on the next deploy.
Verification
Backend
tscclean · both lint suites 0 errors with no new warnings · 478 unit tests across 33 suites · frontend production build green, includingcomposeEnvironment.test.ts.The unit tests assert the two consents stay apart in both directions, including that the marketing sentence still says nothing about tracking — re-bundling them would otherwise pass silently, and this branch made that mistake once already.
Not verified: the migration has not been run against a database; integration and e2e need a Node this machine does not have active; and no real Brevo key was exercised, so the tracker has never been observed reporting into an actual account. Registration copy changed, so the two consent sentences are worth reading on screen in QA.
None of this is legal advice. The wording is built against published guidance for GDPR, ePrivacy, Law 25 and PIPEDA, but it deserves a lawyer's eye before it ships — as the policy's own disclaimer already says.
Closes #56
🤖 Generated with Claude Code