fix(analytics): data hygiene — asset pageviews, reader-path splits, cancellation mislabel, owner exclusion#934
Conversation
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 8m 46s —— View job Claude PR Review — complete ✅
Verdict: 0 high / 0 medium priority issues. A tight, well-tested measurement-hygiene PR. I traced each of the four sub-fixes end-to-end:
Full review is emitted as my final run message; the auto-apply workflow posts it as the review comment (per |
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1.
|
272d323 to
efa6d1d
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 7m 21s —— View job Claude PR Review — complete ✅
Verdict: 0 high / 0 medium. Re-reviewed at
Two low-priority maintainability nits (non-blocking) are in the review block. Full review is emitted as my final run message; the auto-apply workflow posts it as the review comment (per |
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1. Apple-touch-icon regex is duplicated between the route and the classifier
2.
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 30m 1s —— View job Conflicts resolved ✅Rebased Root cause of the conflicts: 3 conflicted files:
Composition-root wiring (preserves the PR's four fixes exactly):
Tests: the auto-merged shared-package tests were adapted to assert the injection contract (a flagged path is dropped / the canonicalizer's output is stored). The hutch path-shape specifics (
|
efa6d1d to
d145c45
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 6m 43s —— View job Claude PR Review — complete ✅
Verdict: 0 high / 0 medium. Re-reviewed at the rebased
Three low-priority notes (one new re: |
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1.
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 11m 1s —— View job Conflicts resolved ✅Rebased 1 conflict — if (!shouldLog({ req, path, statusCode: res.statusCode, isStaticAssetPath: deps.isStaticAssetPath })) return;
const userAgent = req.get("user-agent");Everything else auto-merged cleanly, and I verified the merged results are internally coherent rather than just marker-free:
The cancellation (c) and Pulumi owner-hash (d) fixes had no conflicts. No behavior change — the four measurement fixes land intact on top of main's device-mix analytics work. Verification (
|
…ancellation mislabel, owner exclusion Four independently revertable measurement fixes: (a) Static-asset fetches were counted as pageviews. New isStaticAssetRequestPath, derived from the actual static routes in server.ts (the /client-dist mount and the apple-touch-icon redirect), excludes them in analytics shouldLog and in click-attribution's landing capture. (b) Path fidelity: snapshot req.path at analytics-middleware entry so a finish-time express mount-trim can no longer corrupt the logged path; stop counting 3xx responses as pageviews (a redirect leg is not a pageview); and canonicalize /view scheme-variant landing_path at cookie time via canonicalizeViewLandingPath so the same article stops splitting attribution. (c) Cancellation reason: thread a trigger through CancelSubscriptionCommand → deferred-cancellation schedule → final conversion so a trial that lapsed for no card on file emits trial_ended_no_payment_method instead of user_initiated_trial. The churned-trial feedback-email filter accepts both trial reasons so the churn email keeps firing. (d) Owner exclusion: 22 production-confirmed owner device hashes appended to hutch:excludedVisitorHashes (dashboard-query-side only). Claude-Session: https://claude.ai/code/session_01En2uUiiwQoRQKWenCnkPi9
d145c45 to
e6e2922
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 4m 30s —— View job Claude PR Review — complete ✅
Verdict: 0 high / 0 medium. Re-reviewed at the rebased
Three low-priority notes (all carried over from prior reviews, unchanged at this commit — 304-cutoff, apple-touch regex duplication, |
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1.
|
fix(analytics): data hygiene — asset pageviews, reader-path splits, cancellation mislabel, owner exclusion
Hypothesis
Four measurement defects make the funnel unreadable: (1) static-asset fetches are counted as pageviews, (2) the same article's traffic splits across scheme-variant reader paths, (3) every automatic trial expiration is labelled as a user-initiated cancel, and (4) the owner's own reading dominates readership metrics. Fixing measurement changes no user-facing behavior but makes every subsequent funnel PR (#916–#918) evaluable.
Evidence (30d, Jun 4 – Jul 4 2026)
/client-dist/toast.client.js.req.pathatres.on("finish"), butexpress.staticandres.redirectrespond terminally, so the router never restores the mount-trimmedreq.url./client-dist/toast.client.jstherefore logs as/toast.client.js, and/view/https:/host/sluglogs as/https:/host/slug. Click-attribution runs pre-mount, which is why landing_path shows the untrimmed variants — two different corruptions of the same requests./fagnerbrack.com/learn-sql-…1,173 pv/878 v vs/https:/fagnerbrack.com/learn-sql-…756 pv/715 v (~40% of top-article traffic); landing_path/view/https:/…vs/view/…splits signup attribution 7 vs 5. (The routing-level 301 with query preservation already exists — PR feat(hutch): canonicalize /view URLs to drop https:// and url encoding #439,view.page.ts:141-148; the residual split is entirely analytics-side.)cancelled reason=user_initiated_trial~60 min aftercharge_failed reason=no_card_on_file— the reason is derived fromrow.subscriptionIdalone at the final conversion, so "never added a card" is indistinguishable from "chose to cancel". Trial→paid is 0%; this label mislead is why churn looked voluntary.c6f587a1…= 360 of 422 article_reads and is NOT excluded; production query (this PR's evidence) recovered its 22 device visitor_hashes covering 361 reads.What this changes
isStaticAssetRequestPathderived from the actual static routes inserver.ts(/client-distmount, apple-touch-icon redirect, root-level trimmed bundle names). Applied inshouldLogand in click-attribution's landing capture. Explicitly not an extension regex: reader paths mirror arbitrary source URLs and may end.png/.html— covered by tests.req.pathat middleware entry (kills both trimmed shapes for every route), stop counting 3xx responses as pageviews (the 301 canonicalization leg no longer double-counts), and canonicalize/viewscheme-variantlanding_pathat cookie time viacanonicalizeViewLandingPath.trial_ended_no_payment_method.SubscriptionChargeFailedhandling now publishesCancelSubscriptionCommandwithtrigger:"charge_failed", round-tripped through the deferred-cancellation EventBridge schedule Input, and resolved at the final conversion. The churned-trial feedback email filter accepts both trial reasons, so the only churn email keeps firing.hutch:excludedVisitorHashes(Pulumi prod config; dashboard-query-side only).Estimated result
cancelledevents split intotrial_ended_no_payment_method(expected ~28/mo today) vs trueuser_initiated_trial(expected ≈0) — the actual conversion-blocking problem (no card on file) becomes visible in one dashboard widget.How to measure
CloudWatch Logs Insights on
/aws/lambda/hutch-handlerafter deploy:Expect
polluted= 0 from deploy day onward. For (c):Expect
trial_ended_no_payment_methodat the next trial expiry, each followed by a[schedule-trial-feedback-email] schedule createdline. For (d): the readership widget baseline steps down; raw logs still contain owner reads (exclusion is query-side, reversible).Coordination note
analytics-dashboard.test.tspins the widget count (toHaveLength(27)on current main). This PR adds no events or widgets and does not touch that assertion, but sibling PRs #916–#918 may bump it; on conflict, keep the sibling's count — R6 contributes +0. The four sub-fixes are grouped in separate commits ((a) asset exclusion, (b) path fidelity/3xx/landing canonicalization, (c) cancellation reason + feedback-email filter [revert together], (d) Pulumi hash list) so each is independently revertable.https://claude.ai/code/session_01En2uUiiwQoRQKWenCnkPi9