Smart Recording

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 happensOld behaviorSmart recording
User visits, bounces in 3sFull recording sent ❌Discarded automatically βœ…
User browses normally, leavesFull recording sent ❌Buffered & discarded βœ…
User hits a JS errorFull recording sentBuffer flushed + continues recording βœ…
User rage-clicks a broken buttonFull recording sentRage click detected β†’ buffer flushed βœ…
User opens chat for helpFull recording sentChat 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:

  1. The buffered events are flushed as a retroactive batch (you get context before the trigger)
  2. Recording switches to full mode for the rest of the session
  3. 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:

TriggerDescriptionDefault
'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 buffer

Rage 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 sizeApproximate coverageMemory 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:

FieldEffect
enabledKill switch β€” disable all recording
modeOverride client-side mode
samplingRateOverride sampling rate (0–1)
quotaRemainingStop recording when quota hits 0
triggersOverride which triggers are active
alwaysRecordSegmentsUsers matching these rules are always recorded in full
neverRecordSegmentsUsers matching these rules are never recorded
maxEventsPerSessionHard 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

OperatorDescriptionExample
'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 email
  • identifier β€” User's unique ID
  • domain β€” User's domain (extracted from email)
  • name β€” User's display name
  • tags.* β€” 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