@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 withok,status,text()). - It MUST NOT change the semantics of the SDK-supplied body. Forward it unchanged — when transport
compression is active,
bodyis a gzippedUint8Arrayandheadersalready includesContent-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. WhenhandleSendEventsis set, the SDK instead routes the final batch through your callback withkeepalive: true, so the exit batch is still authenticated. - Covers all outbound traffic. When
handleSendEventsis set it is used for every outbound Session Replay request: replay event uploads, the page-exit beacon, and interaction/scroll beacons (these otherwise send vianavigator.sendBeaconand 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
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.
Keep the path and query string exactly as-is. The SDK builds URLs like
…/sessions/v2/track?device_id=…&session_id=…&type=replayand…/config/<apiKey>?config_group=browser. Those query params matter — forward them unchanged. Don't drop or rewrite them.Forward the body untouched, and keep
Content-Encoding. The replay body is usually gzipped, and the request carries aContent-Encoding: gzipheader 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.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
Authorizationwith the JWT; your proxy validates the JWT and then swapsAuthorizationback toBearer <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
Authorizationheader 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 untouchedAuthorization) 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: falseopts 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, andFormDatabody types.Blob,ArrayBuffer, andReadableStreambodies 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 haveresponseBodyStatus: 'skipped_binary'. - Truncation: Bodies exceeding
maxBodySizeBytesare truncated to fit within the limit (byte-accurate for multi-byte characters). The event will haveresponseBodyStatus: '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
- Go to Actions → Publish v2.x in the GitHub repo
- Click Run workflow
- 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)
- Use workflow from:
- 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
--tagvalue is derived from the branch name with non-alphanumeric characters (except-) stripped, soSR-2728→ tagSR-2728,feature/my-branch→ tagfeaturemy-branch - Triggering from a branch other than
mainwill fail the authorization check — always usemainin 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 whensessionReplay.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']
}
})