Repository navigation
feat(analytics): report anonymous shopper events - #3
Merged
Merged
Conversation
The API can record anonymous shopper events, but the storefront sent none, so the funnel had no data before an order existed. Reports four events — page view, product view, add to cart, checkout started — through an AnalyticsService port. No page imports an analytics library, so the backend is a configuration change rather than a code change. Nothing collected identifies a person. The only identifier is a session id from crypto.randomUUID() held in sessionStorage, so it dies with the tab and never spans a visit. Not a cookie and not localStorage: either would be a persistent identifier, which is precisely what drags a shop into needing a consent banner. Query strings are stripped before the request leaves the browser, so an email in a share link cannot reach a request log on the way to an API that would have stripped it anyway. Off by default. Cloning this repository must not start collecting anything, and while disabled the service attaches no router listener and never generates an identifier, so a visitor's browser is left completely untouched. Failures are swallowed and events dropped rather than retried. A shop that breaks because its analytics endpoint is down has been made worse by measuring itself, and retrying into a queue during an incident turns a quiet failure into a loud one. add_to_cart is instrumented in CartService rather than at the button, so every route into the cart is counted and a new one cannot silently skip reporting. checkout_started carries the order id, which is what lets the API verify payment from the orders table instead of taking the browser's word for it. This repository had no test harness: no spec files, no test target and no CI. Adds all three, since the privacy promise is a property of this class and needs something that fails when it stops holding. Closes #2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YY1ekLLeFLkAU2kvdQ8Ey4
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.
Closes #2 · consumes OpenTaberna/fastapi#47
The storefront now reports four events — page view, product view, add to cart, checkout
started — so the shopper funnel has data before an order exists.
A port, not an SDK
No page imports an analytics library. Components call
analytics.track(...); swapping thebackend is a change to
storefrontConfig.analytics.endpoint.Privacy
The only identifier is
crypto.randomUUID()in sessionStorage — it dies with the taband never spans a visit. Not a cookie, not
localStorage: either would be a persistentidentifier, which is exactly what requires a consent banner.
Query strings are stripped before the request leaves the browser. The API strips them
too, but sending them at all is a needless way to put an email into a request log on the way
there.
Off by default. While disabled the service attaches no router listener and never
generates an identifier, so a deployment that has not opted in leaves the visitor's browser
completely untouched. There is a test for that.
Never degrade the shop
Failures are swallowed and events dropped rather than retried. A shop that breaks
because its analytics endpoint is down has been made worse by measuring itself, and retrying
into a queue during an incident turns a quiet failure into a loud one. Tests pin both.
sendBeaconon tab hide, so a closing tab's partial batch still arrives.Two placement decisions
add_to_cartlives inCartService, not on the button. Every route into the cart isthen counted, and a new one cannot silently skip reporting.
checkout_startedcarries the order id. That is what lets the API verify payment fromthe orders table rather than taking the browser's word for a conversion.
This repository had no tests
No spec files, no
testtarget inangular.json, no CI. Since the privacy promise is aproperty of this class, it needs something that fails when it stops holding — so this adds
the harness as part of the change:
tsconfig.spec.json, the@angular/build:unit-testtarget, vitest + jsdom, and a CI workflow running tests and a production build.
Verification
The eleven tests cover: disabled sends nothing and stores nothing, batching, the payload
carrying no PII field, query-string stripping, sessionStorage rather than cookie, one
session id across events, a failing endpoint not throwing into the page, no retry, and
surviving
sessionStoragebeing unavailable in private browsing.End to end through the storefront's own nginx proxy, which is the real integration risk
(
/api/v1/...→/v1/...):Two assertions I wrote were wrong and I fixed the tests, not the code:
JSON.stringifydrops undefined keys so the payload has fewer fields than declared, and this test
environment provides no
localStorageto assert against.