Anatomy of an Adobe Analytics hit (AppMeasurement vs Web SDK)
If you've ever opened your browser's network tab while debugging an Adobe
Analytics implementation, you've seen it: a long, ugly request to something
like /b/ss/examplecompanyprod/1/H29-abc123 or /ee/v1/interact, packed
with query params or a JSON body nobody memorizes on purpose. Tools like
Omnibug or Adobe's own
Experience Platform Debugger will decode that for you -- nicely, even --
so you can eyeball it and go "yep, that looks right."
That's the thing, though: eyeballing it is the whole workflow. There's no
step where a machine checks the hit and fails a build if it's wrong. You
ship a change, the tag either still works or it doesn't, and you find out
when an analyst asks why last week's numbers look weird.
This post is about what's actually inside these two hit shapes, and why I
ended up writing a parser (and then a few more tools) instead of just
living with the debugger-and-eyeball loop.
Two generations
Adobe Analytics tracking comes in two flavors depending on what's
implemented:
AppMeasurement -- the older, still extremely common library. Fires as
either a GET image request or a POST, always to/b/ss/{reportSuiteId}/{version}/{requestType}, with the actual data as
query-string (or form-body) params.
Web SDK (Alloy) -- the newer Experience Platform-based approach. Fires
a POST to */ee/{something}/interact or /collect, with a JSON body
shaped like an XDM event, and (if you're using classic Analytics reporting
alongside it) an __adobe.analytics block tucked inside data.
They look nothing alike on the wire. Here's the same "user viewed the
checkout confirmation page and fired a purchase event" hit, in both shapes.
AppMeasurement
https://metrics.example.com/b/ss/examplecompanyprod/1/H29-abc123
?pageName=checkout-confirmation
&g=https%3A%2F%2Fwww.example.com%2Fcheckout%2Fconfirmation
&events=purchase
&v12=checkout
&c3=checkout-flow
pageName-- the page name, straightforward.g-- the current page URL, URL-encoded.events-- comma-separated success events.purchasehere is a custom
event; it could just as easily beevent5orevent5=29.99(that=
sets a numeric value) orevent5:abc123(that:is a dedup/
serialization id instead -- easy to confuse with=, and I've seen
parsers get this wrong).v12-- eVar 12. Thev/cprefix plus a number is not the same as
the JS variable name (s.eVar12) -- it's the wire key, and Adobe's docs
list them on a different page than the one most people find first.c3-- prop 3, same story.
Numbered eVars/props are the part that trips people up long-term: v12
means nothing on its own. What it represents -- "checkout step," "search
term," whatever -- is configured server-side, per report suite. The wire
format has no idea; it's just a slot.
Web SDK
POST https://edge.adobedc.net/ee/v1/interact?configId=fake-datastream-id
{
"events": [
{
"xdm": { "eventType": "web.webpagedetails.pageViews" },
"data": {
"__adobe": {
"analytics": {
"pageName": "checkout-confirmation",
"events": ["purchase"],
"contextData": {
"checkout.step": "confirmation"
}
}
}
}
}
]
}
No numbered eVars or props on the wire at all. Web SDK sends XDM plus
free-form context data; the mapping from checkout.step (or whatever key
you choose) to an actual Analytics dimension happens in the datastream
configuration, server-side, same as the eVar-slot problem above but now
even further from the client. If you're migrating from AppMeasurement to
Web SDK, this is the part that's genuinely customer-specific -- there's no
universal prop3 -> XDM field table, because the mapping doesn't exist
until someone configures it.
Where a parser earns its keep
Once you've decoded a few hundred of these by hand, the actual interesting
question stops being "what does this hit say" and becomes "does this hit
say what it's supposed to say, every time, in CI, without a human opening
a debugger." That's a different job than Omnibug's, not a replacement for
it -- Omnibug is still what I reach for while I'm building a tag. This
is for after that, when I want "did this still work" to be a test that
fails the build instead of a question someone has to remember to ask.
That's beacon-parser: a
zero-dependency TypeScript library that turns either hit shape above into
one normalized, typed object.
import { parseHit } from "@bonv/beacon-parser";
const hit = parseHit({ url: appMeasurementUrl });
// hit.kind === "appmeasurement"
// hit.pageName, hit.events, hit.eVars["12"], hit.props["3"]
Same function, same output shape, for a Web SDK hit -- hit.kind just
comes back "websdk" instead, with hit.events[].analytics carrying thepageName/events/contextData block. It never throws on something it
doesn't recognize either; unknown fields land under raw/unknown
instead of getting silently dropped or crashing your test run.
On top of that:
@bonv/beacon-playwright
captures hits during a real Playwright test and gives you matchers liketoHaveAdobeEvent("purchase")andtoHaveEvar(12, "checkout").@bonv/tracking-plan
lets you define what a flow is supposed to send as a plain TypeScript
object, then validate captured hits against it with structured pass/fail
reasons instead of a wall of individualexpect()calls.@bonv/beacon-cli
runs that plan against real flows in a real browser and exits non-zero
on failure -- the part that actually plugs into CI.
If you want to see hit decoding without installing anything, there's a
playground that runs the
same published package entirely in your browser. And if you've never
touched Adobe Analytics at all and want to see what a browser actually
sends without needing an Adobe account, there's a
learning lab -- a fake shop with a live
inspector and three guided exercises.
Everything here is client-side only, on purpose. None of it verifies what
ends up in an Adobe report -- server-side processing rules, VRS, and
report-suite configuration are invisible to a browser, so they're invisible
to this too. It answers one narrower question reliably: did the browser
send what it was supposed to send. That turns out to be most of what
breaks in practice.