Documentation · Troubleshoot · Missing attribution data
Why am I missing attribution / first-touch data?
Between 2026-04-29 and 2026-05-05 a small number of Admaxxer workspaces saw two visible symptoms: empty first-touch UTM data on the Acquisition card, and a frozen Pixel Events This Period counter on the billing page that always read 0. Three pre-existing bugs surfaced during a routine post-deploy verification of GL#367 (editable dashboards). All three are now fixed, the last 7 days of first-touch data has been backfilled, and we’ve added postbuild canaries so this exact failure shape can’t recur silently. This page explains what broke, what was recovered, and how to verify your workspace.
What broke
GL#368 — Tinybird ingest 403 on visitor_first_touch_landing. The ingest helper that writes raw first-touch rows to Tinybird had a token-fallback chain that resolved to a per-datasource token without falling back to either TINYBIRD_INGEST_TOKEN or TINYBIRD_ADMIN_TOKEN as final escapes. In dev the per-datasource token happened to be sufficient; in prod it wasn’t, so every identify-driven first-touch write returned 403. The pixel still captured first-touch tuples on the client side, but the server-side raw datasource that the visitor_first_touch AggregatingMergeTree is built on (per GL#361) silently rejected each row. Net effect: the Acquisition card on /dashboard/analytics and the source-breakdown on /marketing-acquisition showed UTMs only for visitors who had hit a page within the cache window — returning visitors with stored first-touch from before the deploy were rendered as Direct.
GL#369 — workspaces.monthly_events_used read but never written. The billing UI’s Pixel Events This Period tile and the admin dashboard’s workspace usage table both read from workspaces.monthly_events_used, a numeric counter on the workspaces table. A repo grep found 14 read sites — and zero write sites. No code path was incrementing the counter. The column had survived a refactor as a read-only widget while the writer was never restored. Result: every workspace showed 0 events used regardless of actual Tinybird traffic, even on workspaces processing millions of events per day. The actual rate-limiter at /api/event queries Tinybird directly via a different code path, so quota enforcement was always correct — this bug only affected the display surface, not throttling. Customer-impacting because a merchant looking at /billing would conclude they’d used 0 of their plan quota regardless of reality, which makes mid-month plan-upgrade decisions opaque.
GL#370 — Stripe migration error at boot. Every container boot was logging type "stripe.subscription_status" does not exist because the stripe.* schema migration sequence had drifted — a typed enum was being referenced by a downstream CREATE TABLE before the CREATE TYPE SQL that defined it ran. Customer-facing impact was zero (the actual Stripe sync code catches the error and falls back to an untyped column), but the noise made it harder to spot real Stripe-related errors in production logs. Bundled into the same ship for documentation completeness.
Symptoms you may have noticed
- Acquisition card on
/dashboard/analyticsshowing fewer source/campaign attributions than expected, with returning visitors classified as Direct when they had previously been classified as Paid Search / Paid Social / Email. - Source-breakdown on
/marketing-acquisitionmissing rows for utm_source values you know are running in active campaigns. Pixel Events This Periodtile on/billingstuck at0regardless of how many real events your store is generating.- Workspace usage table on the internal
/adminpage showingmonthly_events_used = 0for every workspace. - Boot logs occasionally showing
type "stripe.subscription_status" does not exist— only visible if you have access to Coolify container logs.
If your dashboard otherwise looked fine — pageviews and revenue counts were correct, you just had fewer attributed UTM sources than you expected — you were hitting GL#368. Pageviews, sessions, payments, and goal events all wrote to separate Tinybird datasources that were never affected.
What we fixed
- Token fallback chain extended.
tokenFor()inserver/routes/pixelIngest.tsnow falls back throughTINYBIRD_INGEST_TOKEN→TINYBIRD_ADMIN_TOKENas final escapes when no per-datasource token is configured. Newscripts/check-tinybird-token-scopes.tspostbuild canary validates every Tinybird ingest call has a token scope sufficient for its target datasource — the build fails if a future PR introduces another silent 403. - Hourly aggregator for the events counter. A new BullMQ cron pulls the truth from Tinybird’s
summary_kpispipe every hour and writesmonthly_events_usedback to the workspaces table. Aggregator-based instead of per-eventINCRbecause it’s race-free (no double-increment under concurrent ingest) and self-healing (the counter converges to truth even if a write is missed). Newscripts/check-monthly-events-counter-writer.tspostbuild canary fails the build if any column onworkspacesis read by 3 or more sites without a corresponding writer. - Idempotent Stripe migration applied to Neon prod. The
stripe.subscription_statusenum and the dependent table are now created in correct order via an idempotentCREATE TYPE IF NOT EXISTS→CREATE TABLE IF NOT EXISTSsequence inserver/billing/stripeSync.ts. New admin health-check endpoint at/api/admin/health/stripe-migrationsgoes red if any required Stripe schema artifact ever goes missing again — surfaces the failure in real time instead of leaving it buried in boot logs.
Backfill window
The last 7 days of first-touch tuples were re-populated from pageviews for every merchant. We replayed every pageview row landed between 2026-04-29 and 2026-05-05 through the now-fixed ingest path; visitor_first_touch_landing is now caught up and visitor_first_touch AggregatingMergeTree has merged the new state. First-touch data older than 7 days is not recoverable — the source rows simply weren’t in the system to replay. In practice 365-day first-touch only matters for cohorts that span more than 7 days of dormant visitor return, so most workspaces will see no functional gap. If your store relies on first-touch attribution for cohorts longer than 7 days (e.g. quarterly cohort LTV reports), expect slightly lower match rates on rows whose first touch fell inside the loss window — downstream rows will fall back to last-click only.
The Pixel Events This Period counter is correct from the moment the hourly aggregator’s first run lands — no replay needed because the truth lives in Tinybird, not on the workspaces row.
How to verify your data is now flowing
- Confirm pageviews are growing. Open /marketing-acquisition. The pageview total at the top of the page should advance as your store gets traffic. If it’s flat for >30 minutes during business hours, the pixel itself is the issue (see CSP errors blocking the pixel).
- Confirm source breakdown is non-empty. Open /dashboard/analytics and scroll to the Acquisition card. You should see UTM sources for any campaigns running this week (
google/facebook/klaviyo/ direct / etc.). Returning-visitor rows from the loss window will fall back to last-click; new visitors from this week onward will show full first-touch. - Confirm the events counter is populating. Open /billing. The
Pixel Events This Periodtile should show a non-zero number that matches your actual Tinybird traffic. The aggregator runs hourly — if you just deployed, allow up to 60 minutes for the first run to land. - Confirm Stripe migrations are clean. Workspace owners with admin access can hit /api/admin/health/stripe-migrations. A green response means every required Stripe schema artifact is present; a red response surfaces the missing artifact name.
If any step still shows the bug-era symptom, the page Missing orders walks through the deeper causes (pixel install, consent blocking, webhook lag) that can produce a similar empty-data look.
FAQ
- Was my data lost forever?
- Pageviews, sessions, payments, and goal events were never lost — those write to separate Tinybird datasources that were not affected by the bug. The narrow loss was on the `visitor_first_touch_landing` raw datasource: rows that should have written between 2026-04-29 and 2026-05-05 were rejected by the Tinybird API with a 403. We backfilled the last 7 days of first-touch tuples (UTMs + referrer + landing path) by replaying every `pageview` row in that window through the now-fixed ingest path. First-touch data older than 7 days is not recoverable, but in practice 365-day first-touch only matters for cohorts that span more than 7 days of dormant visitor return — most workspaces will see no functional gap.
- Why did my billing page show `0` events used?
- The `workspaces.monthly_events_used` column was being read by the billing UI and the admin dashboard, but no code path ever wrote to it. Effectively a column that survived a refactor as a read-only widget — the writer was never restored. We've shipped an hourly aggregator cron that pulls the truth from Tinybird's `summary_kpis` pipe and writes the latest count back to `workspaces.monthly_events_used`, plus a postbuild canary that fails the build if any other column drifts into the same read-but-never-written shape. Your billing page now reflects real usage within an hour of any traffic change.
- Did this affect my plan limits or trigger over-quota throttling?
- No. The `monthly_events_used` column was a display-only counter — the actual rate-limiter at `/api/event` reads quota from Tinybird directly via a different code path, so quota enforcement was always correct. The bug only affected the `Pixel Events This Period` tile on /billing and the workspace usage table in /admin. Nobody was throttled because of the frozen counter.
- How was the Tinybird token bug not caught at deploy time?
- The token fallback chain that broke `visitor_first_touch_landing` ingest had a path that resolved to a per-datasource token without falling back to either `TINYBIRD_INGEST_TOKEN` or `TINYBIRD_ADMIN_TOKEN` as final escapes. In the dev workspace the per-datasource token happened to work; in the prod workspace it didn't. This is the same shape as GL#363 (a route handler defined but never registered) — a feature shipped but the wiring resolved to the wrong endpoint. We've added a `check-tinybird-token-scopes.ts` postbuild canary that validates every Tinybird ingest call has a token scope sufficient for its target datasource, so this exact failure mode can't recur silently.
- What's the Stripe migration error and how is it related?
- Unrelated to the attribution bug, but bundled into the same ship. Every container boot was logging `type "stripe.subscription_status" does not exist` because the `stripe.*` schema migration sequence had drifted — a typed enum was being referenced before the SQL that created it ran. Customer-facing impact was zero (the actual Stripe sync code paths catch and recover), but it spammed boot logs and made it harder to spot real Stripe errors. We've shipped an idempotent migration applied directly to Neon prod, fixed the migration ordering inside `server/billing/stripeSync.ts`, and exposed a new admin health-check at `/api/admin/health/stripe-migrations` that goes red if any required Stripe schema artifact is ever missing again.
Next steps
- Attribution discrepancies — if your numbers disagree with Meta or Google by more than 20%, this is the reconciliation methodology.
- Sales mismatch (revenue) — if your Shopify total revenue disagrees with Admaxxer’s revenue card.
- Missing orders — if Admaxxer is missing entire orders that exist in Shopify.
- Back to Documentation