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>
72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { fetchConfig } from './api';
|
|
import { useCustomerAuth } from './customer/CustomerAuthContext';
|
|
import { brevoPage, startBrevoTracking, stopBrevoTracking } from './brevo';
|
|
|
|
/**
|
|
* Drives the Brevo tracker from the two things that decide whether it may run
|
|
* at all: the environment's key and the signed-in customer's consent (#56).
|
|
*
|
|
* Renders nothing. It exists as a component rather than a hook because it needs
|
|
* both the router and the auth context, and mounting it inside `AppRoutes` is
|
|
* what guarantees it sits under both — a hook called from the wrong place would
|
|
* fail at runtime instead of being impossible to misplace.
|
|
*
|
|
* ## Why the gate is a server-computed field
|
|
*
|
|
* `analytics_consent` is not `marketing_consent`. The consent sentence was
|
|
* widened to mention analytics, and the server reports this flag by comparing
|
|
* the wording each customer actually agreed to against the current text. A
|
|
* customer who consented to the older, email-only wording is not tracked. Never
|
|
* substitute `marketing_consent` here — that is the exact retroactive widening
|
|
* the field exists to prevent.
|
|
*
|
|
* A signed-out visitor has no consent record, so no key is ever used and the
|
|
* script is never injected. Anonymous browsing is not reported at all.
|
|
*/
|
|
export default function BrevoTracking() {
|
|
const { customer } = useCustomerAuth();
|
|
const location = useLocation();
|
|
const [trackerKey, setTrackerKey] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
// Swallowed for the same reason main.tsx swallows its warm-up call: a
|
|
// config that cannot be fetched is a broken deployment every other request
|
|
// will report, and losing analytics is not worth surfacing to a customer.
|
|
void fetchConfig()
|
|
.then(config => { if (!cancelled) setTrackerKey(config.brevoTrackerKey); })
|
|
.catch(() => {});
|
|
return () => { cancelled = true; };
|
|
}, []);
|
|
|
|
const consented = customer?.analytics_consent === true;
|
|
const email = customer?.email;
|
|
|
|
// Start and stop are driven by the same effect so there is no state in which
|
|
// consent has gone away and nothing has acted on it. Withdrawing consent,
|
|
// signing out, and deleting the account all arrive here as `consented`
|
|
// turning false, because each one ends with the customer no longer being a
|
|
// consenting signed-in customer.
|
|
useEffect(() => {
|
|
if (consented && trackerKey && email) {
|
|
startBrevoTracking(trackerKey, email);
|
|
} else {
|
|
stopBrevoTracking();
|
|
}
|
|
}, [consented, trackerKey, email]);
|
|
|
|
// Declared after the effect above so that on the render where consent or the
|
|
// key first arrives, tracking is started before this reports the route.
|
|
//
|
|
// The storefront is a single-page app: without this, the tracker's own
|
|
// initial call would be the only page view it ever saw, and every customer
|
|
// would look like they viewed one page and left.
|
|
useEffect(() => {
|
|
brevoPage(location.pathname);
|
|
}, [location.pathname, consented, trackerKey]);
|
|
|
|
return null;
|
|
}
|