Browser source
Captures user interactions directly from the DOM using data-elb-* attributes. Extracts tracking data from markup, tracks pageviews automatically or manually, exposes an enhanced elb function with walker init support, and provides a trigger system covering click, submit, load, hover, scroll, and visible.
The browser source is a source in the walkerOS flow:
It reads `data-elb-*` attributes from your HTML and generates events. The Collector routes these to your configured destinations.
Installation
- Integrated
- Bundled
Install the packages:
npm install @walkeros/collector @walkeros/web-source-browser @walkeros/web-source-sessionConfigure in your code:
import { startFlow } from '@walkeros/collector';
import { sourceBrowser } from '@walkeros/web-source-browser';
import { sourceSession } from '@walkeros/web-source-session';
const { collector, elb } = await startFlow({
sources: {
browser: sourceBrowser,
session: sourceSession, // Add for session tracking
},
});Add to your flow.json sources:
"sources": {
"browser": {
"package": "@walkeros/web-source-browser"
},
"session": {
"package": "@walkeros/web-source-session",
"config": {
"settings": {
"storage": true
}
}
}
}Configuration
This source uses the standard source config wrapper (consent, data, env, id, ...). For the shared fields see source configuration. Package-specific fields live under config.settings and are listed below.
Settings
| Property | Type | Description | More |
|---|---|---|---|
prefix | string | Prefix for data attributes (default: data-elb) | |
scope | string | DOM scope for event tracking (default: document) | |
pageview | boolean | Enable automatic pageview tracking | |
capture | boolean | Read click/submit triggers in the capture phase so tagged elements are resolved at click time before app handlers run (set false for the previous bubble-phase behavior) | |
elb | string | boolean | Name for global elb function, or false to install none | |
name | string | Custom name for source instance | |
elbLayer | boolean | string | any | Enable elbLayer for async command queuing (boolean, string, or Elb.Layer) |
Mapping
This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.
Examples
Click event
A button click with walker data attributes is captured as an entity action event with the mapped label data.
"<button data-elb=\"cta\" data-elb-cta=\"label:Sign Up\" data-elbaction=\"click:click\">Sign Up</button>"elb({
"name": "cta click",
"entity": "cta",
"action": "click",
"data": {
"label": "Sign Up"
},
"context": {},
"globals": {},
"nested": [],
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
},
"trigger": "click"
})Context and globals
Walker context and globals attributes attach ambient metadata to events emitted by the browser source.
"<div data-elbcontext=\"test:engagement_flow\" data-elbglobals=\"language:en;plan:premium\"><div data-elb=\"cta\" data-elb-cta=\"label:Try Now\" data-elbaction=\"click:signup\">Try Now</div></div>"elb({
"name": "cta signup",
"entity": "cta",
"action": "signup",
"data": {
"label": "Try Now"
},
"context": {
"test": [
"engagement_flow",
0
]
},
"globals": {
"language": "en",
"plan": "premium"
},
"nested": [],
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
},
"trigger": "click"
})Data attribute types
Walker data attributes parse scalar, boolean, and array values into typed fields on the emitted event.
"<div data-elb=\"product\" data-elb-product=\"price:99.99;available:true;colors[]:red;colors[]:blue\" data-elbaction=\"click:select\"></div>"elb({
"name": "product select",
"entity": "product",
"action": "select",
"data": {
"price": 99.99,
"available": true,
"colors": [
"red",
"blue"
]
},
"context": {},
"globals": {},
"nested": [],
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
},
"trigger": "click"
})Impression event
An intersection observer impression on a div with walker attributes emits a banner view event.
"<div data-elb=\"banner\" data-elb-banner=\"type:promotional;position:sidebar\" data-elbaction=\"impression:view\"></div>"elb({
"name": "banner view",
"entity": "banner",
"action": "view",
"data": {
"type": "promotional",
"position": "sidebar"
},
"context": {},
"globals": {},
"nested": [],
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
},
"trigger": "impression"
})Nested entities
A page load produces a page view plus a product view whose nested size entity is captured on the product event.
"<div data-elb=\"product\" data-elb-product=\"id:SKU-42;name:Sneakers\" data-elbaction=\"load:view\"><div data-elb=\"size\" data-elb-size=\"selected:large;inStock:true\"></div></div>"elb({
"name": "page view",
"data": {
"domain": "example.com",
"title": "",
"referrer": "",
"id": "/"
},
"context": {},
"globals": {},
"trigger": "load",
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
}
});
elb({
"name": "product view",
"entity": "product",
"action": "view",
"data": {
"id": "SKU-42",
"name": "Sneakers"
},
"context": {},
"globals": {},
"nested": [
{
"entity": "size",
"data": {
"selected": "large",
"inStock": true
},
"context": {},
"nested": []
}
],
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
},
"trigger": "load"
})Page view
A page load trigger captures the current URL, title, and referrer as a walker page view event.
""elb({
"name": "page view",
"data": {
"domain": "example.com",
"title": "Documentation",
"referrer": "https://example.com/",
"id": "/docs"
},
"context": {},
"globals": {},
"trigger": "load",
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/docs",
"referrer": "https://example.com/"
}
})Submit event
A form submit with walker data attributes produces a signup complete event carrying the selected plan.
"<form data-elb=\"signup\" data-elb-signup=\"plan:premium\" data-elbaction=\"submit:complete\"></form>"elb({
"name": "signup complete",
"entity": "signup",
"action": "complete",
"data": {
"plan": "premium"
},
"context": {},
"globals": {},
"nested": [],
"source": {
"type": "browser",
"platform": "web",
"url": "https://example.com/",
"referrer": ""
},
"trigger": "submit"
})Enhanced elb
The browser source returns an enhanced elb function with additional features:
// Collector elb: Basic event tracking
collector.push('product view', { id: 'P123' });
// Browser source elb: Enhanced with DOM commands
elb('walker init', document.querySelector('#app'));
elb('product view', { id: 'P123' }); // Also tracks eventsBrowser source elb adds:
- DOM commands:
walker initfor asynchronous DOM element loading - Flexible arguments: support for multiple argument patterns
- elbLayer integration: automatic processing of queued commands
- Element parameters: support for element parameters in commands
See Commands for complete API documentation.
Multiple sources on one page
Any number of browser sources can run on one page. Each keeps its own triggers, its own element registry, and its own visibility state, so nothing one source wires, times, or tears down reaches another.
Two page-level resources are the exception, because each is a single slot:
| Resource | Setting | Rule |
|---|---|---|
The writer installed at window[elb] | elb | The first source to install the name keeps it. |
The command queue at window[elbLayer] | elbLayer | The first source to adopt the array keeps it. |
A source that asks for a name another source already holds is refused that one
resource and logs an error. Everything else keeps working: its scope is still
scanned, its triggers still fire, and its own push still resolves. A page's
own bootstrap, the elb stub function and pre-filled elbLayer array defined
before the source loads, belongs to no source and is adopted as usual.
The DOM is not owned. Any number of sources may scan the same node, document
included. That is how two flows run on one page: give each its own elb name
and its own elbLayer name (or false), and let them share the markup.
A vendor flow beside the page's own
Distinct names on both single-slot resources mean nothing is refused on either
side. pageview: false leaves the page view event to the walker the page
already runs, and the scope narrows the vendor's delegation to the subtree it
cares about.
import { startFlow } from '@walkeros/collector';
import { sourceBrowser } from '@walkeros/web-source-browser';
const { collector, elb } = await startFlow({
sources: {
browser: {
code: sourceBrowser,
config: {
settings: {
elb: 'vendorElb',
elbLayer: 'vendorLayer',
scope: document.body,
pageview: false,
},
},
},
},
});An embedded overlay with no window footprint
elb: false installs no window property at all and elbLayer: false creates no
queue, so the source adds nothing to the page window and reaches its collector
through the elb that startFlow returns. Pair it with an explicit scope and
the overlay captures only its own markup.
import { startFlow } from '@walkeros/collector';
import { sourceBrowser } from '@walkeros/web-source-browser';
const host = document.createElement('div');
document.body.appendChild(host);
const overlay = host.attachShadow({ mode: 'closed' });
const { collector, elb } = await startFlow({
sources: {
browser: {
code: sourceBrowser,
config: {
settings: {
elb: false,
elbLayer: false,
scope: overlay,
pageview: false,
},
},
},
},
});One elbLayer belongs to one walker
A queue is drained once. The source that adopts a named layer reads every entry,
and a command like walker run has exactly one addressee, so a second source on
the same array would starve the first rather than join it. Parallel flows use
separate layer names or elbLayer: false, and share instrumentation through the
data-elb attributes rather than through a queue.
Overlapping scopes fire once per source
Two sources scanning the same tagged element wire it twice. click, hover,
pulse, scroll, wait, impression, and visible each fire once per
source, into that source's own pipeline, and an observed element carries one
dwell timer per source.
The prefix setting decides whether that is what you want:
- Same prefix is deliberate dual collect. One set of attributes feeds both pipelines, and each collector sees the interaction once.
- Different prefix isolates the two. Each source only sees elements tagged for it, so overlapping scopes cost nothing.
Give overlapping sources the same scope root. A source discovers
data-elbobserve
containers inside its own scope only. When two sources share a prefix and the
container sits outside one of their scopes, only the source that discovered it
reaps elements the container removes. For the other one that element's teardown
never runs: its pulse intervals keep firing, its wait timeouts stay armed,
and its hover and visibility registrations stay in place against a node that has
left the document, which also keeps that node referenced until the source is
torn down. A shared scope root means both sources see the same containers.
Known limits
- Version skew. A page running an older release of the package leaves no ownership mark, so a source injected later reads that layer as free and adopts it. This only matters when both use the same layer name, which is what the contract above tells parallel flows not to do.
- Integrations that assign
window[elb]themselves. Thewalker.jsbundle assigns the name after the source has installed its writer, which leaves a value in the slot that belongs to no source, so a second source reads the name as free and installs over it. - The same script included twice. Each copy scans the same markup into its own pipeline, so every event is collected twice. That is a page misconfiguration rather than something the source can resolve, and it is diagnosable: both refusals log at initialization.
Next steps
- Session source - session and device tracking
- Tagging guide - learn about data attributes
- Commands reference - full API documentation
- Tagger tool - visual tagging tool