2
6 Comments

100% of our signups this week are unattributed. i did this to myself

was doing a channel review this week. which channels actually drive signups, kill the rest. simple question.

pulled the data. every signup this week: unattributed. not most. all of them.

took me an hour to accept it wasn't a query mistake.

here's what i built, apparently without thinking about it:

  • the browser session carries all the good stuff. first referrer, utm params, landing page. it lives under the analytics tool's anonymous id.
  • the signup event fires server-side, inside the auth hook, under the brand new user id. clean, reliable, survives ad blockers. i was proud of that part.
  • those are two different people as far as analytics is concerned. the server-side person is born with zero history. no referrer, no utms, nothing.

the browser does call identify() after signup, which is supposed to stitch the anonymous session to the user. but the server event already created the person before that merge lands, and the conversion event itself is stamped on the pre-merge person. so the one event i judge channels on is the one with no first-touch on it.

the punchline: i moved the signup event server-side FOR data quality. ad blockers were eating client events. i fixed the reliability problem and created an attribution black hole. every channel decision i've made off that chart was a coin flip wearing a dashboard.

the obvious fixes all feel wrong:

  • pass the anonymous id from the browser into the signup call and capture the server event under it. works, but now my auth path depends on an analytics cookie being present and readable.
  • fire the conversion client-side again and accept the ad-blocker loss. so, choose between missing data and wrong data.
  • write first-touch into my own db at session start and join it myself. now i'm building a tiny attribution warehouse for a product my size. feels absurd.

genuine question for anyone who's actually solved this: how do you stitch server-side conversions to first-touch without shipping a cookie monster or rebuilding attribution inside your own db? there has to be a boring standard answer and i suspect i'm going to hate it.

on July 16, 2026
  1. 1

    The boring answer is a first-party signup correlation ID, not the analytics cookie itself. Mint it on landing, persist only first-touch fields plus an expiry server-side, carry the opaque ID through auth, then emit signup after the anonymous-to-user link is committed. That keeps auth independent of a vendor cookie and limits the data surface to one short-lived join key.

    1. 1

      this is the right shape, and it quietly fixes the exact places the vendor cookie dies: oauth redirects, safari itp, consent banners. one wrinkle worth adding for anyone implementing it: the join key has to survive the auth round trip, and oauth is where naive versions fail, the user leaves for the provider and comes back with none of your client state guaranteed. threading the id through the oauth state param or holding it in a server session keyed to the pre-auth visit both work. localstorage does not, especially on cross-device magic links where the click lands on a different machine than the landing did. curious where you land on expiry length for the first-touch fields. seven days feels honest, ninety starts feeling like surveillance, and i have never seen a principled argument for any specific number in between.

      1. 1

        I'd tie expiry to the actual buying cycle rather than pick a round number. Start with the 95th percentile of first-touch-to-activation time, cap it at 30 days, and delete the person-level join key after that while keeping only aggregate channel counts. If seven days misses real consideration and ninety feels invasive, the observed decision window gives you a defensible middle.

        1. 1

          p95 capped at 30 is a better rule than the round number i was about to pick. our first-touch-to-activation p95 is closer to two weeks than seven days, so seven would have quietly dropped a chunk of real conversions. dropping the person-level join key after expiry and keeping only aggregate channel counts is probably exactly where this lands.

          1. 1

            Two weeks is useful because now you can make 14 days the working window and keep 30 as a hard ceiling, not the default. I would recompute the p95 monthly and apply any longer window only to new sessions, so a drift in buying time does not silently extend retention for IDs already collected.

            1. 1

              late reply, sorry. the split you describe is the part i had wrong: i was treating the window as one number rather than as a default plus a ceiling, and applying a change retroactively to ids already collected is exactly the silent drift you are pointing at.

              the piece i had not thought about is recomputing monthly. a single p95 taken once becomes a constant that nobody revisits, and buying time is not stationary. treating it as a measurement on a schedule rather than a setting is the actual fix.

              for what it is worth the attribution problem underneath it turned out to be less about windows than about most of our arrivals being genuinely unattributable at the source. search and one job board referrer dominate, and neither carries anything we can key on.