# JavaScript tag events

Complete reference of every web event generated by the JavaScript tag.

## Basic event structure

Every event shares the same top-level envelope:

```json
[label Event envelope structure]
{
  "dt": 1772115228563,
  "event": "<event-name>",
  "payload": { ... },
  "session_id": "551790ef-...",
  "anonymous_user_id": "671c8bdc-...",
  "user": { "email": "user@example.com" },
  "fingerprint": ["v1_<hash>"],
  "_app_id": 4153,
  "_ip_addresses": ["94.230.145.34"],
  "contexts": { ... }
}
```

| Field | Type | Notes |
|---|---|---|
| `dt` | Number | Unix timestamp (ms) when the event was captured client-side |
| `event` | String | Event type name (see sections below) |
| `payload` | Object | Event-specific data |
| `session_id` | UUID | Session identifier, rotated after `session_timeout_ms` of inactivity |
| `anonymous_user_id` | UUID | Stable anonymous user identifier persisted in localStorage |
| `user` | Object? | Present only if `betterstack('user', {...})` was called. Contains whatever was passed. |
| `fingerprint` | Array? | Present only if FingerprintJS is configured. Array with one `"v1_<hash>"` string. |
| `_app_id` | Number | Internal app identifier (added server-side) |
| `_ip_addresses` | Array | Client IP addresses (added server-side) |
| `contexts` | Object? | Browser/OS/device info. Present on autocapture events only (not on `memory`, `user-agent-specific-memory`, `session-start`, or custom events). |

### `contexts` object

```json
[label Event context data]
{
  "browser": { "type": "browser", "browser": "Chrome 145", "name": "Chrome", "version": "145.0.0.0" },
  "client_os": { "type": "os", "os": "macOS 10.15.7", "name": "macOS", "version": "10.15.7" },
  "device": { "type": "unknown" }
}
```

### Shared `payload.meta` for autocapture events

All autocapture events such as click, dblclick, contextmenu, tap, input, change, submit, reset, page-load, page-change, page-leave, rage-click include this `meta` block in `payload`:

```json
[label Event metadata]
{
  "title": "Page Title",
  "timestamp": 1772115228563,
  "timezone": -60,
  "url": "https://example.com/page",
  "userAgent": "Mozilla/5.0 ...",
  "referrer": "https://example.com/previous",
  "screen": { "width": 3008, "height": 1692 },
  "window": { "width": 1504, "height": 1528 },
  "devicePixelRatio": 2,
  "language": "en",
  "platform": "MacIntel",
  "isMobile": false,
  "isTouch": false,
  "isBot": false
}
```

## Tracking custom events

Created with the `betterstack('track', '<name>', <data>)` API call.

```json
[label Custom event structure]
{
  "event": "custom-event-name",
  "payload": { "sampleKey": "sampleValue", ... }
}
```

## Events

| Event | Description |
|---|---|---|
| `session-start` | Marks the beginning of a new session. Includes UTM params, referrer, and fingerprint data. |
| `page-load` | Initial full page load (`window.load`). |
| `page-change` | In-app SPA navigation (pushState, replaceState, popstate, hashchange). |
| `page-leave` | Tab close or full navigation away (`beforeunload`). |
| `click` | Left-click on non-touch devices. |
| `dblclick` | Double-click on non-touch devices. |
| `contextmenu` | Right-click (all devices). |
| `tap` | Touch start on touch devices. |
| `rage-click` | 5+ clicks within 8px / 2s — indicates user frustration. |
| `input` | Keystroke or value change in a form element. |
| `change` | Form element value committed (blur after change, or select/checkbox toggle). |
| `submit` | Form submission. |
| `reset` | Form reset. |
| `memory` | JS heap snapshot via `performance.memory` (Chrome-only), polled every 15s. |
| `user-agent-specific-memory` | Detailed memory breakdown via `performance.measureUserAgentSpecificMemory()`, polled every 60s. |
| `<custom>` | Any event sent via `betterstack('track', name, data)`. |

## session-start

First event in a new session, or after session timeout. Fired lazily: queued when the first trackable event occurs, not on page load itself. If fingerprinting is configured, waits for fingerprint resolution up to 500ms before sending.

The session is new when there is no existing `sid` in localStorage, or when the time since last activity exceeds `session_timeout_ms`.

```json
[label session-start event]
{
  "event": "session-start",
  "payload": {
    "utm_source": null,
    "utm_medium": null,
    "utm_campaign": null,
    "utm_term": null,
    "utm_content": null,
    "referrer": "https://google.com/",
    "gclid": null,
    "fbclid": null,
    "msclkid": null,
    "ttclid": null,
    "li_fat_id": null,
    "twclid": null,
    "page_url": "https://example.com/landing",
    "fingerprint_data": { "...": "..." }
  }
}
```

`fingerprint_data` is only present when fingerprinting is enabled; contains raw component data.

## page-load

`window.load` event (fires once per full page load). Enabled when `autocapture_enabled?` and `autoPageview !== false`.

```json
[label page-load event]
{
  "event": "page-load",
  "payload": {
    "event": "page",
    "timestamp": "2026-02-26T14:13:48.563Z",
    "meta": { "...shared meta..." },
    "session": "551790ef-...",
    "details": {
      "type": "page-load",
      "url": "https://example.com/"
    }
  }
}
```

Note: `meta.referrer` is the browser's `document.referrer` (the previous page that linked here).

## page-change

In-app navigation via:

* `history.pushState()` (SPA routers: React Router, Vue Router, Next.js, Turbo)
* `history.replaceState()` (only if URL actually changes)
* `popstate` (browser back/forward)
* `hashchange` (hash-based routing)

Deduplicated: skipped if URL hasn't changed since last capture.

```json
[label page-change event]
{
  "event": "page-change",
  "payload": {
    "event": "page",
    "timestamp": "2026-02-26T14:13:48.589Z",
    "meta": { "...shared meta...", "referrer": "https://example.com/previous-spa-url" },
    "session": "551790ef-...",
    "details": {
      "type": "page-change",
      "url": "https://example.com/new-page"
    }
  }
}
```

Note: `meta.referrer` is the previous SPA URL, not `document.referrer`. The `onEventCapture` callback in `AutocaptureConfigGenerator` moves `details.referrer` → `meta.referrer`.

## page-leave

`window.beforeunload` event: tab close, full navigation away, page refresh.

```json
[label page-leave event]
{
  "event": "page-leave",
  "payload": {
    "event": "page",
    "timestamp": "...",
    "meta": { "...shared meta..." },
    "session": "551790ef-...",
    "details": {
      "type": "page-leave",
      "url": "https://example.com/current-page"
    }
  }
}
```

Special behavior: `page-leave` skips `_ensureSessionStarted()`: it won't create a new session on its own.

## click

`document.click` event on non-touch devices (`!('ontouchstart' in window)`). Enabled when `autocapture_enabled?`.

```json
[label click event]
{
  "event": "click",
  "payload": {
    "event": "click",
    "timestamp": "...",
    "meta": { "...shared meta..." },
    "session": "551790ef-...",
    "target": {
      "selector": "a",
      "attributes": { "href": "https://example.com/about" }
    },
    "details": {
      "type": "click",
      "x": 652,
      "y": 89
    }
  }
}
```

`target.selector` is the element tag name. `target.attributes` contains whitelisted attributes configured in `AutocaptureConfigGenerator`: `value`, `type`, `href`, `src`, `id`, `name`, `placeholder`, `title`, `alt`, `role`.

## dblclick

`document.dblclick` event on non-touch devices.

Same payload structure as `click`, but `details.type` = `"dblclick"` and `payload.event` = `"click"`.

### contextmenu

`document.contextmenu` event, right-click. Fires on all devices. no touch guard.

Same payload structure as `click`, but `details.type` = `"contextmenu"`.

## tap

`document.touchstart` event on touch devices.

Same payload structure as `click`, but `details.type` = `"tap"` and coordinates come from the touch event. Only fires on touch-capable devices.

## rage-click

5+ clicks within 8px radius over 2 seconds. Detected by monitoring all `document.click` events and calculating centroid clustering. **Config:** Enabled when `autocapture_enabled?` AND `rage_clicks_enabled?`.

```json
[label rage-click event]
{
  "event": "rage-click",
  "payload": {
    "event": "rage-click",
    "timestamp": "...",
    "meta": { "...shared meta..." },
    "session": "551790ef-...",
    "details": {
      "type": "rage-click",
      "clickCount": 5,
      "firstClickAt": 1772115228000,
      "lastClickAt": 1772115229500,
      "centroid": { "x": 652, "y": 89 }
    }
  }
}
```

After detection, the click buffer is reset.

## input

`document.input` event: fires on every keystroke/value change in form elements.

```json
[label input event]
{
  "event": "input",
  "payload": {
    "event": "form",
    "timestamp": "...",
    "meta": { "...shared meta..." },
    "session": "551790ef-...",
    "target": {
      "selector": "input",
      "attributes": { "type": "text", "id": "name", "name": "name", "placeholder": "Your name" }
    },
    "details": {
      "type": "input"
    }
  }
}
```

Note: the `value` attribute is captured for `select` elements but the actual typed text in text inputs is NOT captured, only the element attributes are collected via the safelist.

## change

`document.change` event:fires when a form element loses focus after its value changed, or on select/checkbox/radio change.

Same structure as `input`, but `details.type` = `"change"`. For `select` elements, `target.attributes.value` reflects the newly selected value.

## submit

`document.submit` event: form submission.

```json
[label submit event]
{
  "event": "submit",
  "payload": {
    "event": "form",
    "timestamp": "...",
    "meta": { "...shared meta..." },
    "session": "551790ef-...",
    "target": {
      "selector": "form",
      "attributes": { "id": "contact-form", "name": "contact" }
    },
    "details": {
      "type": "submit",
      "action": "https://example.com/api/submit",
      "method": "post",
      "id": "contact-form",
      "name": "contact"
    }
  }
}
```

`details` includes form metadata such as `action`, `method`, `id`, `name` extracted from `target.form`.

## reset

`document.reset` event, when form reset button clicked.

Same structure as `submit`, but `details.type` = `"reset"`.

## memory

Polled every 15 seconds via `setInterval`. Also fires once immediately on init. Uses `performance.memory` (Chrome-only, non-standard).

```json
[label memory event]
{
  "event": "memory",
  "payload": {
    "memory": {
      "jsHeapSizeLimit": 4294967296,
      "totalJSHeapSize": 4078952,
      "usedJSHeapSize": 2979308
    },
    "userAgent": "Mozilla/5.0 ...",
    "meta": { "url": "https://example.com/page" }
  }
}
```

No `contexts` field. Completely different payload structure from autocapture events: no `event`, `timestamp`, `session`, `target`, or `details` fields inside `payload`.

## user-agent-specific-memory

Polled every 60 seconds via `setInterval`. Also fires once immediately on init. Uses `performance.measureUserAgentSpecificMemory()` which requires `crossOriginIsolated` context (COOP/COEP headers).

```json
[label user-agent-specific-memory event]
{
  "event": "user-agent-specific-memory",
  "payload": {
    "bytes": 1234567,
    "breakdown": [
      {
        "bytes": 1000000,
        "attribution": [{ "url": "https://example.com/", "scope": "Window" }],
        "types": ["JavaScript"]
      }
    ],
    "userAgent": "Mozilla/5.0 ...",
    "meta": { "url": "https://example.com/page" }
  }
}
```

No `contexts` field. The `bytes` and `breakdown` fields come directly from the `performance.measureUserAgentSpecificMemory()` API result, spread into the payload.

## Need help integrating the JavaScript tag?

We're here for you! Let us know at [hello@betterstack.com](mailto:hello@betterstack.com) and we'll be happy to help.
