The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
6.7 KiB
Diagnostic playbooks by ticket shape
Worked patterns from real triage sessions, anonymized. Each playbook states the evidence chain that settles the diagnosis.
Frontend crash
- Get the exception: ticket usually has an exception ID or a minified stack (
chunk-XXXX.jsframes). - Search error tracking in the internal project (
query-error-tracking-issues-listwithsearchQuery+ a/webURL filter). The issue'ssourcefield gives the sourcemapped file; events withverbosity: stackgive resolved function names. - Read the named code path. Common web analytics crash class: kea-router JSON-parses query params, so
?someParam=123arrives as a number and?p=a&p=bas an array; any persisted reducer fed fromsearchParamscan hold a non-string forever and crash every selector recompute (date change, filter change) — including on scenes that only connect the logic. - Fix at both ends: coerce at the router boundary AND at the read path (persisted bad values outlive a boundary-only fix). Add a cheap pure-function regression test asserting non-string inputs don't throw.
- CI gate jobs ("X Tests Pass") fail when a dependent job is cancelled by a superseded duplicate run — read the gate log before chasing phantom test failures; a runner that failed with zero executed steps is infra, rerun it.
Two numbers don't match
Almost always scoping semantics, in one of three flavors:
- Landing vs containing: overview tiles scope sessions by "any event matches the filters"; per-path bounce/breakdown rows scope by session entry value. Same filters, different denominators, both correct.
- Event-level filter vs session-entry breakdown: filtering by event
utm_campaignselects sessions containing any matching event; the UTM breakdown tile groups by session$entry_utm_campaign. A session that picked up the UTM mid-session matches the filter but shows "(not set)". - Operator drift:
containsvsequalson URLs, path cleaning on vs off, event vs session property of the same name.
Resolution: name the two definitions precisely, then hand the customer the aligned pair (filter by the session entry property when comparing against a session-scoped breakdown). Only escalate to "bug" if the two surfaces claim the same definition and still disagree.
Key code: products/web_analytics/backend/insightsql_queries/ (overview vs stats_table query builders), insights/insightsql/database/schema/sessions_v2.py (entry property aggregation).
Count drop over time
The decisive question: which layer dropped?
- Pull the raw daily series (
count()on events by day for their team, viaquery-datastore-via-metabase). If raw counts dropped, no query-time change (bot classification, exclusions, materialization) can be the cause — those never alter stored counts. - Sanity checks that kill false leads fast:
$lib+ SDK version split (pinned version = no SDK regression), duplicate-UUID counts (dedup), day-of-week matched comparison (seasonality), hour-of-day spread (region/outage). - The bot fingerprint: only
$pageviewdrops while$pageleavestays flat, loss concentrated in a few UA strings collapsing 10-50x week over week, ~2 pageviews/session with no pageleave. That is non-human traffic that stopped executing the JS SDK — typically the customer's edge (WAF, bot-fight mode, JS challenge) changed, or the scraper stopped. Their server logs still count those requests, which is why "our logs look unchanged". - Reply framing: Insights stored what it received; identify the segment that vanished; ask what changed at their edge on the exact step dates; note the new lower level is closer to true human traffic.
Tracker not loading / undercounting vs a competitor
Run a runtime loading audit (see loading-audit.md) against their live pages. Findings that recur:
- Delivery chain beats endpoint proxying: a reverse-proxied
api_hostis unblockable, but if the SDK loads through GTM, blockinggoogletagmanager.comkills it anyway. First-party script + first-party endpoint or it doesn't count. - Consent latency loses quick bounces: even in banner-less regions, tag managers + consent platforms resolve asynchronously; every visitor who leaves before that window sends nothing. Measure first-request timing vs the competitor script.
- Compare like with like: competitor tools differ on visit definitions, bot filtering, and cookieless counting; quantify the load-chain gap before litigating definitional gaps.
Ad-platform integration errors
- Soft-deleted sources do not block re-adding (prefix checks exclude deleted rows) — "Prefix already exists" means the old source is still live.
- OAuth reconnect failures are the top re-add blocker; Microsoft AADSTS650052 (missing tenant admin consent / service principal) surfaces as a bare
invalid_clienttoast. Ask for the exact error text and where it appears (sign-in popup vs field vs create toast) — each maps to a different code path. - Account pickers may be project-admin-gated; a member sees a permission error where an admin sees accounts.
- Marketing analytics attribution: conversion events attribute via their own UTMs, else fall back to the last prior pageview carrying both
utm_campaignandutm_sourcein the window, then LEFT JOIN on exact case-sensitive campaign name plus normalized source. Any miss lands in "organic". Zero paid conversions with a fat organic row means the join keys or the both-UTMs requirement failed, not the goals. - Native ad sources need only their stats tables synced for spend metrics; Insights events matter only for conversion goals.
Channel type misclassification
- Classification is query-time (InsightsQL), driven by
insights/models/channel_type/channel_definitions.jsonthrough thechannel_definition_dictDatastore dictionary; changes reclassify history automatically. - The default decision tree ends in a fallback that maps unknown source +
$directreferring domain to Direct — an unrecognizedutm_sourceon referrer-stripped traffic therefore reads as Direct. Fix by adding definition rows, not by changing the fallback (its behavior is pinned by tests as intentional for garbage UTMs). - Definition changes need a Datastore migration:
add_missing_channel_typesonly INSERTs missing pairs; type changes need a rebuild (truncate → re-insert →SYSTEM RELOAD DICTIONARY). Updatecreate_channel_definitions_file.pytoo, or the next regeneration reverts the JSON. - Same-origin interstitials (bot challenges) destroy
document.referrerwhile preserving query strings — self-referrals with intact UTMs are the signature. Mitigations:before_sendrewrite of self-referrals, a custom channel rule on the customer's own domain, scoping the challenge down.