Clickstream Tracking
Client-side event tracking for the product. Events are declared declaratively in
the markup with data-* attributes (no JS wiring needed) or dispatched from your
own JS. The tracker batches events and sends them to the backend.
Sections
- Script Integration
- Data Attributes
- Iframe Tracking
- Custom Events
- Scroll Tracking
- Performance Tracking
- User Identification
Script Integration
The script is available at a relative path within the product.
Add it to the end of the <head> section in your HTML:
<head>
...
<script async type="module" src="/ext/clickstream/stream.min.js"></script>
</head> The script loads fp.min.js automatically when needed. You do not need to add fp.min.js to the HTML separately.
Data Attributes
Events are declared declaratively in the markup using data-* attributes. No JS
wiring is required — the tracker listens globally and picks up any element that
carries one of the attributes below.
Event-trigger attributes
| Type | Attribute | Fires on |
|---|---|---|
| Click | data-track-event-click | click (capture, delegated; the nearest matching ancestor is used) |
| Form submit | data-track-event-form-submit | submit of the <form> itself |
| Text input | data-track-event-text-input | focusout of an <input> (i.e. when the field loses focus) |
| Iframe load | data-track-event-iframe-load | iframe present at load or inserted later — see Iframe Tracking |
| Scroll | data-track-event-scroll-start, data-track-event-scroll-y-depth, data-track-event-scroll-x-depth | scroll — see Scroll Tracking |
Example:
<button data-track-event-click="event_name"></button> The attribute value is the event name. It is sent to the backend prefixed by the
event type, so the example above produces click:event_name. Each type uses its
own prefix: click, formSubmit, textInput, iframeLoad, scroll-start, scroll-y-depth, scroll-x-depth.
Passing data with an event
Any attribute prefixed with data-track-data- is collected into the event’s data object. The key is the attribute name with the prefix stripped; empty
values are ignored.
By convention:
data-track-data-value— an abstract value.data-track-data-currency— a currency.
Any other data-track-data-* attribute is allowed and passed through as-is.
Example:
<button
data-track-event-click="event_name"
data-track-data-value="10"
data-track-data-currency="USD"
data-track-data-bonus="ABC"
></button> The backend receives:
{
type: "click:event_name",
data: {
value: "10",
currency: "USD",
bonus: "ABC"
},
metadata: {/**/}
} The same data-track-data-* mechanism works for every event type (click, form
submit, text input, scroll), not just clicks.
Custom events
For events fired from your own JS (not tied to a DOM element), see Custom Events.
Logging
Run localStorage.setItem('clickstream_debug', 'true') (the legacy tracker_debug key also works) to log every queued and flushed event to the
browser console. Debug logging is also on automatically in dev builds.
Iframe Tracking
Iframe loads are tracked with data-track-event-iframe-load="event_name".
<iframe data-track-event-iframe-load="event_name" src="https://example.com/widget"></iframe> Event iframe-load
The event is sent as iframeLoad:event_name. The iframe src (falling back to data-src, then about:blank) is passed in data.value:
{
type: "iframeLoad:event_name",
data: { value: "https://example.com/widget" },
metadata: {/**/}
} Notes
- Both iframes already present on page load and iframes inserted later are
tracked — a
MutationObserverwatches the DOM, including nested iframes. - Each iframe fires only once; an internal marker attribute prevents duplicates.
- Deposit iframes are tracked separately and automatically (no attribute needed):
any iframe whose
allowincludespaymentis reported asiframeLoad:depositIframeLoaded, with the transaction id parsed from thesrcindata.valueand the fullsrcindata.src.
Custom Events
For cases that cannot be expressed with data-* attributes (e.g. events fired
from your own JS), the tracker listens for a track:custom event on window.
window.dispatchEvent(
new CustomEvent('track:custom', {
detail: {
name: 'event_name',
data: { value: '10', currency: 'USD' },
},
})
); The event is sent as custom:event_name:
{
type: "custom:event_name",
data: { value: "10", currency: "USD" },
metadata: {/**/}
} Rules
detail.nameis required and must be a string. If it is missing or not a string, the event is ignored.detail.datais optional. It is used only when it is an object; anything else (or omitting it) results in an emptydata: {}.
Scroll Tracking
The scroll tracking system provides three types of events to monitor user interaction with scrollable elements (including the page itself).
1. Scroll Start
Tracks the first actual user movement within a scrollable area.
- Attribute:
data-track-event-scroll-start="event-name" - Event sent:
scroll-start:event-name - Behavior:
- Fires only once per element per session.
- Session-Pure: It ignores browser scroll restoration (e.g., on page refresh). It only triggers when the scroll position actually changes from its initial landing state.
2. Scroll Depth (Milestones)
Tracks when a user reaches specific milestones (percentages or pixels) during scrolling.
- Vertical Trigger:
data-track-event-scroll-y-depth="event-name"(sent asscroll-y-depth:event-name) - Horizontal Trigger:
data-track-event-scroll-x-depth="event-name"(sent asscroll-x-depth:event-name)
The reached milestone is included in the event payload as data.depth (e.g. "50%" or "500px").
Milestone Parameters
You can customize milestones using the following attributes (comma-separated values):
| Attribute | Unit | Logic / Description |
|---|---|---|
data-track-scroll-y-progress / data-track-scroll-x-progress | % preferred | Visibility-based. Fires when the bottom edge (y) / right edge (x) of the viewport reaches the specified percentage. Example: 10%, 50% |
data-track-scroll-y-offset / data-track-scroll-x-offset | px preferred | Distance-based. Fires when the top edge (y) / left edge (x) of the viewport (scroll position) reaches the specified pixel offset. Example: 500px |
Default Behavior
If no progress or offset parameters are provided, the system defaults to tracking 100% progress.
Attribute Placement (Direct vs. Wrapper)
The tracking attributes can be placed in two ways:
- Directly on the scrollable container (
overflow: scroll). - On a Wrapper (any parent element). The system will automatically detect the scrolling child and use its position for tracking. This is useful for grouping attributes or tracking complex components.
Nested & Sibling Scrolls (Priority & Isolation)
- Closest Wins: If multiple elements in the DOM hierarchy have scroll attributes, the tracker uses the config from the closest ancestor (including the element itself).
- State Isolation: Tracking state (e.g., reached milestones or initial position) is stored directly on the actual scroller, even if the configuration is inherited from a wrapper. This allows multiple sibling scrollers to share a single parent wrapper without interfering with each other.
Immediate Tracking vs. Session Tracking
- Immediate Milestones: If a milestone (e.g.,
10%) is already visible when the page loads, the event fires immediately. This ensures analytics capture the initial state of the user. - Scroll Start: Remains quiet until the user performs a physical scroll action.
Examples
Simple usage (default 100% milestone) — if you only provide the event trigger, the system automatically tracks when the content is fully scrolled (reaches 100% progress):
<div data-track-event-scroll-y-depth="read-article">... content ...</div> Custom milestones:
<div
data-track-event-scroll-y-depth="article-progress"
data-track-scroll-y-progress="25%, 50%, 75%, 100%"
data-track-scroll-y-offset="500px"
>
... content ...
</div> Performance Tracking
@packages/perf collects Resource Timing data for network requests made by
the page and reports each one as a resource_timing event.
Web Vitals (LCP, INP, …) are handled separately by the core tracker (
trackWebVitals→web_vitals_metric), not by this package.
How it works
trackResourceTiming subscribes to the browser PerformanceObserver for resource entries (with buffered: true, so entries that occurred before
the observer started are included). For every matching entry it builds a ResourceTimingData payload and passes it to the callback.
In the bundle it is wired to the tracker, which sends it as resource_timing:
trackResourceTiming(tracker.trackResourceTimingEntry.bind(tracker)); Usage
import { trackResourceTiming } from '@packages/perf';
const stop = trackResourceTiming(
(data) => {
// data: ResourceTimingData
},
{
initiatorTypes: ['fetch', 'xmlhttprequest'],
sampleRate: 1,
}
);
// later
stop(); The returned function disconnects the observer. If PerformanceObserver is
unavailable (or observing throws), a no-op cleanup is returned and nothing is
tracked.
Options
| Option | Default | Description |
|---|---|---|
initiatorTypes | ["fetch", "xmlhttprequest"] | Only entries whose initiatorType is in this list are tracked. |
sampleRate | 1 | Probability (0–1) that any given entry is reported. 1 = every entry, 0.1 = ~10%. |
Filtering
An entry is reported only when all of the following hold:
- Its
initiatorTypeis ininitiatorTypes. - Its URL does not include the self endpoint
/tracker-batch/push(so the tracker’s own batch requests are never tracked). - It passes the
sampleRaterandom check.
Reported data (ResourceTimingData)
| Field | Description |
|---|---|
url | Resource URL (entry.name). |
duration | Total duration, ms (rounded). |
ttfb | Time to first byte: responseStart − requestStart, ms. |
downloadTime | responseEnd − responseStart, ms. |
transferSize | Bytes transferred over the network (entry.transferSize). |
serviceWorkerTime | fetchStart − workerStart, ms — or null if no service worker. |
initiatorType | What initiated the request (fetch, xmlhttprequest, …). |
startTime | When the request started relative to navigation, ms. |
The event sent to the backend:
{
type: "resource_timing",
data: { url, duration, ttfb, downloadTime, transferSize, serviceWorkerTime, initiatorType, startTime },
metadata: {/**/}
} User Identification
The user ID (userId) is automatically captured and sent in the event’s metadata block (as metadata.userId).
This ID is fetched from Smartico. Technically, the tracker reads the value of the __smartico_login_tracker1 key from localStorage, parses it as JSON, and extracts the ext_user_id property (which is expected to be a number).