Adobe Has No Guidance for Shared-Kiosk Analytics. Here's How to Count People Instead of Terminals.

Let me set the scene. You've got a fleet of self-service kiosks in transit stations. Thousands of people walk up to them every day: buy a ticket, print it, head for the gate. You open Adobe Analytics to find out how many people that was.
Unique Visitors comes back at roughly the number of machines you own.
The report isn't broken. Adobe is doing exactly what you configured it to do, which is the part that should worry you.
s_vi cookie, the s_fid fallback: all of them answer the question "is this the same browser?" On a laptop that's a serviceable proxy for "is this the same person." On a machine bolted to the floor of an airport, train station or bus terminal, it's the wrong question, asked confidently, over and over... forever.s.visitorID, on cookie lifetimes, on the identification precedence order. Nothing that addresses the case where one browser is used by hundreds of unrelated people per day. So here's what I worked out and more usefully, the three things that will bite you that no doc warns you about.Why the Obvious Fixes Don't Work
visitor.resetState(), and on paper it does what you want.AMCV_* cookie. It says future IDs get requested differently. Whether that's enough to sever cookie continuity on a kiosk tab that gets reset four hundred times a day and never closes is a question the docs don't answer, and I couldn't find a community source that resolved it either.The second instinct is to delete the AMCV cookies yourself and re-init. That works right up until Adobe changes their cookie names or adds one you didn't know about or pivot away from first party cookies entirely, and you find out six weeks later from a report nobody trusts anymore.
sessionStorage, let the browser clear it, done.sessionStorage is scoped to the tab's lifetime, and a kiosk browser launches at boot and never closes. Tab lifetime is machine uptime usually; which can be several days. That storage clears when the kiosk reboots, which might be monthly. localStorage and cookies have the same problem with less excuse, and even in-memory state only clears if your app actually tears down, which it doesn't if you reset by navigating within the same tab.None of the four storage locations solves this by default, and it's worth pausing and asking why: there is no browser-level lifecycle event that corresponds to "a different human is standing here now." That signal exists in exactly one place, which is your application's own session-timeout handler. Whatever you build has to hang off that.
post_visid_high / post_visid_low, which is whatever the identification chain resolved to. Stamping a GUID into eVar 42 doesn't touch that. You'd be counting distinct values of an eVar and calling it visitors, which works in a calculated metric and confuses everyone who reads the standard report next to it.The One Variable That Actually Works
s.visitorID still exists, still works, and sits at the top of Adobe's identification precedence:vid (s.visitorID) <- first one present wins
aid (s_vi cookie)
mid (ECID)
fid (s_fid fallback)
IP + User-Agent <- last resortSet it and everything below it is ignored for that hit. No cookie surgery, no fighting the ID service, no dependence on an under-documented reset method.
s.visitorID is incompatible with Analytics for Target, with shared audiences, and with Customer Attributes. Every hit for a logical visitor must carry an identical value or the visitor fragments. A constant or default value, the classic being a literal "0" or "NULL" leaking through, collapses unrelated people into one visitor.s.visitorID a bad idea on a marketing site are mostly inapplicable here, which is a large part of why this is the right call on a kiosk and the wrong call almost everywhere else.Two constraints on the value itself, and both are load-bearing: alphanumeric only (Adobe says avoid dashes, underscores, and symbols) and 100 bytes maximum. That rules out a raw UUID. Strip the hyphens:
function mintVisitorGUID() {
if (window.crypto && typeof window.crypto.randomUUID === "function") {
return window.crypto.randomUUID().replace(/-/g, "");
}
var bytes = new Uint8Array(16);
window.crypto.getRandomValues(bytes);
var hex = "";
for (var i = 0; i < bytes.length; i++) {
hex += (bytes[i] + 0x100).toString(16).slice(1);
}
return hex;
}Math.random() anywhere. crypto.randomUUID() requires a secure context and crypto.getRandomValues() does not, which is why the fallback is the one that survives a non-HTTPS environment rather than the other way around.vid on the hit and a fall through to the device identity, which is recoverable. A guessable ID is not.The Part Nobody Documents
Everything above is reconstructable from Adobe's docs if you're patient. These three aren't, and each one produces a failure that looks like success.
The first beacon of a visit is the one that matters most, because it's the one attached to a person who just walked up. If your Angular or React app mints the ID in a component lifecycle hook, that first hit already fired with an empty value. It falls through to the device identity, and now a single person's visit is split across two identities: machine-level for hit one, person-level for the rest.
<body>, which parses before the SPA bundle has finished downloading, let alone bootstrapping. Then have the app read the existing value rather than generating its own.Once you accept that no browser event marks a new person, you have to decide which of your own events do. Session timeout is obvious. Finishing a transaction and walking away is obvious. A "Start Over" button is not, and it's worth thinking about for longer than it seems to deserve.
Someone who abandons a booking and restarts is _usually_ the same human. Rotating there inflates Unique Visitors. But on an unattended terminal you cannot actually distinguish "same person restarting" from "they left and someone else stepped up," so you're picking which direction to be wrong in.
I preserve across Start Over and rotate on timeout and completion, which means the ID has to survive the same storage wipe that's designed to forget the previous user:
public clearSession(): void {
const terminal_id = this.get(StorageName.TERMINAL_ID);
const visitor_guid = this.get(StorageName.VISITOR_GUID); // person-scoped, survives
sessionStorage.clear();
if (terminal_id) { this.set(StorageName.TERMINAL_ID, terminal_id); }
if (visitor_guid) { this.set(StorageName.VISITOR_GUID, visitor_guid); }
}
// and at the real boundaries, explicitly:
public resetVisitorGuid(): void {
sessionStorage.removeItem(StorageName.VISITOR_GUID);
}That preserve list is worth reading as a design document, because it's the line between "belongs to the machine" and "belongs to the person." Terminal ID, hardware config, and API credentials are on the machine side. The visitor GUID is deliberately not, and it's only on that list because Start Over is the one boundary where the person doesn't change.
doPlugins, not in a rule, and not in the extension's Visitor ID field.**This one cost me the most time, and I only caught it because I checked the wire instead of the UI.
Launch rules are per-page by construction. Put the assignment in a rule and it lands on the pages that rule covers and silently vanishes everywhere else, and those hits fall through to the device identity. My property had twenty rules, most of them page-specific DOM Ready rules. There's no rule that reliably covers everything.
<script async>, tracker configuration is racing the script that builds your data layer. On the load I measured, the document finished at about 4.0s and AppMeasurement landed at about 4.39s, so init-time assignment won. That's luck, not a guarantee, and it's the kind of luck that holds in testing and breaks on a cold cache.doPlugins runs immediately before each hit serializes, well after DOM Ready:// Adobe Analytics extension -> Configure -> Custom Code
s.usePlugins = true;
s.doPlugins = function (s) {
var guid = _satellite.getVar("Visitor GUID");
if (guid) { s.visitorID = guid; }
};s.usePlugins = true is not optional. AppMeasurement only calls doPlugins when that flag is set, so the function on its own is inert and fails completely silently.s.visitorID = undefined while your data layer is still empty, and depending on how it stringifies you can end up sending a literal vid=undefined on every hit, which merges your entire report suite into one visitor for as long as it's live. Guarded, an empty value means s.visitorID stays genuinely undefined and AppMeasurement omits the parameter:data layer empty ...&fid=20442CF8B889A27C-...&ce=UTF-8&g=... no vid
data layer populated ...&vid=a3f9c1d84e2b47...&fid=20442CF8B889A27C-... vid present_satellite.getVar() resolving correctly in the console proves the data element works and proves nothing about whether anything consumes it. I had a property where the data element was perfect and the assignment had silently failed to save, and the only way to see the difference was a beacon with no vid on it.Don't Deploy ECID on a Shared Kiosk
The reflex when analytics identity is misbehaving is to reach for the Experience Cloud ID Service, since it's the modern, recommended, well-supported answer. On a kiosk it's actively harmful, and this is the point I'd most want someone to take away.
vid outranks mid. It doesn't restore the features you'd want it for, because s.visitorID is incompatible with A4T, Customer Attributes, and shared audiences whether or not an ECID exists.And it's machine-scoped, and it's the identity that flows into all of Adobe's downstream systems. Deploy it on a shared terminal and you don't get one profile per traveler. You get one profile per machine, accumulating the trips, destinations, and behavior of every person who ever used it into a single synthetic human that AEP will then happily segment and activate on.
That's worse than a bad number in a report, because a bad number stays in the report. A bad profile gets acted on.
What I Learned
usePlugins forgotten. The beacon is the only thing that knows what actually happened.vid missing means graceful fallback to device identity. vid=undefined means every hit in the suite belongs to one visitor. Those two states are one missing if apart, and only one of them is loud.The Takeaway
The whole fix is about forty lines: a mint function, a preserve-and-rotate pair in the storage layer, and five lines of Launch custom code. It is not hard. It's just that none of it is written down anywhere, and every individual piece has a failure mode that produces plausible-looking data instead of an error.
What still bothers me is that the default configuration doesn't fail loudly. A kiosk fleet reporting Unique Visitors equal to its machine count is a number a dashboard will render without complaint, that a quarterly deck will cite, and that nobody questions until someone asks how many people used the kiosks last Tuesday and gets an answer that's off by three orders of magnitude.
Shared-device analytics is a real category with real deployments behind it, and Adobe treats it as though it doesn't exist. Until that changes, you're reconstructing this from a precedence table and a cookie reference page.
s.visitorID, or if you got resetState() to reliably sever cookie continuity, because I'd genuinely like to be wrong about that one.*Comments
Loading... on "Adobe Has No Guidance for Shared-Kiosk Analytics. Here's How to Count People Instead of Terminals."
Join the Discussion
Related Articles
Continue reading with these related posts

The AEP Field Change That Could Have Broken Everything — And the Scanner I Built to Prevent It
How a silent type mismatch between sandboxes sent me down a rabbit hole that ended with me building a tool Adobe should've shipped years ago.

The Hidden Tax on Your Customer Data
*After 16 years implementing multi-channel analytics for enterprises, I've watched the pendulum swing hard toward cloud. Now I'm watching it start to ...

The Hidden Challenges of Adobe Analytics in Ionic Capacitor Apps
Spent weeks fixing 30-second app startup hangs in my Ionic Capacitor app. The culprit? Adobe Analytics making native network calls to demdex.net durin...