The tour that counted people who never clicked

The proactive tour on this site auto-advances. It wakes up on a first visit, walks through a few steps of the terminal on its own, and stops. I was most of the way through wiring it when I noticed what it was about to do on every one of those automatic steps: write to the visitor record. Not because I asked it to. Because the record was the nearest durable place to keep a number, and the tour needed to remember which step it was on.

The fix is a split, and for any vibe coder shipping a tour it is the part worth stealing. Keep the tour's state and the visitor's identity in two different places. The step the tour is on is ephemeral session state, scoped to the tab, holding no identity at all. The visitor record, the durable thing that says this browser has been here before, gets written only on a real interaction, a click or a typed message, never on an automatic tour step. Two stores, one wall between them. The tour can count all day and never make a person into a row.

That one decision is the difference between a helpful onboarding feature and a silent tracker, and it is invisible in every test you would think to write, because when you test a tour you interact with it. The passive visitor, the one who lands, lets the animation play, reads, and leaves, is the exact case your testing never covers and the exact case that matters.

why does an onboarding tour start tracking people who never clicked?

Because auto-advance looks like engagement to code that was not told the difference. The tour has to persist one small thing, which step it is on, so it survives a re-render or a page load. It reaches for the nearest store that outlives a render, and on most apps the nearest durable store already wired up is the visitor or analytics identity. The step goes there. Now every passive watcher who let the tour play has a row with their number on it.

The failure here is not malice, and not really a bug in the usual sense. Call it proximity. The identity store was the closest durable hook, so the feature reached for it. This is a specific tax of vibe coding: the generator wires the feature to whatever store is already there, because that is the shortest path to something that works, and it will not pause to ask whether this particular number belongs next to a person's identity. The tour works on the first try. It also quietly changed what your app collects.

Name the general pattern so you catch it next time. A feature's state lands wherever storage is easiest, and if the easiest storage is your identity layer, the feature inherits identity semantics it never wanted. A step counter is harmless. A step counter living on the visitor record is a tracking decision nobody made on purpose.

how do i keep the tour's state without writing a record?

Isolate the two kinds of state before you integrate anything, and never let them share a home. This is the whole method, and it has a name I lean on constantly: isolation before integration. The variable that changes fast and means nothing (which step the tour is on) does not belong in the same store as the variable that changes rarely and carries weight (who this visitor is).

Concretely, three moves. First, put the step in a session-scoped store, tied to the tab, with no id attached. A person who watches and leaves writes nothing that survives them. Second, gate the identity write behind a real interaction, so the record that says this browser has been here is created on the first click or message, never on page load and never on an automatic step. Third, derive the step from the session store rather than from a mounted component's memory, so a hard navigation reloads the page without restarting the tour or double-counting a step. The tour picks up where it was because the count lived in the session, not in a component that just got torn down and rebuilt.

The result is a tour that behaves correctly and collects nothing about people who never chose to be counted. You did not harden that data. You declined to collect it, which is the cheapest privacy control there is.

what breaks with this approach

Session-scoped state means the tour restarts in a new tab. A visitor who opens your app, watches half the tour, and opens a second tab sees the tour begin again there. That reads like a bug and it is not. A new tab is a new session, and repeating a short tour is a smaller cost than the alternative, which is a durable per-person record just to remember that one browser already saw the walkthrough. If the tour is short enough to auto-advance, it is short enough to repeat.

The tempting overcorrection is to reach for a durable cookie or a stored id to remember the tour forever across tabs and visits. Do not. That reintroduces exactly the identity write you just removed, dressed up as a convenience. The moment the tour writes something durable and per-person, you are back to minting records for passive watchers, and now it is deliberate.

The last thing that breaks is your confidence that it works, because a hard navigation mid-tour is easy to get wrong. Verify it in the real environment. Open the tour, hard-refresh in the middle of a step, and confirm the count holds and the identity store is still empty. Do not trust that the split works because the code looks right. Watch the two stores while you use it.

questions that keep coming up

Isn't a step counter harmless? Why does it matter where it lives? The number is harmless. The store it lives in is not. The same integer means "which slide" in a session store and "this specific browser, remembered" on an identity record. Same value, different meaning, decided entirely by which store holds it. Where state lives is a semantic decision, not a storage detail.

How do I tell if my tour is doing this? If you vibe coded an app with an auto-advancing tour, look at what it writes on an automatic step. If any auto-advance touches the same store as your visitor or analytics identity, it is creating records for people who never chose to be counted. It takes about ten minutes: open the app, do nothing but let the tour run, and watch whether anything durable and per-person gets written. If it does, you found it.

Do I need a consent banner for a step counter? A tab-scoped counter with no identity is not the thing consent exists for. The honest move is upstream of consent: do not collect the identity in the first place, and there is nothing to disclose, retain, or ask permission for. Consent is what you reach for when you have already decided to collect. The cheaper answer is usually to not collect.

If you're shipping product with AI and you keep finding state landing in the wrong place, a UI feature quietly writing to your identity layer, and you want a sparring partner on the build, /work-with-us.

// part of the custom apps topic

// grab the free starter kit that makes your AI stop forgetting and stop guessing: get it →

// building with AI? the field manual has the structured lessons.

// hitting this on a real build? this is what I fix →