@@ -24,6 +24,7 @@ import type {
2424 EventBuffer ,
2525 InternalEventContext ,
2626 PopEventContext ,
27+ RecordingEvent ,
2728 RecordingOptions ,
2829 ReplayContainer as ReplayContainerInterface ,
2930 ReplayPluginOptions ,
@@ -42,6 +43,8 @@ import { getHandleRecordingEmit } from './util/handleRecordingEmit';
4243import { isExpired } from './util/isExpired' ;
4344import { isSessionExpired } from './util/isSessionExpired' ;
4445import { sendReplay } from './util/sendReplay' ;
46+ import type { SKIPPED } from './util/throttle' ;
47+ import { throttle , THROTTLED } from './util/throttle' ;
4548
4649/**
4750 * The main replay container class, which holds all the state and methods for recording and sending replays.
@@ -75,6 +78,11 @@ export class ReplayContainer implements ReplayContainerInterface {
7578 maxSessionLife : MAX_SESSION_LIFE ,
7679 } as const ;
7780
81+ private _throttledAddEvent : (
82+ event : RecordingEvent ,
83+ isCheckout ?: boolean ,
84+ ) => typeof THROTTLED | typeof SKIPPED | Promise < AddEventResult | null > ;
85+
7886 /**
7987 * Options to pass to `rrweb.record()`
8088 */
@@ -136,6 +144,14 @@ export class ReplayContainer implements ReplayContainerInterface {
136144 this . _debouncedFlush = debounce ( ( ) => this . _flush ( ) , this . _options . flushMinDelay , {
137145 maxWait : this . _options . flushMaxDelay ,
138146 } ) ;
147+
148+ this . _throttledAddEvent = throttle (
149+ ( event : RecordingEvent , isCheckout ?: boolean ) => addEvent ( this , event , isCheckout ) ,
150+ // Max 300 events...
151+ 300 ,
152+ // ... per 5s
153+ 5 ,
154+ ) ;
139155 }
140156
141157 /** Get the event context. */
@@ -565,6 +581,39 @@ export class ReplayContainer implements ReplayContainerInterface {
565581 this . _context . urls . push ( url ) ;
566582 }
567583
584+ /**
585+ * Add a breadcrumb event, that may be throttled.
586+ * If it was throttled, we add a custom breadcrumb to indicate that.
587+ */
588+ public throttledAddEvent (
589+ event : RecordingEvent ,
590+ isCheckout ?: boolean ,
591+ ) : typeof THROTTLED | typeof SKIPPED | Promise < AddEventResult | null > {
592+ const res = this . _throttledAddEvent ( event , isCheckout ) ;
593+
594+ // If this is THROTTLED, it means we have throttled the event for the first time
595+ // In this case, we want to add a breadcrumb indicating that something was skipped
596+ if ( res === THROTTLED ) {
597+ const breadcrumb = createBreadcrumb ( {
598+ category : 'replay.throttled' ,
599+ } ) ;
600+
601+ this . addUpdate ( ( ) => {
602+ void addEvent ( this , {
603+ type : EventType . Custom ,
604+ timestamp : breadcrumb . timestamp || 0 ,
605+ data : {
606+ tag : 'breadcrumb' ,
607+ payload : breadcrumb ,
608+ metric : true ,
609+ } ,
610+ } ) ;
611+ } ) ;
612+ }
613+
614+ return res ;
615+ }
616+
568617 /**
569618 * Initialize and start all listeners to varying events (DOM,
570619 * Performance Observer, Recording, Sentry SDK, etc)
@@ -803,7 +852,7 @@ export class ReplayContainer implements ReplayContainerInterface {
803852 */
804853 private _createCustomBreadcrumb ( breadcrumb : Breadcrumb ) : void {
805854 this . addUpdate ( ( ) => {
806- void addEvent ( this , {
855+ void this . throttledAddEvent ( {
807856 type : EventType . Custom ,
808857 timestamp : breadcrumb . timestamp || 0 ,
809858 data : {
0 commit comments