Skip to content

feat(analytics): report anonymous shopper events - #3

Merged
PhilippTheServer merged 1 commit into
mainfrom
pl/analytics
Aug 26, 2026
Merged

PhilippTheServer merged 1 commit into
mainfrom
pl/analytics

Conversation

@PhilippTheServer

Copy link
Copy Markdown
Contributor

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 the
backend is a change to storefrontConfig.analytics.endpoint.

Privacy

The only identifier is crypto.randomUUID() in sessionStorage — it dies with the tab
and never spans a visit. Not a cookie, not localStorage: either would be a persistent
identifier, 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.

sendBeacon on tab hide, so a closing tab's partial batch still arrives.

Two placement decisions

add_to_cart lives in CartService, not on the button. Every route into the cart is
then counted, and a new one cannot silently skip reporting.

checkout_started carries the order id. That is what lets the API verify payment from
the orders table rather than taking the browser's word for a conversion.

This repository had no tests

No spec files, no test target in angular.json, no CI. Since the privacy promise is a
property 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-test
target, vitest + jsdom, and a CI workflow running tests and a production build.

Verification

$ npx ng test --watch=false
 Test Files  1 passed (1)
      Tests  11 passed (11)

$ npx ng build --configuration production
Initial total | 327.98 kB | 89.07 kB

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 sessionStorage being unavailable in private browsing.

End to end through the storefront's own nginx proxy, which is the real integration risk
(/api/v1/... → /v1/...):

$ curl -X POST http://localhost:4300/api/v1/analytics/events -d '{"events":[...]}'
{"success":true,"message":"Events recorded","accepted":1,"rejected":0}   status=202

$ psql -c "SELECT session_id, event_type, path FROM storefront_events WHERE ..."
proxy-check-0001|page_view|/shop

Two assertions I wrote were wrong and I fixed the tests, not the code: JSON.stringify
drops undefined keys so the payload has fewer fields than declared, and this test
environment provides no localStorage to assert against.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The storefront reports nothing, so the shopper funnel has no data

1 participant