Skip to main content

Collectors

Sentinel ships six collectors, each implementing the Collector interface:

interface Collector {
start(): void;
stop(): void;
}

All collectors receive a SignalHandler callback (onSignal) during construction, which they invoke whenever a new signal is captured. Signals are created via the shared createSignal() factory that stamps each with a ULID and the current timestamp.


FetchCollector

Signal type: fetch

Captures outbound HTTP requests that pass through Sentinel's FetchInterceptor.

FieldSource
hostURL.host of the request
urlURL.pathname of the request

How it works

FetchCollector does not instrument window.fetch itself — instead, the FetchInterceptor calls onFetchCall(input) for every intercepted POST request. The collector extracts the host and pathname, then emits a fetch signal.

SSR guard

start() is a no-op when typeof window === 'undefined'.


ClickCollector

Signal type: click

Captures user click/tap interactions.

FieldSource
identifierStable selector for the clicked element
screenCurrent window.location.pathname

Event Listener

Listens on document for pointerdown events (capture phase, passive). Only primary pointer (mouse button 0 or touch) events are processed.

Element Resolution

  1. Walk composedPath() to find the first Element (handles Shadow DOM).
  2. Find the nearest clickable ancestor using element.closest() against:
    • button, a, input, select, textarea
    • [role="button"], [data-testid], [data-test], [data-id]

Identifier Strategy

The collector builds a stable, human-readable identifier for each click target using the following priority:

PriorityStrategyExample
1Stable data attribute (data-testid, data-test, data-id, name)button.search-submit
2Stable id attribute (non-numeric, reasonably short)button.main-cta
3CSS selector path (up to 4 ancestors)div.header>button:nth-of-type(2)
4Tag name onlybutton

Identifiers are cached per-element with a 10-second TTL (WeakMap) and truncated to 200 characters.

Class Name Cleaning

CSS-module hashes (e.g. styles_button_a1b2c) are stripped to their semantic prefix (styles_button). Single/double-letter class names and strings with 5+ consecutive uppercase/digit chars are discarded as unstable.


TrackingCollector

Signal type: tracking

Captures analytics events sent through Traveloka's tracking pipeline.

FieldSource
eventNameevent field from the tracking payload

How it works

Like FetchCollector, this collector is not self-instrumenting. The FetchInterceptor calls onFetchCall(input, init) for intercepted requests. TrackingCollector checks if the URL contains /api/v1/tvlk/events and, if so, parses the JSON body to extract event names from data.events[].event.

Parse errors are silently ignored.


Signal type: navigation

Tracks client-side page navigations.

FieldSource
screenNamewindow.location.pathname

Instrumentation

  1. history.pushState / history.replaceState — monkey-patched to call the original then check for path changes.
  2. popstate event — listener on window for back/forward navigation.

A signal is only emitted when the pathname actually changes (deduplication).

Cleanup

stop() removes the popstate listener. Note: pushState/replaceState patches are not reverted (they are applied once and remain for the page lifetime).


CustomCollector

Signal type: custom

Allows domain code to emit arbitrary signals.

FieldSource
nameCaller-provided event name
payloadCaller-provided key-value map

Public API

// Via the Sentinel instance (window.__SEN)
window.__SEN.collect('checkout_started', { currency: 'IDR' });

collect(name, payload) is a no-op when the collector is inactive.


AppStateCollector

Signal type: appState

Tracks foreground/background transitions of the browser tab.

FieldSource
state'goes_to_foreground' or 'goes_to_background'

Detection Strategy

Browser SupportMechanism
document.visibilityState availablevisibilitychange event
Fallbackfocus / blur events on window

Foreground Callback

When the app returns to foreground, the collector invokes onGoesToForeground(), which triggers reporter.flushIfNeeded(true) — ensuring buffered signals are sent promptly after the user returns.

Deduplication

State changes are deduplicated; the same state emitted consecutively produces only one signal.


Signal Factory

All collectors use the shared createSignal function:

function createSignal<T extends SignalData>(
signalType: SignalType,
signal: T
): Signal<T> {
return {
id: ulid(), // time-sortable unique ID
ts: Date.now(), // creation timestamp (ms)
signalType,
signal,
};
}

The ULID ensures global uniqueness and chronological ordering without coordination.


Last updated: 2026-02-24