@multiplayer-app/session-recorder-common 1.3.31 → 1.3.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"crash-buffer.d.ts","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACxC,cAAc,CAAC,EAAE,GAAG,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,GAAG,CAAA;CACV,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,+BAA+B,GAAG,0BAA0B,EAAE,CAAA;AAE1E,MAAM,MAAM,iCAAiC,GAAG,0BAA0B,CAAA;AAE1E,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAA;AAExD,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB,EAAE,iCAAiC,CAAA;CACzD,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,4BAA4B,EAAE,CAAA;IAC3C,SAAS,EAAE,0BAA0B,EAAE,CAAA;IACvC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,WAAW,CAAC,OAAO,EAAE,4BAA4B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpF,WAAW,CAAC,OAAO,EAAE,+BAA+B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,EAAE,CAAC,CACD,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GACrE,MAAM,IAAI,CAAA;IACb,GAAG,CAAC,CACF,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GACrE,IAAI,CAAA;CACR;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IAChC,iBAAiB,IAAI,OAAO,CAAA;CAC7B"}
1
+ {"version":3,"file":"crash-buffer.d.ts","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACxC,cAAc,CAAC,EAAE,GAAG,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,GAAG,CAAA;CACV,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,+BAA+B,GAAG,0BAA0B,EAAE,CAAA;AAE1E,MAAM,MAAM,iCAAiC,GAAG,0BAA0B,CAAA;AAE1E,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAA;AAExD,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB,EAAE,iCAAiC,CAAA;CACzD,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,4BAA4B,EAAE,CAAA;IACtC,KAAK,EAAE,0BAA0B,EAAE,CAAA;CACpC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,4BAA4B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpF,WAAW,CAAC,OAAO,EAAE,+BAA+B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,EAAE,CAAC,CAAC,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACpH,GAAG,CAAC,CAAC,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GAAG,IAAI,CAAA;CAChH;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IAChC,iBAAiB,IAAI,OAAO,CAAA;CAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"crash-buffer.js","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"","sourcesContent":["export type CrashBufferAttrs = {\n sessionAttributes?: Record<string, any>\n resourceAttributes?: Record<string, any>\n userAttributes?: any\n}\n\nexport type CrashBufferRrwebEventPayload = {\n ts: number\n isFullSnapshot?: boolean\n event: any\n}\n\nexport type CrashBufferOtelSpanPayload = {\n ts: number\n span: any\n}\n\n/**\n * Batch append payload for OTEL spans.\n * This is intentionally the same per-span shape as `CrashBufferOtelSpanPayload`,\n * just provided as an array to allow implementations to persist efficiently.\n */\nexport type CrashBufferOtelSpanBatchPayload = CrashBufferOtelSpanPayload[]\n\nexport type CrashBufferErrorSpanAppendedEvent = CrashBufferOtelSpanPayload\n\nexport type CrashBufferEventName = 'error-span-appended'\n\nexport type CrashBufferEventMap = {\n 'error-span-appended': CrashBufferErrorSpanAppendedEvent\n}\n\nexport type CrashBufferSnapshot = {\n rrwebEvents: CrashBufferRrwebEventPayload[]\n otelSpans: CrashBufferOtelSpanPayload[]\n attrs: CrashBufferAttrs | null\n windowMs: number\n fromTs: number\n toTs: number\n}\n\n/**\n * Shared CrashBuffer contract used across browser + react-native implementations.\n *\n * Notes:\n * - `windowMs` is optional because browser implementations usually bake the window into the instance,\n * while React Native typically passes it per call.\n * - `pruneOlderThan` is optional because browser implementations can handle pruning internally.\n */\nexport interface CrashBuffer {\n setAttrs(attrs: CrashBufferAttrs): Promise<void>\n appendEvent(payload: CrashBufferRrwebEventPayload, windowMs?: number): Promise<void>\n appendSpans(payload: CrashBufferOtelSpanBatchPayload, windowMs?: number): Promise<void>\n snapshot(windowMs?: number, now?: number): Promise<CrashBufferSnapshot>\n clear(): Promise<void>\n pruneOlderThan?(cutoffTs: number): Promise<void>\n on?(\n event: CrashBufferEventName,\n listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void\n ): () => void\n off?(\n event: CrashBufferEventName,\n listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void\n ): void\n}\n\n/**\n * Optional lifecycle controls supported by some CrashBuffer implementations (e.g. browser tabs).\n */\nexport interface CrashBufferLifecycle extends CrashBuffer {\n setActive(active: boolean): void\n needsFullSnapshot(): boolean\n}\n"]}
1
+ {"version":3,"file":"crash-buffer.js","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"","sourcesContent":["export type CrashBufferAttrs = {\n sessionAttributes?: Record<string, any>\n resourceAttributes?: Record<string, any>\n userAttributes?: any\n}\n\nexport type CrashBufferRrwebEventPayload = {\n ts: number\n isFullSnapshot?: boolean\n event: any\n}\n\nexport type CrashBufferOtelSpanPayload = {\n ts: number\n span: any\n}\n\n/**\n * Batch append payload for OTEL spans.\n * This is intentionally the same per-span shape as `CrashBufferOtelSpanPayload`,\n * just provided as an array to allow implementations to persist efficiently.\n */\nexport type CrashBufferOtelSpanBatchPayload = CrashBufferOtelSpanPayload[]\n\nexport type CrashBufferErrorSpanAppendedEvent = CrashBufferOtelSpanPayload\n\nexport type CrashBufferEventName = 'error-span-appended'\n\nexport type CrashBufferEventMap = {\n 'error-span-appended': CrashBufferErrorSpanAppendedEvent\n}\n\nexport type CrashBufferSnapshot = {\n startedAt: number\n stoppedAt: number\n events: CrashBufferRrwebEventPayload[]\n spans: CrashBufferOtelSpanPayload[]\n}\n\n/**\n * Shared CrashBuffer contract used across browser + react-native implementations.\n *\n * Notes:\n * - `windowMs` is optional because browser implementations usually bake the window into the instance,\n * while React Native typically passes it per call.\n * - `pruneOlderThan` is optional because browser implementations can handle pruning internally.\n */\nexport interface CrashBuffer {\n appendEvent(payload: CrashBufferRrwebEventPayload, windowMs?: number): Promise<void>\n appendSpans(payload: CrashBufferOtelSpanBatchPayload, windowMs?: number): Promise<void>\n snapshot(windowMs?: number, now?: number): Promise<CrashBufferSnapshot>\n clear(): Promise<void>\n pruneOlderThan?(cutoffTs: number): Promise<void>\n on?(event: CrashBufferEventName, listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void): () => void\n off?(event: CrashBufferEventName, listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void): void\n}\n\n/**\n * Optional lifecycle controls supported by some CrashBuffer implementations (e.g. browser tabs).\n */\nexport interface CrashBufferLifecycle extends CrashBuffer {\n setActive(active: boolean): void\n needsFullSnapshot(): boolean\n}\n"]}
@@ -4,4 +4,5 @@
4
4
  * @returns {void}
5
5
  */
6
6
  export declare const captureException: (error: Error, errorInfo?: Record<string, any>) => void;
7
+ export declare const shouldCaptureException: (error: Error, _errorInfo?: Record<string, any>) => boolean;
7
8
  //# sourceMappingURL=capture-exception.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"capture-exception.d.ts","sourceRoot":"","sources":["../../../src/sdk/capture-exception.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,KAAK,EACZ,YAAY,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,SAgDhC,CAAA"}
1
+ {"version":3,"file":"capture-exception.d.ts","sourceRoot":"","sources":["../../../src/sdk/capture-exception.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,KAAK,EAAE,YAAY,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,SA4C7E,CAAA;AAUD,eAAO,MAAM,sBAAsB,GAAI,OAAO,KAAK,EAAE,aAAa,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,OAkCvF,CAAA"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.captureException = void 0;
3
+ exports.shouldCaptureException = exports.captureException = void 0;
4
4
  const api_1 = require("@opentelemetry/api");
5
5
  const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
6
6
  const set_resource_attributes_1 = require("./set-resource-attributes");
@@ -10,7 +10,7 @@ const set_resource_attributes_1 = require("./set-resource-attributes");
10
10
  * @returns {void}
11
11
  */
12
12
  const captureException = (error, errorInfo) => {
13
- if (!error) {
13
+ if (!error || !(0, exports.shouldCaptureException)(error)) {
14
14
  return;
15
15
  }
16
16
  const activeContext = api_1.context.active();
@@ -18,7 +18,7 @@ const captureException = (error, errorInfo) => {
18
18
  let isNewSpan = false;
19
19
  if (!span || !span.isRecording()) {
20
20
  span = api_1.trace.getTracer('exception').startSpan(error.name || 'Error', {
21
- attributes: Object.assign({ [semantic_conventions_1.ATTR_EXCEPTION_MESSAGE]: error.message, [semantic_conventions_1.ATTR_EXCEPTION_STACKTRACE]: error.stack, [semantic_conventions_1.ATTR_EXCEPTION_TYPE]: error.name }, (0, set_resource_attributes_1.getResourceAttributes)()),
21
+ attributes: Object.assign({ [semantic_conventions_1.ATTR_EXCEPTION_MESSAGE]: error.message, [semantic_conventions_1.ATTR_EXCEPTION_STACKTRACE]: error.stack, [semantic_conventions_1.ATTR_EXCEPTION_TYPE]: error.name }, (0, set_resource_attributes_1.getResourceAttributes)())
22
22
  });
23
23
  api_1.trace.setSpan(activeContext, span);
24
24
  isNewSpan = true;
@@ -27,7 +27,7 @@ const captureException = (error, errorInfo) => {
27
27
  span.setAttributes({
28
28
  [semantic_conventions_1.ATTR_EXCEPTION_MESSAGE]: error.message,
29
29
  [semantic_conventions_1.ATTR_EXCEPTION_STACKTRACE]: error.stack,
30
- [semantic_conventions_1.ATTR_EXCEPTION_TYPE]: error.name,
30
+ [semantic_conventions_1.ATTR_EXCEPTION_TYPE]: error.name
31
31
  });
32
32
  }
33
33
  if (errorInfo) {
@@ -38,11 +38,46 @@ const captureException = (error, errorInfo) => {
38
38
  span.recordException(error);
39
39
  span.setStatus({
40
40
  code: api_1.SpanStatusCode.ERROR,
41
- message: error.message,
41
+ message: error.message
42
42
  });
43
43
  if (isNewSpan) {
44
44
  span.end();
45
45
  }
46
46
  };
47
47
  exports.captureException = captureException;
48
+ /**
49
+ * Best-effort deduplication of exceptions that fire multiple times
50
+ * (e.g. framework handler + global handlers) within a short time window.
51
+ */
52
+ const exceptionDedupeWindowMs = 2000;
53
+ const recentExceptionFingerprints = new Map();
54
+ const shouldCaptureException = (error, _errorInfo) => {
55
+ if (!error)
56
+ return false;
57
+ const now = Date.now();
58
+ // Build a fingerprint that is stable enough across repeated emissions
59
+ // but not so broad that different errors collapse into one.
60
+ const keyParts = [];
61
+ keyParts.push(error.name || 'Error');
62
+ keyParts.push(error.message || '');
63
+ // First stack line tends to include file/line where it originated.
64
+ if (typeof error.stack === 'string') {
65
+ const firstFrame = error.stack.split('\n')[1] || '';
66
+ keyParts.push(firstFrame.trim());
67
+ }
68
+ const fingerprint = keyParts.join('|').slice(0, 500);
69
+ const lastSeen = recentExceptionFingerprints.get(fingerprint);
70
+ if (lastSeen && now - lastSeen < exceptionDedupeWindowMs) {
71
+ return false;
72
+ }
73
+ recentExceptionFingerprints.set(fingerprint, now);
74
+ // Cheap cleanup of old entries to avoid unbounded growth.
75
+ for (const [key, ts] of recentExceptionFingerprints) {
76
+ if (now - ts > exceptionDedupeWindowMs * 5) {
77
+ recentExceptionFingerprints.delete(key);
78
+ }
79
+ }
80
+ return true;
81
+ };
82
+ exports.shouldCaptureException = shouldCaptureException;
48
83
  //# sourceMappingURL=capture-exception.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"capture-exception.js","sourceRoot":"","sources":["../../../src/sdk/capture-exception.ts"],"names":[],"mappings":";;;AAAA,4CAAmE;AACnE,8EAI4C;AAC5C,uEAAiE;AAEjE;;;;GAIG;AACI,MAAM,gBAAgB,GAAG,CAC9B,KAAY,EACZ,SAA+B,EAC/B,EAAE;IACF,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAM;IACR,CAAC;IAED,MAAM,aAAa,GAAG,aAAO,CAAC,MAAM,EAAE,CAAA;IAEtC,IAAI,IAAI,GAAG,WAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;IACvC,IAAI,SAAS,GAAG,KAAK,CAAA;IAErB,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACjC,IAAI,GAAG,WAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,SAAS,CAC3C,KAAK,CAAC,IAAI,IAAI,OAAO,EACrB;YACE,UAAU,kBACR,CAAC,6CAAsB,CAAC,EAAE,KAAK,CAAC,OAAO,EACvC,CAAC,gDAAyB,CAAC,EAAE,KAAK,CAAC,KAAK,EACxC,CAAC,0CAAmB,CAAC,EAAE,KAAK,CAAC,IAAI,IAC9B,IAAA,+CAAqB,GAAE,CAC3B;SACF,CACF,CAAA;QACD,WAAK,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,CAAA;QAClC,SAAS,GAAG,IAAI,CAAA;IAClB,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,aAAa,CAAC;YACjB,CAAC,6CAAsB,CAAC,EAAE,KAAK,CAAC,OAAO;YACvC,CAAC,gDAAyB,CAAC,EAAE,KAAK,CAAC,KAAK;YACxC,CAAC,0CAAmB,CAAC,EAAE,KAAK,CAAC,IAAI;SAClC,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YACjD,IAAI,CAAC,YAAY,CAAC,cAAc,GAAG,EAAE,EAAE,KAAK,CAAC,CAAA;QAC/C,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC3B,IAAI,CAAC,SAAS,CAAC;QACb,IAAI,EAAE,oBAAc,CAAC,KAAK;QAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;KACvB,CAAC,CAAA;IAEF,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC,GAAG,EAAE,CAAA;IACZ,CAAC;AACH,CAAC,CAAA;AAlDY,QAAA,gBAAgB,oBAkD5B","sourcesContent":["import { context, trace, SpanStatusCode } from '@opentelemetry/api'\nimport {\n ATTR_EXCEPTION_MESSAGE,\n ATTR_EXCEPTION_STACKTRACE,\n ATTR_EXCEPTION_TYPE,\n} from '@opentelemetry/semantic-conventions'\nimport { getResourceAttributes } from './set-resource-attributes'\n\n/**\n * @description Add error to current span\n * @param {Error} error\n * @returns {void}\n */\nexport const captureException = (\n error: Error,\n errorInfo?: Record<string, any>,\n) => {\n if (!error) {\n return\n }\n\n const activeContext = context.active()\n\n let span = trace.getSpan(activeContext)\n let isNewSpan = false\n\n if (!span || !span.isRecording()) {\n span = trace.getTracer('exception').startSpan(\n error.name || 'Error',\n {\n attributes: {\n [ATTR_EXCEPTION_MESSAGE]: error.message,\n [ATTR_EXCEPTION_STACKTRACE]: error.stack,\n [ATTR_EXCEPTION_TYPE]: error.name,\n ...getResourceAttributes(),\n },\n },\n )\n trace.setSpan(activeContext, span)\n isNewSpan = true\n } else {\n span.setAttributes({\n [ATTR_EXCEPTION_MESSAGE]: error.message,\n [ATTR_EXCEPTION_STACKTRACE]: error.stack,\n [ATTR_EXCEPTION_TYPE]: error.name,\n })\n }\n\n if (errorInfo) {\n Object.entries(errorInfo).forEach(([key, value]) => {\n span.setAttribute(`error_info.${key}`, value)\n })\n }\n\n span.recordException(error)\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message,\n })\n\n if (isNewSpan) {\n span.end()\n }\n}\n"]}
1
+ {"version":3,"file":"capture-exception.js","sourceRoot":"","sources":["../../../src/sdk/capture-exception.ts"],"names":[],"mappings":"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","sourcesContent":["import { context, trace, SpanStatusCode } from '@opentelemetry/api'\nimport {\n ATTR_EXCEPTION_MESSAGE,\n ATTR_EXCEPTION_STACKTRACE,\n ATTR_EXCEPTION_TYPE\n} from '@opentelemetry/semantic-conventions'\nimport { getResourceAttributes } from './set-resource-attributes'\n\n/**\n * @description Add error to current span\n * @param {Error} error\n * @returns {void}\n */\nexport const captureException = (error: Error, errorInfo?: Record<string, any>) => {\n if (!error || !shouldCaptureException(error)) {\n return\n }\n\n const activeContext = context.active()\n\n let span = trace.getSpan(activeContext)\n let isNewSpan = false\n\n if (!span || !span.isRecording()) {\n span = trace.getTracer('exception').startSpan(error.name || 'Error', {\n attributes: {\n [ATTR_EXCEPTION_MESSAGE]: error.message,\n [ATTR_EXCEPTION_STACKTRACE]: error.stack,\n [ATTR_EXCEPTION_TYPE]: error.name,\n ...getResourceAttributes()\n }\n })\n trace.setSpan(activeContext, span)\n isNewSpan = true\n } else {\n span.setAttributes({\n [ATTR_EXCEPTION_MESSAGE]: error.message,\n [ATTR_EXCEPTION_STACKTRACE]: error.stack,\n [ATTR_EXCEPTION_TYPE]: error.name\n })\n }\n\n if (errorInfo) {\n Object.entries(errorInfo).forEach(([key, value]) => {\n span.setAttribute(`error_info.${key}`, value)\n })\n }\n\n span.recordException(error)\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message\n })\n\n if (isNewSpan) {\n span.end()\n }\n}\n\n/**\n * Best-effort deduplication of exceptions that fire multiple times\n * (e.g. framework handler + global handlers) within a short time window.\n */\n\nconst exceptionDedupeWindowMs = 2000\nconst recentExceptionFingerprints = new Map<string, number>()\n\nexport const shouldCaptureException = (error: Error, _errorInfo?: Record<string, any>): boolean => {\n if (!error) return false\n\n const now = Date.now()\n\n // Build a fingerprint that is stable enough across repeated emissions\n // but not so broad that different errors collapse into one.\n const keyParts: string[] = []\n keyParts.push(error.name || 'Error')\n keyParts.push(error.message || '')\n\n // First stack line tends to include file/line where it originated.\n if (typeof error.stack === 'string') {\n const firstFrame = error.stack.split('\\n')[1] || ''\n keyParts.push(firstFrame.trim())\n }\n\n const fingerprint = keyParts.join('|').slice(0, 500)\n\n const lastSeen = recentExceptionFingerprints.get(fingerprint)\n if (lastSeen && now - lastSeen < exceptionDedupeWindowMs) {\n return false\n }\n\n recentExceptionFingerprints.set(fingerprint, now)\n\n // Cheap cleanup of old entries to avoid unbounded growth.\n for (const [key, ts] of recentExceptionFingerprints) {\n if (now - ts > exceptionDedupeWindowMs * 5) {\n recentExceptionFingerprints.delete(key)\n }\n }\n\n return true\n}\n"]}
@@ -24,12 +24,10 @@ export type CrashBufferEventMap = {
24
24
  'error-span-appended': CrashBufferErrorSpanAppendedEvent;
25
25
  };
26
26
  export type CrashBufferSnapshot = {
27
- rrwebEvents: CrashBufferRrwebEventPayload[];
28
- otelSpans: CrashBufferOtelSpanPayload[];
29
- attrs: CrashBufferAttrs | null;
30
- windowMs: number;
31
- fromTs: number;
32
- toTs: number;
27
+ startedAt: number;
28
+ stoppedAt: number;
29
+ events: CrashBufferRrwebEventPayload[];
30
+ spans: CrashBufferOtelSpanPayload[];
33
31
  };
34
32
  /**
35
33
  * Shared CrashBuffer contract used across browser + react-native implementations.
@@ -40,7 +38,6 @@ export type CrashBufferSnapshot = {
40
38
  * - `pruneOlderThan` is optional because browser implementations can handle pruning internally.
41
39
  */
42
40
  export interface CrashBuffer {
43
- setAttrs(attrs: CrashBufferAttrs): Promise<void>;
44
41
  appendEvent(payload: CrashBufferRrwebEventPayload, windowMs?: number): Promise<void>;
45
42
  appendSpans(payload: CrashBufferOtelSpanBatchPayload, windowMs?: number): Promise<void>;
46
43
  snapshot(windowMs?: number, now?: number): Promise<CrashBufferSnapshot>;
@@ -1 +1 @@
1
- {"version":3,"file":"crash-buffer.d.ts","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACxC,cAAc,CAAC,EAAE,GAAG,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,GAAG,CAAA;CACV,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,+BAA+B,GAAG,0BAA0B,EAAE,CAAA;AAE1E,MAAM,MAAM,iCAAiC,GAAG,0BAA0B,CAAA;AAE1E,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAA;AAExD,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB,EAAE,iCAAiC,CAAA;CACzD,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,4BAA4B,EAAE,CAAA;IAC3C,SAAS,EAAE,0BAA0B,EAAE,CAAA;IACvC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,WAAW,CAAC,OAAO,EAAE,4BAA4B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpF,WAAW,CAAC,OAAO,EAAE,+BAA+B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,EAAE,CAAC,CACD,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GACrE,MAAM,IAAI,CAAA;IACb,GAAG,CAAC,CACF,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GACrE,IAAI,CAAA;CACR;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IAChC,iBAAiB,IAAI,OAAO,CAAA;CAC7B"}
1
+ {"version":3,"file":"crash-buffer.d.ts","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACxC,cAAc,CAAC,EAAE,GAAG,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,GAAG,CAAA;CACV,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,+BAA+B,GAAG,0BAA0B,EAAE,CAAA;AAE1E,MAAM,MAAM,iCAAiC,GAAG,0BAA0B,CAAA;AAE1E,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAA;AAExD,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB,EAAE,iCAAiC,CAAA;CACzD,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,4BAA4B,EAAE,CAAA;IACtC,KAAK,EAAE,0BAA0B,EAAE,CAAA;CACpC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,4BAA4B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpF,WAAW,CAAC,OAAO,EAAE,+BAA+B,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,EAAE,CAAC,CAAC,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACpH,GAAG,CAAC,CAAC,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC,KAAK,IAAI,GAAG,IAAI,CAAA;CAChH;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IAChC,iBAAiB,IAAI,OAAO,CAAA;CAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"crash-buffer.js","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"","sourcesContent":["export type CrashBufferAttrs = {\n sessionAttributes?: Record<string, any>\n resourceAttributes?: Record<string, any>\n userAttributes?: any\n}\n\nexport type CrashBufferRrwebEventPayload = {\n ts: number\n isFullSnapshot?: boolean\n event: any\n}\n\nexport type CrashBufferOtelSpanPayload = {\n ts: number\n span: any\n}\n\n/**\n * Batch append payload for OTEL spans.\n * This is intentionally the same per-span shape as `CrashBufferOtelSpanPayload`,\n * just provided as an array to allow implementations to persist efficiently.\n */\nexport type CrashBufferOtelSpanBatchPayload = CrashBufferOtelSpanPayload[]\n\nexport type CrashBufferErrorSpanAppendedEvent = CrashBufferOtelSpanPayload\n\nexport type CrashBufferEventName = 'error-span-appended'\n\nexport type CrashBufferEventMap = {\n 'error-span-appended': CrashBufferErrorSpanAppendedEvent\n}\n\nexport type CrashBufferSnapshot = {\n rrwebEvents: CrashBufferRrwebEventPayload[]\n otelSpans: CrashBufferOtelSpanPayload[]\n attrs: CrashBufferAttrs | null\n windowMs: number\n fromTs: number\n toTs: number\n}\n\n/**\n * Shared CrashBuffer contract used across browser + react-native implementations.\n *\n * Notes:\n * - `windowMs` is optional because browser implementations usually bake the window into the instance,\n * while React Native typically passes it per call.\n * - `pruneOlderThan` is optional because browser implementations can handle pruning internally.\n */\nexport interface CrashBuffer {\n setAttrs(attrs: CrashBufferAttrs): Promise<void>\n appendEvent(payload: CrashBufferRrwebEventPayload, windowMs?: number): Promise<void>\n appendSpans(payload: CrashBufferOtelSpanBatchPayload, windowMs?: number): Promise<void>\n snapshot(windowMs?: number, now?: number): Promise<CrashBufferSnapshot>\n clear(): Promise<void>\n pruneOlderThan?(cutoffTs: number): Promise<void>\n on?(\n event: CrashBufferEventName,\n listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void\n ): () => void\n off?(\n event: CrashBufferEventName,\n listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void\n ): void\n}\n\n/**\n * Optional lifecycle controls supported by some CrashBuffer implementations (e.g. browser tabs).\n */\nexport interface CrashBufferLifecycle extends CrashBuffer {\n setActive(active: boolean): void\n needsFullSnapshot(): boolean\n}\n"]}
1
+ {"version":3,"file":"crash-buffer.js","sourceRoot":"","sources":["../../../src/type/crash-buffer.ts"],"names":[],"mappings":"","sourcesContent":["export type CrashBufferAttrs = {\n sessionAttributes?: Record<string, any>\n resourceAttributes?: Record<string, any>\n userAttributes?: any\n}\n\nexport type CrashBufferRrwebEventPayload = {\n ts: number\n isFullSnapshot?: boolean\n event: any\n}\n\nexport type CrashBufferOtelSpanPayload = {\n ts: number\n span: any\n}\n\n/**\n * Batch append payload for OTEL spans.\n * This is intentionally the same per-span shape as `CrashBufferOtelSpanPayload`,\n * just provided as an array to allow implementations to persist efficiently.\n */\nexport type CrashBufferOtelSpanBatchPayload = CrashBufferOtelSpanPayload[]\n\nexport type CrashBufferErrorSpanAppendedEvent = CrashBufferOtelSpanPayload\n\nexport type CrashBufferEventName = 'error-span-appended'\n\nexport type CrashBufferEventMap = {\n 'error-span-appended': CrashBufferErrorSpanAppendedEvent\n}\n\nexport type CrashBufferSnapshot = {\n startedAt: number\n stoppedAt: number\n events: CrashBufferRrwebEventPayload[]\n spans: CrashBufferOtelSpanPayload[]\n}\n\n/**\n * Shared CrashBuffer contract used across browser + react-native implementations.\n *\n * Notes:\n * - `windowMs` is optional because browser implementations usually bake the window into the instance,\n * while React Native typically passes it per call.\n * - `pruneOlderThan` is optional because browser implementations can handle pruning internally.\n */\nexport interface CrashBuffer {\n appendEvent(payload: CrashBufferRrwebEventPayload, windowMs?: number): Promise<void>\n appendSpans(payload: CrashBufferOtelSpanBatchPayload, windowMs?: number): Promise<void>\n snapshot(windowMs?: number, now?: number): Promise<CrashBufferSnapshot>\n clear(): Promise<void>\n pruneOlderThan?(cutoffTs: number): Promise<void>\n on?(event: CrashBufferEventName, listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void): () => void\n off?(event: CrashBufferEventName, listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void): void\n}\n\n/**\n * Optional lifecycle controls supported by some CrashBuffer implementations (e.g. browser tabs).\n */\nexport interface CrashBufferLifecycle extends CrashBuffer {\n setActive(active: boolean): void\n needsFullSnapshot(): boolean\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplayer-app/session-recorder-common",
3
- "version": "1.3.31",
3
+ "version": "1.3.33",
4
4
  "description": "Multiplayer Fullstack Session Recorder - opentelemetry",
5
5
  "author": {
6
6
  "name": "Multiplayer Software, Inc.",
@@ -2,7 +2,7 @@ import { context, trace, SpanStatusCode } from '@opentelemetry/api'
2
2
  import {
3
3
  ATTR_EXCEPTION_MESSAGE,
4
4
  ATTR_EXCEPTION_STACKTRACE,
5
- ATTR_EXCEPTION_TYPE,
5
+ ATTR_EXCEPTION_TYPE
6
6
  } from '@opentelemetry/semantic-conventions'
7
7
  import { getResourceAttributes } from './set-resource-attributes'
8
8
 
@@ -11,11 +11,8 @@ import { getResourceAttributes } from './set-resource-attributes'
11
11
  * @param {Error} error
12
12
  * @returns {void}
13
13
  */
14
- export const captureException = (
15
- error: Error,
16
- errorInfo?: Record<string, any>,
17
- ) => {
18
- if (!error) {
14
+ export const captureException = (error: Error, errorInfo?: Record<string, any>) => {
15
+ if (!error || !shouldCaptureException(error)) {
19
16
  return
20
17
  }
21
18
 
@@ -25,24 +22,21 @@ export const captureException = (
25
22
  let isNewSpan = false
26
23
 
27
24
  if (!span || !span.isRecording()) {
28
- span = trace.getTracer('exception').startSpan(
29
- error.name || 'Error',
30
- {
31
- attributes: {
32
- [ATTR_EXCEPTION_MESSAGE]: error.message,
33
- [ATTR_EXCEPTION_STACKTRACE]: error.stack,
34
- [ATTR_EXCEPTION_TYPE]: error.name,
35
- ...getResourceAttributes(),
36
- },
37
- },
38
- )
25
+ span = trace.getTracer('exception').startSpan(error.name || 'Error', {
26
+ attributes: {
27
+ [ATTR_EXCEPTION_MESSAGE]: error.message,
28
+ [ATTR_EXCEPTION_STACKTRACE]: error.stack,
29
+ [ATTR_EXCEPTION_TYPE]: error.name,
30
+ ...getResourceAttributes()
31
+ }
32
+ })
39
33
  trace.setSpan(activeContext, span)
40
34
  isNewSpan = true
41
35
  } else {
42
36
  span.setAttributes({
43
37
  [ATTR_EXCEPTION_MESSAGE]: error.message,
44
38
  [ATTR_EXCEPTION_STACKTRACE]: error.stack,
45
- [ATTR_EXCEPTION_TYPE]: error.name,
39
+ [ATTR_EXCEPTION_TYPE]: error.name
46
40
  })
47
41
  }
48
42
 
@@ -55,10 +49,54 @@ export const captureException = (
55
49
  span.recordException(error)
56
50
  span.setStatus({
57
51
  code: SpanStatusCode.ERROR,
58
- message: error.message,
52
+ message: error.message
59
53
  })
60
54
 
61
55
  if (isNewSpan) {
62
56
  span.end()
63
57
  }
64
58
  }
59
+
60
+ /**
61
+ * Best-effort deduplication of exceptions that fire multiple times
62
+ * (e.g. framework handler + global handlers) within a short time window.
63
+ */
64
+
65
+ const exceptionDedupeWindowMs = 2000
66
+ const recentExceptionFingerprints = new Map<string, number>()
67
+
68
+ export const shouldCaptureException = (error: Error, _errorInfo?: Record<string, any>): boolean => {
69
+ if (!error) return false
70
+
71
+ const now = Date.now()
72
+
73
+ // Build a fingerprint that is stable enough across repeated emissions
74
+ // but not so broad that different errors collapse into one.
75
+ const keyParts: string[] = []
76
+ keyParts.push(error.name || 'Error')
77
+ keyParts.push(error.message || '')
78
+
79
+ // First stack line tends to include file/line where it originated.
80
+ if (typeof error.stack === 'string') {
81
+ const firstFrame = error.stack.split('\n')[1] || ''
82
+ keyParts.push(firstFrame.trim())
83
+ }
84
+
85
+ const fingerprint = keyParts.join('|').slice(0, 500)
86
+
87
+ const lastSeen = recentExceptionFingerprints.get(fingerprint)
88
+ if (lastSeen && now - lastSeen < exceptionDedupeWindowMs) {
89
+ return false
90
+ }
91
+
92
+ recentExceptionFingerprints.set(fingerprint, now)
93
+
94
+ // Cheap cleanup of old entries to avoid unbounded growth.
95
+ for (const [key, ts] of recentExceptionFingerprints) {
96
+ if (now - ts > exceptionDedupeWindowMs * 5) {
97
+ recentExceptionFingerprints.delete(key)
98
+ }
99
+ }
100
+
101
+ return true
102
+ }
@@ -31,12 +31,10 @@ export type CrashBufferEventMap = {
31
31
  }
32
32
 
33
33
  export type CrashBufferSnapshot = {
34
- rrwebEvents: CrashBufferRrwebEventPayload[]
35
- otelSpans: CrashBufferOtelSpanPayload[]
36
- attrs: CrashBufferAttrs | null
37
- windowMs: number
38
- fromTs: number
39
- toTs: number
34
+ startedAt: number
35
+ stoppedAt: number
36
+ events: CrashBufferRrwebEventPayload[]
37
+ spans: CrashBufferOtelSpanPayload[]
40
38
  }
41
39
 
42
40
  /**
@@ -48,20 +46,13 @@ export type CrashBufferSnapshot = {
48
46
  * - `pruneOlderThan` is optional because browser implementations can handle pruning internally.
49
47
  */
50
48
  export interface CrashBuffer {
51
- setAttrs(attrs: CrashBufferAttrs): Promise<void>
52
49
  appendEvent(payload: CrashBufferRrwebEventPayload, windowMs?: number): Promise<void>
53
50
  appendSpans(payload: CrashBufferOtelSpanBatchPayload, windowMs?: number): Promise<void>
54
51
  snapshot(windowMs?: number, now?: number): Promise<CrashBufferSnapshot>
55
52
  clear(): Promise<void>
56
53
  pruneOlderThan?(cutoffTs: number): Promise<void>
57
- on?(
58
- event: CrashBufferEventName,
59
- listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void
60
- ): () => void
61
- off?(
62
- event: CrashBufferEventName,
63
- listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void
64
- ): void
54
+ on?(event: CrashBufferEventName, listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void): () => void
55
+ off?(event: CrashBufferEventName, listener: (payload: CrashBufferEventMap[CrashBufferEventName]) => void): void
65
56
  }
66
57
 
67
58
  /**