npm.io
1.50.1 • Published yesterday

@amplitude/session-replay-browser

Licence
MIT
Version
1.50.1
Deps
8
Size
6.6 MB
Vulns
0
Weekly
0
Stars
180


@amplitude/session-replay-browser

Official Session Replay SDK

Installation

This package is published on NPM registry and is available to be installed using npm and yarn.

# npm
npm install @amplitude/session-replay-browser

# yarn
yarn add @amplitude/session-replay-browser

Usage

This SDK provides access to the Amplitude Session Replay product.

This plugin requires that default tracking for sessions is enabled. If default tracking for sessions is not enabled in the config, the plugin will automatically enable it.

1. Import Amplitude packages
  • @amplitude/session-replay-browser
import * as sessionReplay from '@amplitude/session-replay-browser';
2. Initialize session replay collection

The SDK must be configured via the following code. This call kicks off collection of replays for the user.

sessionReplay.init(API_KEY, {
  deviceId: DEVICE_ID,
  sessionId: SESSION_ID,
  sampleRate: 0.5,
});
3. Evaluate targeting (optional)

Any event that occurs within the span of a session replay must be passed to the SDK to evaluate against targeting conditions. This should be done before step 4, getting the event properties. If you are not using the targeting condition logic provided via the Amplitude UI, this step is not required.

const sessionTargetingMatch = sessionReplay.evaluateTargetingAndCapture({ event: {
  event_type: EVENT_NAME,
  time: EVENT_TIMESTAMP,
  event_properties: eventProperties
} });
4. Get session replay event properties

Any event that occurs within the span of a session replay must be tagged with properties that signal to Amplitude to include it in the scope of the replay. The following shows an example of how to use the properties

const sessionReplayProperties = sessionReplay.getSessionReplayProperties();
track(EVENT_NAME, {
  ...eventProperties,
  ...sessionReplayProperties
})
5. Update session id

Any time that the session id for the user changes, the session replay SDK must be notified of that change. Update the session id via the following method:

sessionReplay.setSessionId(UNIX_TIMESTAMP)

You can optionally pass a new device id as a second argument as well:

sessionReplay.setSessionId(UNIX_TIMESTAMP, deviceId)
6. Start and stop recording (optional)

Use stop() to pause capture without tearing down the SDK (session id, config, and event listeners stay in place). Call start() to resume. Sampling, targeting, and opt-out still apply — start() will not record a session that is opted out, not sampled, or excluded by targeting.

// Pause capture, for example on a sensitive screen
sessionReplay.stop()

// Resume capture
sessionReplay.start()

stop() flushes any events already captured. Events while recording is stopped are not tagged with session replay properties.

7. Shutdown (optional)

If at any point you would like to discontinue collection of session replays, for example in a part of your application where you would not like sessions to be collected, you can use the following method to stop collection and remove collection event listeners. After shutdown(), call init() again to restart — start() alone is not enough because listeners have been removed.

sessionReplay.shutdown()
Options
Name Type Required Default Description
deviceId string Yes undefined Sets an identifier for the device running your application.
sessionId number Yes undefined Sets an identifier for the users current session. The value must be in milliseconds since epoch (Unix Timestamp).
sampleRate number No 0 Use this option to control how many sessions will be selected for replay collection. A selected session will be collected for replay, while sessions that are not selected will not.

The number should be a decimal between 0 and 1, ie 0.01, representing the fraction of sessions you would like to have randomly selected for replay collection. Over a large number of sessions, 0.01 would select 1% of those sessions.
optOut boolean No false Sets permission to collect replays for sessions. Setting a value of true prevents Amplitude from collecting session replays.
flushMaxRetries number No 2 Sets the maximum number of retries for failed upload attempts. This is only applicable to retryable errors.
logLevel number No LogLevel.Warn LogLevel.None or LogLevel.Error or LogLevel.Warn or LogLevel.Verbose or LogLevel.Debug. Sets the log level.
loggerProvider Logger No Logger Sets a custom loggerProvider class from the Logger to emit log messages to desired destination.
serverZone string No US EU or US. Sets the Amplitude server zone. Set this to EU for Amplitude projects created in EU data center.
privacyConfig object No undefined Supports advanced masking configs with CSS selectors.
debugMode boolean No false Adds additional debug event property to help debug instrumentation issues (such as mismatching apps). Only recommended for debugging initial setup, and not recommended for production.
configServerUrl string No undefined Specifies the endpoint URL to fetch remote configuration. If provided, it overrides the default server zone configuration.
trackServerUrl string No undefined Specifies the endpoint URL for sending session replay data. If provided, it overrides the default server zone configuration.
shouldInlineStylesheet boolean No true If stylesheets are inlined, the contents of the stylesheet will be stored. During replay, the stored stylesheet will be used instead of attempting to fetch it remotely. This prevents replays from appearing broken due to missing stylesheets. Note: Inlining stylesheets may not work in all cases. If this is undefined stylesheets will be inlined.
inlineImages boolean No false When true, image sources are inlined as data URLs in the rrweb snapshot so replays do not depend on fetching remote image assets at playback time. Increases snapshot payload size. Passed through to rrweb's inlineImages record option.
storeType string No idb Specifies how replay events should be stored. idb uses IndexedDB to persist replay events when all events cannot be sent during capture. memory stores replay events only in memory, meaning events are lost when the page is closed. If IndexedDB is unavailable, the system falls back to memory.
performanceConfig.enabled boolean No true If enabled, event compression will be deferred to occur during the browser's idle periods.
performanceConfig.timeout number No undefined Optional timeout in milliseconds for the requestIdleCallback API. If specified, this value will be used to set a maximum time for the browser to wait before executing the deferred compression task, even if the browser is not idle.
useWebWorker boolean No false If true, the SDK will compress replay events using a web worker. This offloads compression to a separate thread, improving performance on the main thread.
crossOriginIframes.enabled boolean No false Enables cross-origin iframe recording. Must be set to true on both the parent page and each child iframe page. See Cross-Origin Iframe Recording.
crossOriginIframes.coordinateChildren boolean No true When true, the parent SDK automatically sends start/stop signals to child iframes via postMessage. Set to false to manage child recording lifecycle yourself.
handleSendEvents function No undefined Custom transport for replay event uploads. Lets you fully own the outbound HTTP call (e.g. to attach a JWT Authorization header and route through an authenticated proxy) while the SDK keeps batching, retry and serialization. See Custom Transport.
handleFetchConfig function No undefined Custom transport for the remote-config fetch. Same contract as handleSendEvents, for the config GET. See Custom Transport.

Custom Transport (authenticated proxies / JWT)

By default the SDK sends replay events and fetches remote config with its own internal fetch. If your environment requires custom request logic on every outbound call — for example a JWT Authorization header validated by your own proxy before forwarding to Amplitude — provide the handleSendEvents and/or handleFetchConfig callbacks. The SDK hands each callback a fully-formed request and the callback's only job is to execute it and return the Response. The SDK keeps everything else: URL resolution (including trackServerUrl / configServerUrl), batching, retry/backoff, serialization, compression, and error handling.

import * as sessionReplay from '@amplitude/session-replay-browser';
import type { SendEventsRequest, FetchConfigRequest } from '@amplitude/session-replay-browser';

sessionReplay.init(API_KEY, {
  deviceId,
  sessionId,
  // Optional: point at your proxy. The resolved URL is passed into the callbacks.
  trackServerUrl: 'https://my-proxy.example.com/sr-events',
  configServerUrl: 'https://my-proxy.example.com/sr-config',

  handleSendEvents: async ({ url, method, headers, body, keepalive }: SendEventsRequest) => {
    return fetch(url, {
      method,
      // Spread the SDK headers so you don't drop Content-Encoding etc., then add your auth.
      headers: { ...headers, Authorization: `Bearer ${getJwt()}` },
      body,
      keepalive, // forward this so page-exit batches survive unload
    });
  },

  handleFetchConfig: async ({ url, method, headers, signal }: FetchConfigRequest) => {
    return fetch(url, {
      method,
      headers: { ...headers, Authorization: `Bearer ${getJwt()}` },
      signal, // honor the SDK's fetch-timeout abort signal
    });
  },
});

For a cookie-authenticated proxy, omit the Authorization header and add credentials: 'include' instead — the shape is otherwise identical.

Wiring it up for your integration

The two callbacks are the same no matter how you install Session Replay — only where you pass them in changes. The example above is the standalone SDK. If you use the analytics plugin or the unified SDK, drop the same handleSendEvents / handleFetchConfig into your existing init instead:

Plugin — @amplitude/plugin-session-replay-browser (alongside @amplitude/analytics-browser):

import * as amplitude from '@amplitude/analytics-browser';
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser';

amplitude.add(
  sessionReplayPlugin({
    sampleRate: 1,
    trackServerUrl: 'https://my-proxy.example.com/sr-events',
    configServerUrl: 'https://my-proxy.example.com/sr-config',
    handleSendEvents: async ({ url, method, headers, body, keepalive }) =>
      fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, body, keepalive }),
    handleFetchConfig: async ({ url, method, headers, signal }) =>
      fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, signal }),
  }),
);
amplitude.init(API_KEY);

Unified — @amplitude/unified (the callbacks go under the sessionReplay block):

import { initAll } from '@amplitude/unified';

initAll(API_KEY, {
  sessionReplay: {
    sampleRate: 1,
    trackServerUrl: 'https://my-proxy.example.com/sr-events',
    configServerUrl: 'https://my-proxy.example.com/sr-config',
    handleSendEvents: async ({ url, method, headers, body, keepalive }) =>
      fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, body, keepalive }),
    handleFetchConfig: async ({ url, method, headers, signal }) =>
      fetch(url, { method, headers: { ...headers, Authorization: `Bearer ${getJwt()}` }, signal }),
  },
});

Everything else in this section — the contract, the proxy-side requirements, web-worker support — applies identically across all three.

Contract

What the SDK guarantees to your callback:

  • It passes a fully-formed request (resolved URL, default headers, serialized body). Your only job is to execute it and return a Response.
  • It calls the callback once per logical request attempt. Retry/backoff is handled by the SDK around the callback, so do not implement your own retry.
  • Non-2xx responses and thrown errors / rejected promises are surfaced to the SDK's existing retry and logging logic, exactly as the built-in path treats them.

What the SDK requires from your callback:

  • It MUST return a Response (or a Response-like object with ok, status, text()).
  • It MUST NOT change the semantics of the SDK-supplied body. Forward it unchanged — when transport compression is active, body is a gzipped Uint8Array and headers already includes Content-Encoding: gzip, so keep them together (don't drop that header while forwarding the compressed body, or the payload won't decode server-side).
Notes
  • You own auth, not Amplitude. Amplitude never validates your JWT; authentication happens in your proxy, which forwards the (re-authenticated) request to Amplitude.
  • Web Worker mode is supported. When useWebWorker: true, the SDK keeps compressing off the main thread and delegates only the network call back to the main thread to run your callback.
  • Page exit. On unload the SDK normally uses navigator.sendBeacon, which can't carry custom headers. When handleSendEvents is set, the SDK instead routes the final batch through your callback with keepalive: true, so the exit batch is still authenticated.
  • Covers all outbound traffic. When handleSendEvents is set it is used for every outbound Session Replay request: replay event uploads, the page-exit beacon, and interaction/scroll beacons (these otherwise send via navigator.sendBeacon and would not carry your auth). So no Session Replay request leaves the page unauthenticated.
Implementing the proxy side

The callbacks above are only half the story — they get the request out of the browser with your auth attached. The other half is your proxy, which has to check that auth and then forward the request on to Amplitude. You build and run this proxy; Amplitude doesn't provide one, and you don't need us to stand up any new endpoint. Here's everything you need to get it right.

Where to forward to

Your proxy forwards to Amplitude's existing public Session Replay endpoints — the same ones the SDK would have hit directly. Pick the row that matches your project's data region:

What US EU
Replay events (the POST from handleSendEvents) https://api-sr.amplitude.com/sessions/v2/track https://api-sr.eu.amplitude.com/sessions/v2/track
Remote config (the GET from handleFetchConfig) https://sr-client-cfg.amplitude.com/config https://sr-client-cfg.eu.amplitude.com/config

So the round trip is just: browser → your proxy → Amplitude → your proxy → browser. Your proxy sits in the middle, checks the JWT, and relays everything else through.

What your proxy needs to do
  1. Validate your auth, then strip it. Read the JWT (or whatever you attached in the callback), verify it however your security team wants, and reject the request if it's bad. Don't pass your JWT on to Amplitude — Amplitude doesn't know what it is and doesn't need it.

  2. Keep the path and query string exactly as-is. The SDK builds URLs like …/sessions/v2/track?device_id=…&session_id=…&type=replay and …/config/<apiKey>?config_group=browser. Those query params matter — forward them unchanged. Don't drop or rewrite them.

  3. Forward the body untouched, and keep Content-Encoding. The replay body is usually gzipped, and the request carries a Content-Encoding: gzip header to say so. Pass the bytes through exactly as received and keep that header on. If you decompress, re-compress, or drop the header, Amplitude won't be able to read the payload.

  4. Make sure Amplitude can still see your API key. This is the one that trips people up — see below.

The API-key gotcha (read this one)

Amplitude identifies your project from your Amplitude API key, which the SDK puts in the Authorization: Bearer <amplitudeApiKey> header. If your callback does the obvious thing and overwrites that header with your JWT, the API key is gone by the time the request leaves the browser — and Amplitude will reject the forwarded request because it can't tell which project it belongs to.

Two ways to handle it. We recommend the second one — it's simpler and harder to get wrong:

  • Option A — your proxy puts the API key back. Your callback overwrites Authorization with the JWT; your proxy validates the JWT and then swaps Authorization back to Bearer <amplitudeApiKey> before forwarding. Works, but your proxy now has to know your Amplitude API key.

  • Option B (recommended) — send the JWT in a different header. Leave the SDK's Authorization header alone and put your JWT somewhere else, e.g. X-Customer-Auth. Your proxy checks that header, strips it, and forwards everything else (including the untouched Authorization) straight through. Your proxy never has to know or handle the Amplitude API key.

    handleSendEvents: async ({ url, method, headers, body, keepalive }) =>
      fetch(url, {
        method,
        // Don't overwrite Authorization — add the JWT under your own header instead.
        headers: { ...headers, 'X-Customer-Auth': await getJwt() },
        body,
        keepalive,
      }),
Quick sanity check

Once your proxy is up, do a quick end-to-end test: load a page with the SDK pointed at your proxy, interact a bit, and confirm (a) your proxy logs show the JWT arriving and getting validated, and (b) replays actually show up in Amplitude. If replays don't land, it's almost always one of the four points above — most often the API-key header (gotcha above) or a dropped Content-Encoding.

Cross-Origin Iframe Recording

The SDK can capture events inside cross-origin <iframe> elements and merge them into the parent page's session replay.

How it works

Both the parent page and each iframe page must load the SDK with crossOriginIframes.enabled: true. The parent SDK coordinates recording across all child iframes using postMessage signals and rrweb's built-in cross-origin relay.

Parent page (yoursite.com)
┌─────────────────────────────────────────────────────────────────┐
│  sessionReplay.init(API_KEY, { crossOriginIframes: { enabled: true } })
│                                                                   │
│  CrossOriginIframeCoordinator                                     │
│  ┌────────────────────────┐                                       │
│  │ MutationObserver       │  postMessage("start") ──────────────► │
│  │ watches for <iframe>   │                                       │
│  │ additions/removals     │  postMessage("stop")  ──────────────► │
│  └────────────────────────┘                                       │
│                                                                   │
│  rrweb (recordCrossOriginIframes: true)                           │
│  ◄─────────────── child rrweb events relayed via postMessage ──── │
│  Merges child events into parent snapshot stream                  │
└─────────────────────────────────────────────────────────────────┘
         │ postMessage("start" / "stop")
         ▼
Child iframe page (payments.example.com)
┌─────────────────────────────────────────────────────────────────┐
│  sessionReplay.init(API_KEY, { crossOriginIframes: { enabled: true } })
│                                                                   │
│  isInIframe() === true → child mode                               │
│  listenForParentSignals()                                         │
│  ┌──────────────────────────────────┐                            │
│  │ Waits for "start" signal         │                            │
│  │   → initialises rrweb recording  │                            │
│  │ On "stop" signal                 │                            │
│  │   → stops rrweb, flushes events  │                            │
│  └──────────────────────────────────┘                            │
│                                                                   │
│  rrweb events ──► postMessage relay ──► parent rrweb             │
└─────────────────────────────────────────────────────────────────┘

Child events are serialized by the child page's own rrweb instance and relayed to the parent via postMessage. The parent rrweb stream stores them inline, so the replay viewer can reconstruct both frames from a single session.

Setup

Parent page:

sessionReplay.init(API_KEY, {
  deviceId: DEVICE_ID,
  sessionId: SESSION_ID,
  crossOriginIframes: { enabled: true },
});

Child iframe page (must also load the SDK):

sessionReplay.init(API_KEY, {
  deviceId: DEVICE_ID,
  sessionId: SESSION_ID,
  crossOriginIframes: { enabled: true },
});

The child SDK detects it is running inside an iframe and automatically enters child mode — it will wait for a start signal from the parent rather than begin recording immediately.

Options
Name Type Required Default Description
crossOriginIframes.enabled boolean Yes Enables cross-origin iframe recording on both parent and child pages.
crossOriginIframes.coordinateChildren boolean No true When true, the parent SDK sends start/stop signals to child iframes and keeps their recording lifecycle in sync. Set to false to manage child recording yourself.
Privacy

The child page's rrweb instance performs its own DOM serialisation. The parent's privacy config (mask levels, block selectors, etc.) does not automatically apply inside the iframe — configure privacy settings independently on the child page.

Limitations
  • Third-party iframes (e.g. Stripe, Google Maps) cannot be captured. Both the parent and child pages must load the SDK with crossOriginIframes.enabled: true.
  • coordinateChildren: false opts out of the coordinator; in this mode the child SDK will not start recording until you manage its lifecycle directly.

Network Request Capture

The SDK can capture network requests made via fetch and include them as events in the session replay. This is opt-in and disabled by default.

Basic setup

Enable network capture via remote configuration by setting sr_logging_config.network.enabled: true. No code changes are required beyond the standard SDK initialization.

Capturing request and response bodies

Body capture is a separate opt-in within network capture. To enable it, set body.request and/or body.response in the network config:

// This is configured server-side via sr_logging_config, not in the SDK init call.
// The shape of the remote config that enables body capture:
{
  sr_logging_config: {
    network: {
      enabled: true,
      body: {
        request: true,   // capture fetch request bodies
        response: true,  // capture fetch response bodies
        maxBodySizeBytes: 10240, // optional, defaults to 10KB
      }
    }
  }
}
Behavior
  • Request bodies: Captured for string, URLSearchParams, and FormData body types. Blob, ArrayBuffer, and ReadableStream bodies are skipped.
  • Response bodies: Captured after the response is received. Binary content types (image/*, audio/*, video/*, application/octet-stream, font/*) are skipped and the event will have responseBodyStatus: 'skipped_binary'.
  • Truncation: Bodies exceeding maxBodySizeBytes are truncated to fit within the limit (byte-accurate for multi-byte characters). The event will have responseBodyStatus: 'truncated'.
  • Errors: If reading the response body fails, the event will have responseBodyStatus: 'error'.
Network event fields
Field Type Description
url string Request URL
method string HTTP method
status number Response status code
duration number Round-trip time in milliseconds
requestHeaders object Request headers
responseHeaders object Response headers
requestBody string Request body (if body capture enabled)
responseBody string Response body (if body capture enabled)
responseBodyStatus string 'captured', 'truncated', 'skipped_binary', or 'error'
error object Error name and message if the request failed

Releasing a Prerelease

Prereleases are published to npm via the Publish v2.x GitHub Actions workflow. The published package will be tagged with the branch name (e.g. @amplitude/session-replay-browser@SR-2728) so it doesn't affect the latest dist-tag.

Steps
  1. Go to Actions → Publish v2.x in the GitHub repo
  2. Click Run workflow
  3. Set the inputs:
    • Use workflow from: main (required — the workflow enforces this)
    • Release type: prerelease
    • Branch to create pre-release from: your feature branch (e.g. SR-2728)
  4. Click Run workflow

The workflow will:

  • Check out your feature branch
  • Build and run tests
  • Bump the version using conventional commits with your branch name as the preid (e.g. 1.33.0-SR-2728.0)
  • Publish to npm with --tag <branch-name>
Installing a prerelease
npm install @amplitude/session-replay-browser@SR-2728
# or a specific version:
npm install @amplitude/session-replay-browser@1.33.0-SR-2728.0
Notes
  • The --tag value is derived from the branch name with non-alphanumeric characters (except -) stripped, so SR-2728 → tag SR-2728, feature/my-branch → tag featuremy-branch
  • Triggering from a branch other than main will fail the authorization check — always use main in the "Use workflow from" dropdown

Bundle Size Optimization

The Session Replay SDK uses dynamic imports to optimize bundle size and improve initial page load performance. Key modules are loaded on-demand rather than being included in the initial bundle:

  • @amplitude/rrweb-record: The core recording functionality is dynamically imported when sessionReplay.init() is called and capture should begin. In cases where users are not sampled or have opted out, then the Session Replay SDK will not import these dependencies.

This approach ensures that:

  • Your application's initial JavaScript bundle remains as small as possible.
  • Only the necessary Session Replay dependencies are loaded.

The dynamic imports happen asynchronously and won't block your application's initialization. If the imports fail for any reason, the SDK will not initiate capture.

Privacy

By default, the session replay will mask all inputs, meaning the text in inputs will appear in a session replay as asterisks: ***. You may require more specific masking controls based on your use case, so we offer the following controls:

1. Unmask inputs

In your application code, add the class .amp-unmask to any input whose text you'd like to have unmasked in the replay. In the session replay, it will be possible to read the exact text entered into an input with this class, the text will not be converted to asterisks.

2. Mask non-input elements

In your application code, add the class .amp-mask to any non-input element whose text you'd like to have masked from the replay. The text in the element, as well as it's children, will all be converted to asterisks.

3. Block non-text elements

In your application code, add the class .amp-block to any element you would like to have blocked from the collection of the replay. The element will appear in the replay as a placeholder with the same dimensions.

4. Block elements by CSS selectors.

In the SDK initialization code, you can configure the SDK to block elements based on CSS selectors.

sessionReplay.init(AMPLITUDE_API_KEY, {
  sampleRate: 0.01, 
  privacyConfig: {
      blockSelector: ['.ignoreClass', '#ignoreId']
  }
})