Smart Recording
Produck SDK uses a smart recording strategy inspired by PostHog (opens in a new tab) and Amplitude (opens in a new tab). Instead of recording every session in full and sending all data to your server, the SDK intelligently decides what to capture and when to send it.
This dramatically reduces bandwidth, storage costs, and performance impact β while ensuring you always have replay data for the sessions that matter.
How It Works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Session β
β β
β rrweb events β [Circular Buffer (last ~30s)] β
β β β
β ββββββββββββββΌβββββββββββββ β
β βΌ βΌ βΌ β
β No trigger Trigger fires Session ends β
β (discard) (flush buffer) (check duration) β
β β β β
β Send to server <5s? β discard β
β + continue full β₯5s? keep β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββThe Problem with Always-On Recording
Traditional session replay SDKs record every session:
- π΄ High bandwidth β DOM mutations, mouse moves, scrolls sent continuously
- π΄ Storage costs β most sessions are uninteresting (bounces, bots, idle tabs)
- π΄ Performance impact β serializing DOM on every mutation
- π΄ Privacy risk β more data captured = more exposure
The Smart Recording Solution
| What happens | Old behavior | Smart recording |
|---|---|---|
| User visits, bounces in 3s | Full recording sent β | Discarded automatically β |
| User browses normally, leaves | Full recording sent β | Buffered & discarded β |
| User hits a JS error | Full recording sent | Buffer flushed + continues recording β |
| User rage-clicks a broken button | Full recording sent | Rage click detected β buffer flushed β |
| User opens chat for help | Full recording sent | Chat trigger β buffer flushed β |
Result: You only store replays for sessions where something interesting happened, with ~30 seconds of context leading up to the trigger.
Recording Modes
'buffered' (Default β Recommended)
rrweb runs in the browser, but events are stored in a circular buffer in memory (default: last 500 events β 30 seconds). No data is sent to your server until a trigger fires.
When a trigger fires:
- The buffered events are flushed as a retroactive batch (you get context before the trigger)
- Recording switches to full mode for the rest of the session
- All subsequent events are sent normally
If no trigger fires, the events are silently discarded when the user leaves β zero network cost.
<ProduckProvider
config={{
sdkKey: 'your-key',
smartRecording: {
mode: 'buffered',
triggers: ['chat_opened', 'error', 'rage_click', 'conversion'],
},
}}
>'lightweight'
No rrweb replay at all. Only user-flow events (clicks, navigation, form submissions) are tracked. Minimal bandwidth, minimal performance impact.
Use this when you only need tracking data, not session replays.
smartRecording: {
mode: 'lightweight',
}'full'
Classic always-on recording. Every session is recorded and sent in full. This is the old default behavior.
Use this sparingly β for debugging, development, or when you have specific compliance needs.
smartRecording: {
mode: 'full',
}'off'
No recording of any kind. User-flow tracking can still be enabled separately.
smartRecording: {
mode: 'off',
}Triggers
Triggers determine what events cause the buffer to flush and full recording to begin. You can configure which triggers are active:
| Trigger | Description | Default |
|---|---|---|
'chat_opened' | User opens the chat widget | β |
'error' | JavaScript error or unhandled promise rejection | β |
'rage_click' | 3+ rapid clicks in the same area (30px radius, 1s window) | β |
'conversion' | User hits a conversion/goal event | β |
'slow_page' | Page load time exceeds threshold (default: 3s) | β |
'form_abandoned' | User started filling a form but didn't submit | β |
'custom' | Your code explicitly triggers recording | β |
'user_segment' | User matches a server-defined always-record segment | β |
'long_session' | Session exceeds a minimum duration | β |
Custom Triggers
You can trigger recording from your own code:
// When user reaches checkout
sdk.triggerRecording();
// The SDK fires the 'custom' trigger, flushing the bufferRage Click Detection
The SDK automatically detects rage clicks β rapid repeated clicks on the same area, indicating user frustration. Configurable:
smartRecording: {
triggers: ['rage_click'],
rageClickWindow: 1000, // Time window in ms (default: 1000)
rageClickThreshold: 3, // Min clicks to trigger (default: 3)
}Circular Buffer
The circular buffer keeps the last N events in memory using a fixed-size ring buffer. When a trigger fires, you get the context leading up to the interesting moment.
smartRecording: {
bufferSize: 500, // ~30 seconds of typical activity
}| Buffer size | Approximate coverage | Memory usage |
|---|---|---|
| 250 | ~15 seconds | ~100 KB |
| 500 (default) | ~30 seconds | ~200 KB |
| 1000 | ~60 seconds | ~400 KB |
| 2000 | ~2 minutes | ~800 KB |
The buffer overwrites oldest events when full, so memory usage is constant regardless of session length.
Bounce Filtering
Sessions shorter than a minimum duration are automatically discarded. This filters out:
- Users who immediately bounce
- Bots and crawlers
- Accidental page loads
smartRecording: {
minimumSessionDuration: 5000, // 5 seconds (default)
}Set to 0 to disable bounce filtering.
Server-Side Configuration
The SDK can fetch recording configuration from your server, giving you centralized control over recording behavior without deploying SDK changes.
smartRecording: {
fetchServerConfig: true, // Enabled by default
}The SDK calls POST /api/v1/sdk/recording-config on init. Your server can respond with:
{
"enabled": true,
"mode": "buffered",
"samplingRate": 0.5,
"quotaRemaining": 1000,
"triggers": ["chat_opened", "error", "rage_click"],
"minimumSessionDuration": 5000,
"maxEventsPerSession": 10000,
"alwaysRecordSegments": [
{ "field": "tags.plan", "operator": "equals", "value": "enterprise" }
],
"neverRecordSegments": [
{ "field": "email", "operator": "contains", "value": "@internal.com" }
]
}What the server can control:
| Field | Effect |
|---|---|
enabled | Kill switch β disable all recording |
mode | Override client-side mode |
samplingRate | Override sampling rate (0β1) |
quotaRemaining | Stop recording when quota hits 0 |
triggers | Override which triggers are active |
alwaysRecordSegments | Users matching these rules are always recorded in full |
neverRecordSegments | Users matching these rules are never recorded |
maxEventsPerSession | Hard cap on events per session |
If the endpoint returns a non-200 response or is unreachable, the SDK gracefully falls back to client-side config.
User Segment Targeting
Target recording based on user attributes set via sdk.identify():
smartRecording: {
userSegments: {
// Always record enterprise customers in full
alwaysRecord: [
{ field: 'tags.plan', operator: 'equals', value: 'enterprise' },
{ field: 'domain', operator: 'contains', value: 'bigcorp.com' },
],
// Never record internal team
neverRecord: [
{ field: 'email', operator: 'contains', value: '@yourcompany.com' },
],
},
}Segment Rule Operators
| Operator | Description | Example |
|---|---|---|
'equals' | Exact match | { field: 'tags.plan', operator: 'equals', value: 'pro' } |
'contains' | Substring match | { field: 'email', operator: 'contains', value: '@acme.com' } |
'matches' | Regex match | { field: 'identifier', operator: 'matches', value: '^usr_test_' } |
'in' | Value in list | { field: 'tags.role', operator: 'in', value: ['admin', 'owner'] } |
Supported Fields
emailβ User's emailidentifierβ User's unique IDdomainβ User's domain (extracted from email)nameβ User's display nametags.*β Any custom tag (e.g.,tags.plan,tags.role,tags.company)
Max Events Cap
Prevent runaway recording from consuming excessive resources:
smartRecording: {
maxEventsPerSession: 10000, // Default: 10,000 events
}When the cap is reached, recording stops for that session.
Full Configuration Reference
<ProduckProvider
config={{
sdkKey: 'your-key',
smartRecording: {
// Recording mode
mode: 'buffered', // 'buffered' | 'lightweight' | 'full' | 'off'
// What triggers buffer flush
triggers: [
'chat_opened',
'error',
'rage_click',
'conversion',
// 'slow_page',
// 'form_abandoned',
// 'custom',
],
// Circular buffer size (events)
bufferSize: 500, // Default: 500 (~30s)
// Discard sessions shorter than this
minimumSessionDuration: 5000, // Default: 5000ms (5s)
// Hard cap on events per session
maxEventsPerSession: 10000, // Default: 10,000
// Fetch config from server on init
fetchServerConfig: true, // Default: true
// Rage click detection
rageClickWindow: 1000, // Default: 1000ms
rageClickThreshold: 3, // Default: 3 clicks
// Slow page detection threshold
slowPageThreshold: 3000, // Default: 3000ms
// User segment targeting (client-side)
userSegments: {
alwaysRecord: [], // Always record these users in full
neverRecord: [], // Never record these users
},
},
}}
>Migration from Always-On Recording
If you were previously using recording: { enabled: true } (the old default), no action is needed β the SDK now defaults to smartRecording.mode: 'buffered' which is a transparent upgrade.
To opt back into always-on recording:
smartRecording: {
mode: 'full',
}To disable recording entirely:
smartRecording: {
mode: 'off',
}
// or
recording: { enabled: false },Related
- Installation β SDK setup guide
- Core Concepts β How the SDK works
- FAQ β Common questions