Skip to main content

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

August 9, 2026
Calculating...
PlaygroundImage.jpeg

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.

Every visitor identification mechanism Adobe ships is device-scoped to a Browser. The ECID, the legacy 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.

I went looking for Adobe's guidance on this and came up empty. I ran the search from several angles: kiosk, shared device, public terminal, unattended. Experience League has pages on ECID, on 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

The first instinct is to reset the ECID between users. There's an API for it, visitor.resetState(), and on paper it does what you want.

Read the docs closely and it gets shakier. Adobe scopes that method to A4T customers and Ajax-driven partial page updates, where the ID service can't otherwise tell that a "new page" happened. Nothing in the documentation says it deletes or overwrites the 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.

The third instinct is storage, and this is the one that catches people, because it sounds like it should just work. Put a session ID in sessionStorage, let the browser clear it, done.

Except 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.

The fourth instinct is to give up on identity and put a session GUID in an eVar. This is genuinely tempting because it's zero-risk, and it's what I'd recommend if you only need to segment. But Unique Visitors is computed from 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 resort

Set 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.

Adobe discourages it, and they're not being coy about why. Their own page on the precedence order notes that the order doesn't reflect the order they recommend. The costs are specific: 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.

Read that list of costs from a kiosk's perspective, though. You're probably not personalizing a ticket machine with Target. You're not activating a shared audience against a walk-up terminal. Customer Attributes needs a CRM key you don't have for an anonymous traveler. The restrictions that make 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:

js
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;
}

Two branches, both CSPRNG, no 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.

Note what's missing: a third branch. If neither is available this throws, the caller catches, and the ID stays empty. That's deliberate. An empty ID means no 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.

Mint it before your framework boots.

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.

Mint it in whatever runs earliest. In my case that was a server-rendered script at the bottom of <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.

The reset boundary lives in your app, and "start over" is a different question than "timed out."

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:

ts
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.

**Set it in 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.

The extension's declarative Visitor ID field looks like the answer, and it nearly is, but it resolves when the tracker is configured. If your Launch embed is <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:

js
// 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.

The guard is the other half. An unguarded assignment writes 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

Check that in Non Prod before you ship it. _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.

It contributes nothing to identification, because 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

Storage lifecycle is not session lifecycle, and on a kiosk they aren't even close. A browser that never closes turns every "temporary" storage mechanism into permanent storage. If your reset logic isn't wired to an event your own application raises, you don't have reset logic.

**Verify identity on the wire, not in the UI.** Every real bug I hit was invisible from Launch's interface and obvious in the query string. Data element resolving correctly, assignment silently absent. Custom code saved, usePlugins forgotten. The beacon is the only thing that knows what actually happened.

**The absence of a parameter is a feature.** 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.

Adobe's kiosk guidance doesn't exist. I'm confident about this rather than merely unable to find it, because several different search angles all converged on the same generic pages. That's a real gap in the documentation for a use case that every transit agency, airline, cinema chain, and hospital check-in desk has.

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.

*If you're running Adobe Analytics on kiosks, ATMs, in-store terminals, or anything else where one browser serves many people, I'd like to know how you handled visitor identity. Especially if you found something better than overriding 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

0/5000
reCAPTCHA loading...

Related Articles

Continue reading with these related posts