Reference9 min read

JavaScript API

Two ways to send a custom event: call a method on window.mrkr, or annotate an element with a data-mrkr-event attribute and let a click do it for you.

Before you call anything

The install snippet loads the tracker with defer, and there is no stub or command queue waiting to replay your early calls: unlike some analytics snippets, window.mrkr simply does not exist until the deferred script has executed. An inline <script> placed after the snippet in your HTML runs immediately during parsing, before the deferred script has had a chance to run, so calling window.mrkr.track(...) from it will throw.

Guard the call, or call it later

Either check window.mrkr exists before calling it, or make the call from your framework's own ready/mount lifecycle (a DOMContentLoaded listener, a React effect, a router hook) rather than from a plain inline script right after the tag. Both window.mrkr and the legacy alias window.pulsewise point at the same object once it exists.

mrkr.track(name, props?)

ts
mrkr.track(name: string, props?: Record<string, string | number | boolean | object>): void

name is truncated to 64 characters with no other validation. If you call track() with no name, or a falsy one, the call doesn't throw: the server receives an event with no name and records it as a plain pageview instead, which will misattribute the hit rather than surface an error.

props accepts a plain object with up to 8 keys. If you send more, the 8 kept are chosen by sorting all keys alphabetically and taking the first 8, not the order you wrote them. Keys longer than 64 characters are truncated; keys containing anything outside letters, numbers, _, ., :, - are dropped silently. String values truncate to 256 characters; booleans and finite numbers pass through unchanged; null, undefined, NaN, and Infinity are dropped; objects and arrays are JSON-stringified and then also truncated to 256 characters, so a large nested object can be cut off mid-JSON and become unparseable on read. track() returns nothing and does not return a promise: it queues the event, which is flushed with the rest of the batch about a second later, sooner if ten events are already waiting, and immediately when the tab is hidden or closed.

mrkr.revenue(amount, props?)

ts
mrkr.revenue(amount: number, props?: Record<string, string | number | boolean | object>): void

amount is coerced with Number(amount) || 0: a non-numeric value silently becomes 0 rather than throwing. The call sends an event named revenue with amount merged into props.

Don't put `amount` in props

The merge order lets a props.amount key override the value you passed as the first argument. If your props object already has an amount field for some other reason, rename it: otherwise it silently replaces the amount you intended to send.

mrkr.pageview()

ts
mrkr.pageview(): void

Closes out the route you're leaving (flushing its final page_leave with duration, visible and engaged time), starts fresh timers, fires a pageview for the current URL, and re-measures scroll geometry for the new page. The tracker already calls this automatically on history.pushState, history.replaceState, popstate, and a bfcache restore, so most single-page apps never need to call it directly: reach for it only if your router doesn't go through the History API, such as an old-style #/route hash router.

mrkr.sessionId

ts
mrkr.sessionId: string

A string, not a function: the session id read once when the tracker script executed. It is not a live getter: if the session later rotates in a long-lived tab (idle past 30 minutes), this property keeps its original value rather than tracking the new one. Treat it as "the session this page load started under."

Declarative attribute API

html
<button data-mrkr-event="cta_click"
        data-mrkr-prop-variant="hero"
        data-mrkr-prop-plan="pro">
  Start free
</button>
<!-- equivalent to: mrkr.track("cta_click", { variant: "hero", plan: "pro" }) -->

data-mrkr-event sets the event name, capped at 64 characters like track(). Any data-mrkr-prop-<key> attribute becomes a property; camelCase-looking dataset keys are converted back to snake_case, so data-mrkr-prop-plan-tier becomes plan_tier in props. Attribute values are always strings: there's no client-side type coercion, unlike calling track() directly with a number or boolean.

It survives stopPropagation()

The tracker installs a single delegated click listener on the document in the capture phase, which runs before your page's own handlers. An element that calls stopPropagation() in its click handler, routine on SPA links, cannot suppress the declarative event. The same listener handles outbound and download links, so a [data-mrkr-event] element wrapping a link fires both the event you named and the link's own outbound or download event.

Script tag configuration

AttributeDefaultWhat it does
data-siteNoneRequired. Your site id.
data-cookiesabsentOpt-in. "on" enables cookie mode (a one-year first-party _pw_id). Absent = cookieless = no cookie written or read.
data-endpointsame-origin /api/collectOverride the ingest URL.
data-replay-endpointsame-origin /api/replayOverride the replay ingest URL.
data-mask-input"1"Masks all typed input in session replay. Set "0" to disable, not recommended.
data-autocaptureno longer usedRetired in September 2026 when click autocapture was removed. Ignored by the tracker; safe to delete from your snippet.
data-debug"0"Set "1" to log tracker activity to the console.
Note

data-mode, data-replay, and data-replay-sample are read by older snippets but ignored today: replay and its sample rate are controlled entirely from Settings in the dashboard now. Tracking mode is set there too, but switching a site to cookie-based also means adding data-cookies="on" to the tag, because the cookie is written in the visitor's browser before any request reaches us.

See custom events for when to reach for track() versus the declarative attribute, and what Mrkr records automatically for what's already captured without either.