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

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

TypeAttributeFires on
Clickdata-track-event-clickclick (capture, delegated; the nearest matching ancestor is used)
Form submitdata-track-event-form-submitsubmit of the <form> itself
Text inputdata-track-event-text-inputfocusout of an <input> (i.e. when the field loses focus)
Iframe loaddata-track-event-iframe-loadiframe present at load or inserted later — see Iframe Tracking
Scrolldata-track-event-scroll-start, data-track-event-scroll-y-depth, data-track-event-scroll-x-depthscroll — 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 MutationObserver watches 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 allow includes payment is reported as iframeLoad:depositIframeLoaded, with the transaction id parsed from the src in data.value and the full src in data.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.name is required and must be a string. If it is missing or not a string, the event is ignored.
  • detail.data is optional. It is used only when it is an object; anything else (or omitting it) results in an empty data: {}.

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 as scroll-y-depth:event-name)
  • Horizontal Trigger: data-track-event-scroll-x-depth="event-name" (sent as scroll-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):

AttributeUnitLogic / Description
data-track-scroll-y-progress / data-track-scroll-x-progress% preferredVisibility-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-offsetpx preferredDistance-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:

  1. Directly on the scrollable container (overflow: scroll).
  2. 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 (trackWebVitalsweb_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

OptionDefaultDescription
initiatorTypes["fetch", "xmlhttprequest"]Only entries whose initiatorType is in this list are tracked.
sampleRate1Probability (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:

  1. Its initiatorType is in initiatorTypes.
  2. Its URL does not include the self endpoint /tracker-batch/push (so the tracker’s own batch requests are never tracked).
  3. It passes the sampleRate random check.

Reported data (ResourceTimingData)

FieldDescription
urlResource URL (entry.name).
durationTotal duration, ms (rounded).
ttfbTime to first byte: responseStart − requestStart, ms.
downloadTimeresponseEnd − responseStart, ms.
transferSizeBytes transferred over the network (entry.transferSize).
serviceWorkerTimefetchStart − workerStart, ms — or null if no service worker.
initiatorTypeWhat initiated the request (fetch, xmlhttprequest, …).
startTimeWhen 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).