walkerOS for developers
Tracking that ships with the feature, reviewed in the same pull request.
Tag components with data-elb attributes or call elb() from your code. The browser source turns those into events, the collector adds consent, user and globals, and each destination maps them into its vendor format, all configured in TypeScript or in a flow.json you review like any other file.
Sounds familiar
Tracking bugs that end up on your desk
page_view never fires, or fires twice once someone adds a History Change trigger on top of Enhanced Measurement.The browser source does not try to detect route changes. You call elb('walker run') when your router navigates, which fires one page view and re-scans the page for tagged elements. The React and Next.js quickstarts show where the call goes and how to skip it on the first render.dataLayer.push it depends on and sends undefined. It works in preview and on slow pages, and loses data on fast ones.A tagged click is resolved in the capture phase, from the DOM as it is at click time and before your own click handlers run, so the event reads the data-elb-* values of the element that was clicked. Calls queued on window.elbLayer before walkerOS loads are processed once it starts. See the click trigger in the HTML attributes reference.data-elb markup in the component file, so it lands in the same pull request as the feature and gets the same review. In JSX, createTagger builds the attributes as an object you spread onto the element and escapes the values for you.elb() calls use your own entity and action names. The vendor's event names and fields live in each destination's mapping, so the next vendor change is a config diff.Tagging
Tracking attributes live in the component file
data-elb names the entity, data-elb-product holds its properties, and data-elbaction pairs a trigger such as click or visible with the action to fire. The browser source reads those values from the DOM when the trigger fires, so a component that moves or rerenders takes its tracking along. Renaming CSS classes in a redesign leaves it alone.
<div data-elb="product" data-elbaction="visible:view"> <h2 data-elb-product="name:Everyday Ruck Snack">Everyday Ruck Snack</h2> </div>
Fires product view with data: { name: 'Everyday Ruck Snack' } once the card has been on screen for a second, and again each time it comes back into view. Reference: HTML attributes.
Contracts
Write down what an event must contain, and drop what breaks it
A contract is JSON Schema keyed by entity and action, kept in the top-level contract block of flow.json. A validate transformer references it with $contract.<name>. In strict mode an event that fails is dropped before any destination sees it; in pass mode it continues with source.valid set to false, so a later step can route it. Contracts can extend each other, which keeps a shared base and a per-site addition in one file.
{
"version": 4,
"contract": {
"web": {
"events": {
"order": {
"complete": {
"properties": { "data": { "required": ["total", "currency"] } }
}
}
}
}
},
"flows": {
"default": {
"transformers": {
"validate": {
"package": "@walkeros/transformer-validate",
"config": {
"settings": { "contract": ["$contract.web"], "mode": "strict" }
},
"next": "ga4"
}
}
}
}
}An order complete without data.total or data.currency stops at this step and never reaches ga4. Inheritance and wildcards: contract.
Testing
Catch a broken event in CI
Each step in a flow can carry named { in, out } examples. walkeros validate flow.json checks that connected steps fit together and that every example satisfies the contract. With --strict a violation counts as an error and the command exits non-zero, which fails the build. walkeros push --simulate runs one event through the flow with the named destination mocked and reports whether it accepted the event, without calling the vendor.
# Fails the build if an example breaks the contract
walkeros validate flow.json --strict
# Runs one event with the ga4 destination mocked
walkeros push flow.json --simulate destination.ga4 --event '{"name":"page view","data":{"title":"Home"}}'Example format: step examples. Every command and flag: CLI reference.
Where it fits
walkerOS runs next to what already sends events
What stays
- Your
dataLayer.pushcalls and your GTM container. They keep running, and the dataLayer source can read the same pushes into walkerOS while you move todata-elbone component at a time. - Your analytics vendors. Each one becomes a destination with its own mapping, so adding or swapping one is a config change.
- Your build, code review and CI. A flow is TypeScript in your app or a
flow.jsonin the repo, so it goes through the same pull requests as the rest of your code. - Your naming. Entities and actions come from your product, written as
entity actionwith a space.
What changes
- Tracking moves into component markup or your own
elb()calls and gets reviewed with the feature. - Vendor event names and fields move out of your code into one mapping per destination.
- A contract in
flow.jsonstates what each event must contain, and a validate transformer drops or flags events that break it. - You pick a mode. Integrated runs the collector inside your app, typed and configured through
startFlow. Bundled keeps the setup inflow.json, and the CLI builds it into a separate file.
Run npm install @walkeros/collector @walkeros/web-source-browser, add the console destination from the quickstart, then tag one component with data-elb and watch its event in the console before you connect a real vendor.
What walkerOS does not do for you
- Contracts are JSON Schema you write by hand. Nothing generates one from a tracking plan spreadsheet or infers it from the events you already send.
- Tagging an existing app is work you do component by component. walkerOS does not scan an untagged site to add attributes, and it does not convert a GTM container.
walkeros validatechecks the examples in your flow, not your real components. A release that drops adata-elb-productattribute still passes it, and the gap shows up only at runtime, when a validate transformer checks the live event.walkeros push --simulatereports success or failure per destination. It does not print the vendor payload, so to inspect that you need theflow_simulatetool of the MCP server withverboseset.- When contracts
extendeach other, onlypropertiesandrequiredmerge. Keywords such asoneOf,enumandallOftake the child value, so combine those in one schema yourself.
Questions
Questions before you start
Why name events "product view" when "Product Viewed" reads better for stakeholders?
The entity comes first because the rest hangs off it: data-elb-product holds the properties, a destination mapping is keyed product.view, and a contract rule under product with the * action applies to every product action. Free-form names drift, which is how one tool ends up with page_view and another with Page Viewed. A friendlier label for stakeholders belongs in the report. See product view become GA4 view_item in the mapping example.
Our tracking is DOM-based in GTM and mostly works. What changes, and what do we keep?
You keep the container and your dataLayer.push calls, and you add data-elb attributes next to them one component at a time. The failure mode is what changes: a GTM trigger on a CSS selector is separate from the markup it depends on, so a template change can break it without anyone noticing, while a data-elb attribute sits on the element and moves with it. Retire each old trigger once the tagged component sends what you expect. The HTML attributes reference has the full syntax to compare against your triggers.
How do I know a mapping or contract change is right before it deploys?
Give the step a named { in, out } example and run walkeros validate flow.json --strict in CI. It fails when an example breaks the contract or when connected steps don't fit. walkeros push flow.json --simulate destination.<name> then runs a real event through the flow with that destination mocked. The validate guide walks through breaking a contract on purpose so you can see the error it prints.
Next steps