@posthog/core 1.46.4 → 1.46.6

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.
@@ -26,10 +26,10 @@ __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
27
  ErrorCoercer: ()=>ErrorCoercer
28
28
  });
29
- const index_js_namespaceObject = require("../../utils/index.js");
29
+ const type_utils_js_namespaceObject = require("../../utils/type-utils.js");
30
30
  class ErrorCoercer {
31
31
  match(err) {
32
- return (0, index_js_namespaceObject.isPlainError)(err);
32
+ return (0, type_utils_js_namespaceObject.isError)(err);
33
33
  }
34
34
  coerce(err, ctx) {
35
35
  return {
@@ -1,7 +1,7 @@
1
- import { isPlainError } from "../../utils/index.mjs";
1
+ import { isError } from "../../utils/type-utils.mjs";
2
2
  class ErrorCoercer {
3
3
  match(err) {
4
- return isPlainError(err);
4
+ return isError(err);
5
5
  }
6
6
  coerce(err, ctx) {
7
7
  return {
@@ -7,6 +7,8 @@ export declare class ErrorEventCoercer implements ErrorTrackingCoercer<ErrorEven
7
7
  constructor();
8
8
  match(err: unknown): err is ErrorEventLike;
9
9
  coerce(err: ErrorEventLike, ctx: CoercingContext): ExceptionLike;
10
+ private _hasUsableMessage;
11
+ private _buildLocationStack;
10
12
  }
11
13
  export {};
12
14
  //# sourceMappingURL=error-event-coercer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-event-coercer.d.ts","sourceRoot":"","sources":["../../../src/error-tracking/coercers/error-event-coercer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAO/E,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,qBAAa,iBAAkB,YAAW,oBAAoB,CAAC,cAAc,CAAC;;IAG5E,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,cAAc;IAI1C,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa;CAajE"}
1
+ {"version":3,"file":"error-event-coercer.d.ts","sourceRoot":"","sources":["../../../src/error-tracking/coercers/error-event-coercer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAO/E,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAQD,qBAAa,iBAAkB,YAAW,oBAAoB,CAAC,cAAc,CAAC;;IAG5E,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,cAAc;IAQ1C,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa;IAahE,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,mBAAmB;CAU5B"}
@@ -30,17 +30,29 @@ const index_js_namespaceObject = require("../../utils/index.js");
30
30
  class ErrorEventCoercer {
31
31
  constructor(){}
32
32
  match(err) {
33
- return (0, index_js_namespaceObject.isErrorEvent)(err) && void 0 != err.error;
33
+ if (!(0, index_js_namespaceObject.isErrorEvent)(err)) return false;
34
+ const errorEvent = err;
35
+ return void 0 != errorEvent.error || this._hasUsableMessage(errorEvent);
34
36
  }
35
37
  coerce(err, ctx) {
36
- const exceptionLike = ctx.apply(err.error);
37
- if (!exceptionLike) return {
38
- type: 'ErrorEvent',
39
- value: err.message,
40
- stack: ctx.syntheticException?.stack,
38
+ if (void 0 != err.error) return ctx.apply(err.error);
39
+ const exceptionLike = ctx.apply(err.message);
40
+ return {
41
+ ...exceptionLike,
42
+ stack: this._buildLocationStack(err) ?? exceptionLike.stack,
41
43
  synthetic: true
42
44
  };
43
- return exceptionLike;
45
+ }
46
+ _hasUsableMessage(err) {
47
+ return (0, index_js_namespaceObject.isString)(err.message) && err.message.length > 0;
48
+ }
49
+ _buildLocationStack(err) {
50
+ const location = err;
51
+ if ((0, index_js_namespaceObject.isString)(location.filename) && location.filename.length > 0) {
52
+ const lineno = location.lineno ?? 0;
53
+ const colno = location.colno ?? 0;
54
+ return `Error\n at ${location.filename}:${lineno}:${colno}`;
55
+ }
44
56
  }
45
57
  }
46
58
  exports.ErrorEventCoercer = __webpack_exports__.ErrorEventCoercer;
@@ -1,18 +1,30 @@
1
- import { isErrorEvent } from "../../utils/index.mjs";
1
+ import { isErrorEvent, isString } from "../../utils/index.mjs";
2
2
  class ErrorEventCoercer {
3
3
  constructor(){}
4
4
  match(err) {
5
- return isErrorEvent(err) && void 0 != err.error;
5
+ if (!isErrorEvent(err)) return false;
6
+ const errorEvent = err;
7
+ return void 0 != errorEvent.error || this._hasUsableMessage(errorEvent);
6
8
  }
7
9
  coerce(err, ctx) {
8
- const exceptionLike = ctx.apply(err.error);
9
- if (!exceptionLike) return {
10
- type: 'ErrorEvent',
11
- value: err.message,
12
- stack: ctx.syntheticException?.stack,
10
+ if (void 0 != err.error) return ctx.apply(err.error);
11
+ const exceptionLike = ctx.apply(err.message);
12
+ return {
13
+ ...exceptionLike,
14
+ stack: this._buildLocationStack(err) ?? exceptionLike.stack,
13
15
  synthetic: true
14
16
  };
15
- return exceptionLike;
17
+ }
18
+ _hasUsableMessage(err) {
19
+ return isString(err.message) && err.message.length > 0;
20
+ }
21
+ _buildLocationStack(err) {
22
+ const location = err;
23
+ if (isString(location.filename) && location.filename.length > 0) {
24
+ const lineno = location.lineno ?? 0;
25
+ const colno = location.colno ?? 0;
26
+ return `Error\n at ${location.filename}:${lineno}:${colno}`;
27
+ }
16
28
  }
17
29
  }
18
30
  export { ErrorEventCoercer };
@@ -6,6 +6,7 @@ export declare class ObjectCoercer implements ErrorTrackingCoercer<ObjectLike> {
6
6
  getType(err: Record<string, unknown>): string;
7
7
  getValue(err: object): string;
8
8
  private isSeverityLevel;
9
+ private getStack;
9
10
  /** If a plain object has a property that is an `Error`, return this error. */
10
11
  private getErrorPropertyFromObject;
11
12
  private getObjectClassName;
@@ -1 +1 @@
1
- {"version":3,"file":"object-coercer.d.ts","sourceRoot":"","sources":["../../../src/error-tracking/coercers/object-coercer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,aAAa,EAAiC,MAAM,UAAU,CAAA;AAG9G,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEzC,qBAAa,aAAc,YAAW,oBAAoB,CAAC,UAAU,CAAC;IACpE,KAAK,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,IAAI,UAAU;IAIlD,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,GAAG,SAAS;IAe9E,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAI7C,QAAQ,CAAC,GAAG,EAAE,MAAM;IAmBpB,OAAO,CAAC,eAAe;IAIvB,8EAA8E;IAC9E,OAAO,CAAC,0BAA0B;IAalC,OAAO,CAAC,kBAAkB;CAQ3B"}
1
+ {"version":3,"file":"object-coercer.d.ts","sourceRoot":"","sources":["../../../src/error-tracking/coercers/object-coercer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,aAAa,EAAiC,MAAM,UAAU,CAAA;AAG9G,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEzC,qBAAa,aAAc,YAAW,oBAAoB,CAAC,UAAU,CAAC;IACpE,KAAK,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,IAAI,UAAU;IAIlD,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,GAAG,SAAS;IAe9E,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAI7C,QAAQ,CAAC,GAAG,EAAE,MAAM;IAmBpB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,QAAQ;IAWhB,8EAA8E;IAC9E,OAAO,CAAC,0BAA0B;IAalC,OAAO,CAAC,kBAAkB;CAQ3B"}
@@ -39,7 +39,7 @@ class ObjectCoercer {
39
39
  return {
40
40
  type: this.getType(candidate),
41
41
  value: this.getValue(candidate),
42
- stack: ctx.syntheticException?.stack,
42
+ stack: this.getStack(candidate) ?? ctx.syntheticException?.stack,
43
43
  level: this.isSeverityLevel(candidate.level) ? candidate.level : 'error',
44
44
  synthetic: true
45
45
  };
@@ -61,6 +61,14 @@ class ObjectCoercer {
61
61
  isSeverityLevel(x) {
62
62
  return (0, index_js_namespaceObject.isString)(x) && !(0, index_js_namespaceObject.isEmptyString)(x) && external_types_js_namespaceObject.severityLevels.indexOf(x) >= 0;
63
63
  }
64
+ getStack(candidate) {
65
+ try {
66
+ if ((0, index_js_namespaceObject.isString)(candidate.stacktrace) && candidate.stacktrace.length > 0) return candidate.stacktrace;
67
+ return (0, index_js_namespaceObject.isString)(candidate.stack) && candidate.stack.length > 0 ? candidate.stack : void 0;
68
+ } catch {
69
+ return;
70
+ }
71
+ }
64
72
  getErrorPropertyFromObject(obj) {
65
73
  for(const prop in obj)if (Object.prototype.hasOwnProperty.call(obj, prop)) {
66
74
  const value = obj[prop];
@@ -11,7 +11,7 @@ class ObjectCoercer {
11
11
  return {
12
12
  type: this.getType(candidate),
13
13
  value: this.getValue(candidate),
14
- stack: ctx.syntheticException?.stack,
14
+ stack: this.getStack(candidate) ?? ctx.syntheticException?.stack,
15
15
  level: this.isSeverityLevel(candidate.level) ? candidate.level : 'error',
16
16
  synthetic: true
17
17
  };
@@ -33,6 +33,14 @@ class ObjectCoercer {
33
33
  isSeverityLevel(x) {
34
34
  return isString(x) && !isEmptyString(x) && severityLevels.indexOf(x) >= 0;
35
35
  }
36
+ getStack(candidate) {
37
+ try {
38
+ if (isString(candidate.stacktrace) && candidate.stacktrace.length > 0) return candidate.stacktrace;
39
+ return isString(candidate.stack) && candidate.stack.length > 0 ? candidate.stack : void 0;
40
+ } catch {
41
+ return;
42
+ }
43
+ }
36
44
  getErrorPropertyFromObject(obj) {
37
45
  for(const prop in obj)if (Object.prototype.hasOwnProperty.call(obj, prop)) {
38
46
  const value = obj[prop];
@@ -2,6 +2,7 @@ import { FetchLike } from '../types';
2
2
  export * from './bot-detection';
3
3
  export * from './browser-utils';
4
4
  export * from './bucketed-rate-limiter';
5
+ export * from './json-utils';
5
6
  export * from './number-utils';
6
7
  export * from './string-utils';
7
8
  export * from './type-utils';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAElC,eAAO,MAAM,aAAa,SAAS,CAAA;AAEnC,eAAO,MAAM,UAAU,QAAoE,CAAA;AAE3F,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9D;AASD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,SAAS,CAMxG;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;CACtC;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAqB5F;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAOnE;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,IAAI,GACrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAoBnB;AAGD,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,GAAG,IAAI,OAAO,CAAC,GAAG,CAEtD,CAAA;AAED,eAAO,MAAM,OAAO,GAAI,GAAG,OAAO,KAAG,CAAC,IAAI,KAEzC,CAAA;AAED,wBAAgB,QAAQ,IAAI,SAAS,GAAG,SAAS,CAEhD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAC1B,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAC1C,OAAO,CAAC,CAAC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,EAAE,CAAC,CAStF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAElC,eAAO,MAAM,aAAa,SAAS,CAAA;AAEnC,eAAO,MAAM,UAAU,QAAoE,CAAA;AAE3F,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9D;AASD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,SAAS,CAMxG;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;CACtC;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAqB5F;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAOnE;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,IAAI,GACrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAoBnB;AAGD,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,GAAG,IAAI,OAAO,CAAC,GAAG,CAEtD,CAAA;AAED,eAAO,MAAM,OAAO,GAAI,GAAG,OAAO,KAAG,CAAC,IAAI,KAEzC,CAAA;AAED,wBAAgB,QAAQ,IAAI,SAAS,GAAG,SAAS,CAEhD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAC1B,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAC1C,OAAO,CAAC,CAAC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,EAAE,CAAC,CAStF"}
@@ -9,6 +9,9 @@ var __webpack_modules__ = {
9
9
  "./bucketed-rate-limiter": function(module) {
10
10
  module.exports = require("./bucketed-rate-limiter.js");
11
11
  },
12
+ "./json-utils": function(module) {
13
+ module.exports = require("./json-utils.js");
14
+ },
12
15
  "./logger": function(module) {
13
16
  module.exports = require("./logger.js");
14
17
  },
@@ -161,9 +164,33 @@ var __webpack_exports__ = {};
161
164
  return _bucketed_rate_limiter__WEBPACK_IMPORTED_MODULE_2__[key];
162
165
  }).bind(0, __WEBPACK_IMPORT_KEY__);
163
166
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
164
- var _number_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("./number-utils");
167
+ var _json_utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("./json-utils");
168
+ var __WEBPACK_REEXPORT_OBJECT__ = {};
169
+ for(var __WEBPACK_IMPORT_KEY__ in _json_utils__WEBPACK_IMPORTED_MODULE_3__)if ([
170
+ "removeTrailingSlash",
171
+ "isValidUUID",
172
+ "currentISOTime",
173
+ "isError",
174
+ "safeSetTimeout",
175
+ "UUID_REGEX",
176
+ "assert",
177
+ "allSettled",
178
+ "retriable",
179
+ "default",
180
+ "stripUrlHash",
181
+ "STRING_FORMAT",
182
+ "currentTimestamp",
183
+ "raceWithTimeout",
184
+ "getFetch",
185
+ "getEventUuid",
186
+ "isPromise"
187
+ ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
188
+ return _json_utils__WEBPACK_IMPORTED_MODULE_3__[key];
189
+ }).bind(0, __WEBPACK_IMPORT_KEY__);
190
+ __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
191
+ var _number_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./number-utils");
165
192
  var __WEBPACK_REEXPORT_OBJECT__ = {};
166
- for(var __WEBPACK_IMPORT_KEY__ in _number_utils__WEBPACK_IMPORTED_MODULE_3__)if ([
193
+ for(var __WEBPACK_IMPORT_KEY__ in _number_utils__WEBPACK_IMPORTED_MODULE_4__)if ([
167
194
  "removeTrailingSlash",
168
195
  "isValidUUID",
169
196
  "currentISOTime",
@@ -182,12 +209,12 @@ var __webpack_exports__ = {};
182
209
  "getEventUuid",
183
210
  "isPromise"
184
211
  ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
185
- return _number_utils__WEBPACK_IMPORTED_MODULE_3__[key];
212
+ return _number_utils__WEBPACK_IMPORTED_MODULE_4__[key];
186
213
  }).bind(0, __WEBPACK_IMPORT_KEY__);
187
214
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
188
- var _string_utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./string-utils");
215
+ var _string_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("./string-utils");
189
216
  var __WEBPACK_REEXPORT_OBJECT__ = {};
190
- for(var __WEBPACK_IMPORT_KEY__ in _string_utils__WEBPACK_IMPORTED_MODULE_4__)if ([
217
+ for(var __WEBPACK_IMPORT_KEY__ in _string_utils__WEBPACK_IMPORTED_MODULE_5__)if ([
191
218
  "removeTrailingSlash",
192
219
  "isValidUUID",
193
220
  "currentISOTime",
@@ -206,12 +233,12 @@ var __webpack_exports__ = {};
206
233
  "getEventUuid",
207
234
  "isPromise"
208
235
  ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
209
- return _string_utils__WEBPACK_IMPORTED_MODULE_4__[key];
236
+ return _string_utils__WEBPACK_IMPORTED_MODULE_5__[key];
210
237
  }).bind(0, __WEBPACK_IMPORT_KEY__);
211
238
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
212
- var _type_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("./type-utils");
239
+ var _type_utils__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("./type-utils");
213
240
  var __WEBPACK_REEXPORT_OBJECT__ = {};
214
- for(var __WEBPACK_IMPORT_KEY__ in _type_utils__WEBPACK_IMPORTED_MODULE_5__)if ([
241
+ for(var __WEBPACK_IMPORT_KEY__ in _type_utils__WEBPACK_IMPORTED_MODULE_6__)if ([
215
242
  "removeTrailingSlash",
216
243
  "isValidUUID",
217
244
  "currentISOTime",
@@ -230,12 +257,12 @@ var __webpack_exports__ = {};
230
257
  "getEventUuid",
231
258
  "isPromise"
232
259
  ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
233
- return _type_utils__WEBPACK_IMPORTED_MODULE_5__[key];
260
+ return _type_utils__WEBPACK_IMPORTED_MODULE_6__[key];
234
261
  }).bind(0, __WEBPACK_IMPORT_KEY__);
235
262
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
236
- var _promise_queue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("./promise-queue");
263
+ var _promise_queue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__("./promise-queue");
237
264
  var __WEBPACK_REEXPORT_OBJECT__ = {};
238
- for(var __WEBPACK_IMPORT_KEY__ in _promise_queue__WEBPACK_IMPORTED_MODULE_6__)if ([
265
+ for(var __WEBPACK_IMPORT_KEY__ in _promise_queue__WEBPACK_IMPORTED_MODULE_7__)if ([
239
266
  "removeTrailingSlash",
240
267
  "isValidUUID",
241
268
  "currentISOTime",
@@ -254,12 +281,12 @@ var __webpack_exports__ = {};
254
281
  "getEventUuid",
255
282
  "isPromise"
256
283
  ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
257
- return _promise_queue__WEBPACK_IMPORTED_MODULE_6__[key];
284
+ return _promise_queue__WEBPACK_IMPORTED_MODULE_7__[key];
258
285
  }).bind(0, __WEBPACK_IMPORT_KEY__);
259
286
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
260
- var _logger__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__("./logger");
287
+ var _logger__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__("./logger");
261
288
  var __WEBPACK_REEXPORT_OBJECT__ = {};
262
- for(var __WEBPACK_IMPORT_KEY__ in _logger__WEBPACK_IMPORTED_MODULE_7__)if ([
289
+ for(var __WEBPACK_IMPORT_KEY__ in _logger__WEBPACK_IMPORTED_MODULE_8__)if ([
263
290
  "removeTrailingSlash",
264
291
  "isValidUUID",
265
292
  "currentISOTime",
@@ -278,12 +305,12 @@ var __webpack_exports__ = {};
278
305
  "getEventUuid",
279
306
  "isPromise"
280
307
  ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
281
- return _logger__WEBPACK_IMPORTED_MODULE_7__[key];
308
+ return _logger__WEBPACK_IMPORTED_MODULE_8__[key];
282
309
  }).bind(0, __WEBPACK_IMPORT_KEY__);
283
310
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
284
- var _user_agent_utils__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__("./user-agent-utils");
311
+ var _user_agent_utils__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__("./user-agent-utils");
285
312
  var __WEBPACK_REEXPORT_OBJECT__ = {};
286
- for(var __WEBPACK_IMPORT_KEY__ in _user_agent_utils__WEBPACK_IMPORTED_MODULE_8__)if ([
313
+ for(var __WEBPACK_IMPORT_KEY__ in _user_agent_utils__WEBPACK_IMPORTED_MODULE_9__)if ([
287
314
  "removeTrailingSlash",
288
315
  "isValidUUID",
289
316
  "currentISOTime",
@@ -302,7 +329,7 @@ var __webpack_exports__ = {};
302
329
  "getEventUuid",
303
330
  "isPromise"
304
331
  ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
305
- return _user_agent_utils__WEBPACK_IMPORTED_MODULE_8__[key];
332
+ return _user_agent_utils__WEBPACK_IMPORTED_MODULE_9__[key];
306
333
  }).bind(0, __WEBPACK_IMPORT_KEY__);
307
334
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
308
335
  const STRING_FORMAT = 'utf8';
@@ -1,6 +1,7 @@
1
1
  export * from "./bot-detection.mjs";
2
2
  export * from "./browser-utils.mjs";
3
3
  export * from "./bucketed-rate-limiter.mjs";
4
+ export * from "./json-utils.mjs";
4
5
  export * from "./number-utils.mjs";
5
6
  export * from "./string-utils.mjs";
6
7
  export * from "./type-utils.mjs";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Converts an arbitrary value, sanitizing `toJSON` results when available.
3
+ * The limits keep pathological values from causing unbounded recursion or traversal.
4
+ */
5
+ export declare function toJsonSafeValue(value: unknown): unknown;
6
+ //# sourceMappingURL=json-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-utils.d.ts","sourceRoot":"","sources":["../../src/utils/json-utils.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2GvD"}
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ toJsonSafeValue: ()=>toJsonSafeValue
28
+ });
29
+ const MAX_JSON_SAFE_VALUE_DEPTH = 20;
30
+ const MAX_JSON_SAFE_VALUE_ITEMS = 1000;
31
+ const MAX_JSON_SAFE_VALUE_NODES = 10000;
32
+ const CIRCULAR_VALUE = '[Circular]';
33
+ const TRUNCATED_VALUE = '[Truncated]';
34
+ const UNSERIALIZABLE_VALUE = '[Unserializable]';
35
+ const FUNCTION_VALUE = '[Function]';
36
+ const dateGetTime = Date.prototype.getTime;
37
+ const dateToISOString = Date.prototype.toISOString;
38
+ const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
39
+ function sanitizeString(value) {
40
+ let output = '';
41
+ for(let index = 0; index < value.length; index++){
42
+ const codeUnit = value.charCodeAt(index);
43
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
44
+ const nextCodeUnit = value.charCodeAt(index + 1);
45
+ if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
46
+ output += value[index] + value[index + 1];
47
+ index++;
48
+ } else output += '\ufffd';
49
+ } else output += codeUnit >= 0xdc00 && codeUnit <= 0xdfff ? '\ufffd' : value[index];
50
+ }
51
+ return output;
52
+ }
53
+ function toJsonSafeValue(value) {
54
+ const state = {
55
+ ancestors: new WeakSet(),
56
+ remainingNodes: MAX_JSON_SAFE_VALUE_NODES
57
+ };
58
+ const convert = (current, depth)=>{
59
+ if (state.remainingNodes <= 0) return TRUNCATED_VALUE;
60
+ state.remainingNodes--;
61
+ try {
62
+ if (null == current || 'boolean' == typeof current) return current;
63
+ if ('string' == typeof current) return sanitizeString(current);
64
+ if ('number' == typeof current) return Number.isFinite(current) ? current : null;
65
+ if ('bigint' == typeof current) return current.toString();
66
+ if ('function' == typeof current) return FUNCTION_VALUE;
67
+ if ('symbol' == typeof current) return current.description ? `Symbol(${current.description})` : 'Symbol()';
68
+ if (depth >= MAX_JSON_SAFE_VALUE_DEPTH) return TRUNCATED_VALUE;
69
+ if (state.ancestors.has(current)) return CIRCULAR_VALUE;
70
+ state.ancestors.add(current);
71
+ try {
72
+ if (current instanceof Date) return Number.isFinite(dateGetTime.call(current)) ? dateToISOString.call(current) : null;
73
+ let hasToJSONResult = false;
74
+ let toJSONResult;
75
+ try {
76
+ const toJSON = current.toJSON;
77
+ if ('function' == typeof toJSON) {
78
+ toJSONResult = toJSON.call(current);
79
+ hasToJSONResult = true;
80
+ }
81
+ } catch {
82
+ hasToJSONResult = false;
83
+ }
84
+ if (hasToJSONResult) return convert(toJSONResult, depth + 1);
85
+ if (Array.isArray(current)) {
86
+ const itemCount = Math.min(current.length, MAX_JSON_SAFE_VALUE_ITEMS);
87
+ const output = [];
88
+ let index = 0;
89
+ for(; index < itemCount && state.remainingNodes > 0; index++)output.push(convert(current[index], depth + 1));
90
+ if (current.length > index) output.push(TRUNCATED_VALUE);
91
+ return output;
92
+ }
93
+ const output = {};
94
+ let itemCount = 0;
95
+ let truncated = false;
96
+ for(const key in current){
97
+ if (!propertyIsEnumerable.call(current, key)) break;
98
+ if (itemCount >= MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
99
+ truncated = true;
100
+ break;
101
+ }
102
+ const converted = convert(current[key], depth + 1);
103
+ Object.defineProperty(output, key, {
104
+ value: converted,
105
+ enumerable: true,
106
+ configurable: true,
107
+ writable: true
108
+ });
109
+ itemCount++;
110
+ }
111
+ if (truncated) output[TRUNCATED_VALUE] = 'Additional properties omitted';
112
+ return output;
113
+ } finally{
114
+ state.ancestors.delete(current);
115
+ }
116
+ } catch {
117
+ return UNSERIALIZABLE_VALUE;
118
+ }
119
+ };
120
+ return convert(value, 0);
121
+ }
122
+ exports.toJsonSafeValue = __webpack_exports__.toJsonSafeValue;
123
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
124
+ "toJsonSafeValue"
125
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
126
+ Object.defineProperty(exports, '__esModule', {
127
+ value: true
128
+ });
@@ -0,0 +1,94 @@
1
+ const MAX_JSON_SAFE_VALUE_DEPTH = 20;
2
+ const MAX_JSON_SAFE_VALUE_ITEMS = 1000;
3
+ const MAX_JSON_SAFE_VALUE_NODES = 10000;
4
+ const CIRCULAR_VALUE = '[Circular]';
5
+ const TRUNCATED_VALUE = '[Truncated]';
6
+ const UNSERIALIZABLE_VALUE = '[Unserializable]';
7
+ const FUNCTION_VALUE = '[Function]';
8
+ const dateGetTime = Date.prototype.getTime;
9
+ const dateToISOString = Date.prototype.toISOString;
10
+ const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
11
+ function sanitizeString(value) {
12
+ let output = '';
13
+ for(let index = 0; index < value.length; index++){
14
+ const codeUnit = value.charCodeAt(index);
15
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
16
+ const nextCodeUnit = value.charCodeAt(index + 1);
17
+ if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
18
+ output += value[index] + value[index + 1];
19
+ index++;
20
+ } else output += '\ufffd';
21
+ } else output += codeUnit >= 0xdc00 && codeUnit <= 0xdfff ? '\ufffd' : value[index];
22
+ }
23
+ return output;
24
+ }
25
+ function toJsonSafeValue(value) {
26
+ const state = {
27
+ ancestors: new WeakSet(),
28
+ remainingNodes: MAX_JSON_SAFE_VALUE_NODES
29
+ };
30
+ const convert = (current, depth)=>{
31
+ if (state.remainingNodes <= 0) return TRUNCATED_VALUE;
32
+ state.remainingNodes--;
33
+ try {
34
+ if (null == current || 'boolean' == typeof current) return current;
35
+ if ('string' == typeof current) return sanitizeString(current);
36
+ if ('number' == typeof current) return Number.isFinite(current) ? current : null;
37
+ if ('bigint' == typeof current) return current.toString();
38
+ if ('function' == typeof current) return FUNCTION_VALUE;
39
+ if ('symbol' == typeof current) return current.description ? `Symbol(${current.description})` : 'Symbol()';
40
+ if (depth >= MAX_JSON_SAFE_VALUE_DEPTH) return TRUNCATED_VALUE;
41
+ if (state.ancestors.has(current)) return CIRCULAR_VALUE;
42
+ state.ancestors.add(current);
43
+ try {
44
+ if (current instanceof Date) return Number.isFinite(dateGetTime.call(current)) ? dateToISOString.call(current) : null;
45
+ let hasToJSONResult = false;
46
+ let toJSONResult;
47
+ try {
48
+ const toJSON = current.toJSON;
49
+ if ('function' == typeof toJSON) {
50
+ toJSONResult = toJSON.call(current);
51
+ hasToJSONResult = true;
52
+ }
53
+ } catch {
54
+ hasToJSONResult = false;
55
+ }
56
+ if (hasToJSONResult) return convert(toJSONResult, depth + 1);
57
+ if (Array.isArray(current)) {
58
+ const itemCount = Math.min(current.length, MAX_JSON_SAFE_VALUE_ITEMS);
59
+ const output = [];
60
+ let index = 0;
61
+ for(; index < itemCount && state.remainingNodes > 0; index++)output.push(convert(current[index], depth + 1));
62
+ if (current.length > index) output.push(TRUNCATED_VALUE);
63
+ return output;
64
+ }
65
+ const output = {};
66
+ let itemCount = 0;
67
+ let truncated = false;
68
+ for(const key in current){
69
+ if (!propertyIsEnumerable.call(current, key)) break;
70
+ if (itemCount >= MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
71
+ truncated = true;
72
+ break;
73
+ }
74
+ const converted = convert(current[key], depth + 1);
75
+ Object.defineProperty(output, key, {
76
+ value: converted,
77
+ enumerable: true,
78
+ configurable: true,
79
+ writable: true
80
+ });
81
+ itemCount++;
82
+ }
83
+ if (truncated) output[TRUNCATED_VALUE] = 'Additional properties omitted';
84
+ return output;
85
+ } finally{
86
+ state.ancestors.delete(current);
87
+ }
88
+ } catch {
89
+ return UNSERIALIZABLE_VALUE;
90
+ }
91
+ };
92
+ return convert(value, 0);
93
+ }
94
+ export { toJsonSafeValue };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/core",
3
- "version": "1.46.4",
3
+ "version": "1.46.6",
4
4
  "bugs": {
5
5
  "url": "https://github.com/PostHog/posthog-js/issues"
6
6
  },
@@ -1,9 +1,9 @@
1
- import { isPlainError } from '@/utils'
1
+ import { isError } from '@/utils/type-utils'
2
2
  import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
3
3
 
4
4
  export class ErrorCoercer implements ErrorTrackingCoercer<Error> {
5
5
  match(err: unknown): err is Error {
6
- return isPlainError(err)
6
+ return isError(err)
7
7
  }
8
8
 
9
9
  coerce(err: Error, ctx: CoercingContext): ExceptionLike {
@@ -0,0 +1,109 @@
1
+ import { ErrorPropertiesBuilder } from '../error-properties-builder'
2
+ import { createDefaultStackParser } from '../parsers'
3
+ import { Exception } from '../types'
4
+ import { ErrorCoercer } from './error-coercer'
5
+ import { ErrorEventCoercer } from './error-event-coercer'
6
+ import { EventCoercer } from './event-coercer'
7
+ import { ObjectCoercer } from './object-coercer'
8
+ import { PrimitiveCoercer } from './primitive-coercer'
9
+ import { StringCoercer } from './string-coercer'
10
+
11
+ // `ErrorEvent` is not available in the Node test environment, so use its runtime brand.
12
+ class FakeErrorEvent {
13
+ message?: string
14
+ error?: unknown
15
+ filename?: string
16
+ lineno?: number
17
+ colno?: number;
18
+ [Symbol.toStringTag] = 'ErrorEvent'
19
+
20
+ constructor(init: Partial<Omit<FakeErrorEvent, typeof Symbol.toStringTag>> = {}) {
21
+ Object.assign(this, init)
22
+ }
23
+ }
24
+
25
+ describe('ErrorEventCoercer', () => {
26
+ const coercer = new ErrorEventCoercer()
27
+ const errorPropertiesBuilder = new ErrorPropertiesBuilder(
28
+ [coercer, new ErrorCoercer(), new EventCoercer(), new ObjectCoercer(), new StringCoercer(), new PrimitiveCoercer()],
29
+ createDefaultStackParser()
30
+ )
31
+
32
+ function buildException(input: unknown, syntheticException?: Error): Exception {
33
+ return errorPropertiesBuilder.buildFromUnknown(input, { syntheticException }).$exception_list[0]
34
+ }
35
+
36
+ it.each([
37
+ {
38
+ name: 'an ErrorEvent carrying an Error',
39
+ input: new FakeErrorEvent({ error: new Error('boom') }),
40
+ expected: true,
41
+ },
42
+ {
43
+ name: 'an ErrorEvent carrying a message',
44
+ input: new FakeErrorEvent({ message: 'Script error.' }),
45
+ expected: true,
46
+ },
47
+ { name: 'an ErrorEvent without an error or message', input: new FakeErrorEvent(), expected: false },
48
+ { name: 'an ErrorEvent with an empty message', input: new FakeErrorEvent({ message: '' }), expected: false },
49
+ { name: 'a non-ErrorEvent', input: { message: 'not an ErrorEvent' }, expected: false },
50
+ ])('matches $name: $expected', ({ input, expected }) => {
51
+ expect(coercer.match(input)).toBe(expected)
52
+ })
53
+
54
+ it('unwraps the Error carried by the ErrorEvent', () => {
55
+ const error = new Error('Something broke')
56
+ error.name = 'CustomTestError'
57
+
58
+ expect(buildException(new FakeErrorEvent({ message: 'ignored', error }))).toMatchObject({
59
+ type: 'CustomTestError',
60
+ value: 'Something broke',
61
+ mechanism: { synthetic: false },
62
+ })
63
+ })
64
+
65
+ it('preserves the message and location when there is no Error object', () => {
66
+ const exception = buildException(
67
+ new FakeErrorEvent({
68
+ message: 'Uncaught TypeError: x is not a function',
69
+ filename: 'https://example.com/app.js',
70
+ lineno: 42,
71
+ colno: 13,
72
+ })
73
+ )
74
+
75
+ expect(exception).toMatchObject({
76
+ type: 'TypeError',
77
+ value: 'x is not a function',
78
+ mechanism: { synthetic: true },
79
+ stacktrace: {
80
+ frames: [
81
+ expect.objectContaining({
82
+ filename: 'https://example.com/app.js',
83
+ lineno: 42,
84
+ colno: 13,
85
+ }),
86
+ ],
87
+ },
88
+ })
89
+ })
90
+
91
+ it('does not parse frame-shaped lines from a multiline message', () => {
92
+ const exception = buildException(
93
+ new FakeErrorEvent({
94
+ message: 'Something broke\n at injected (https://example.com/injected.js:1:2)',
95
+ filename: 'https://example.com/app.js',
96
+ lineno: 42,
97
+ colno: 13,
98
+ })
99
+ )
100
+
101
+ expect(exception.stacktrace?.frames).toEqual([
102
+ expect.objectContaining({
103
+ filename: 'https://example.com/app.js',
104
+ lineno: 42,
105
+ colno: 13,
106
+ }),
107
+ ])
108
+ })
109
+ })
@@ -1,5 +1,5 @@
1
1
  import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
2
- import { isErrorEvent } from '@/utils'
2
+ import { isErrorEvent, isString } from '@/utils'
3
3
 
4
4
  // Structural subset of the DOM `ErrorEvent`. Avoids leaking a DOM-only global
5
5
  // into the public type surface so non-DOM consumers (e.g. React Native, whose
@@ -10,24 +10,48 @@ interface ErrorEventLike {
10
10
  error?: unknown
11
11
  }
12
12
 
13
+ interface ErrorEventLocation {
14
+ filename?: string
15
+ lineno?: number
16
+ colno?: number
17
+ }
18
+
13
19
  export class ErrorEventCoercer implements ErrorTrackingCoercer<ErrorEventLike> {
14
20
  constructor() {}
15
21
 
16
22
  match(err: unknown): err is ErrorEventLike {
17
- return isErrorEvent(err) && (err as ErrorEventLike).error != undefined
23
+ if (!isErrorEvent(err)) {
24
+ return false
25
+ }
26
+ const errorEvent = err as ErrorEventLike
27
+ return errorEvent.error != undefined || this._hasUsableMessage(errorEvent)
18
28
  }
19
29
 
20
30
  coerce(err: ErrorEventLike, ctx: CoercingContext): ExceptionLike {
21
- const exceptionLike = ctx.apply(err.error)
22
- if (!exceptionLike) {
23
- return {
24
- type: 'ErrorEvent',
25
- value: err.message,
26
- stack: ctx.syntheticException?.stack,
27
- synthetic: true,
28
- }
29
- } else {
30
- return exceptionLike
31
+ if (err.error != undefined) {
32
+ return ctx.apply(err.error)
33
+ }
34
+
35
+ const exceptionLike = ctx.apply(err.message)
36
+ return {
37
+ ...exceptionLike,
38
+ stack: this._buildLocationStack(err) ?? exceptionLike.stack,
39
+ synthetic: true,
40
+ }
41
+ }
42
+
43
+ private _hasUsableMessage(err: ErrorEventLike): boolean {
44
+ return isString(err.message) && err.message.length > 0
45
+ }
46
+
47
+ private _buildLocationStack(err: ErrorEventLike): string | undefined {
48
+ const location = err as ErrorEventLike & ErrorEventLocation
49
+ if (isString(location.filename) && location.filename.length > 0) {
50
+ const lineno = location.lineno ?? 0
51
+ const colno = location.colno ?? 0
52
+ // The message stays in `value`, which prevents multiline messages from being parsed as extra frames.
53
+ return `Error\n at ${location.filename}:${lineno}:${colno}`
31
54
  }
55
+ return undefined
32
56
  }
33
57
  }
@@ -21,7 +21,7 @@ export class ObjectCoercer implements ErrorTrackingCoercer<ObjectLike> {
21
21
  return {
22
22
  type: this.getType(candidate),
23
23
  value: this.getValue(candidate),
24
- stack: ctx.syntheticException?.stack,
24
+ stack: this.getStack(candidate) ?? ctx.syntheticException?.stack,
25
25
  level: this.isSeverityLevel(candidate.level) ? candidate.level : 'error',
26
26
  synthetic: true,
27
27
  }
@@ -55,6 +55,17 @@ export class ObjectCoercer implements ErrorTrackingCoercer<ObjectLike> {
55
55
  return isString(x) && !isEmptyString(x) && severityLevels.indexOf(x as SeverityLevel) >= 0
56
56
  }
57
57
 
58
+ private getStack(candidate: ObjectLike): string | undefined {
59
+ try {
60
+ if (isString(candidate.stacktrace) && candidate.stacktrace.length > 0) {
61
+ return candidate.stacktrace
62
+ }
63
+ return isString(candidate.stack) && candidate.stack.length > 0 ? candidate.stack : undefined
64
+ } catch {
65
+ return undefined
66
+ }
67
+ }
68
+
58
69
  /** If a plain object has a property that is an `Error`, return this error. */
59
70
  private getErrorPropertyFromObject(obj: Record<string, unknown>): Error | undefined {
60
71
  for (const prop in obj) {
@@ -1,3 +1,5 @@
1
+ import { runInNewContext } from 'node:vm'
2
+
1
3
  import {
2
4
  DOMExceptionCoercer,
3
5
  ErrorEventCoercer,
@@ -110,6 +112,28 @@ describe('ErrorPropertiesBuilder', () => {
110
112
  })
111
113
  })
112
114
 
115
+ it('should preserve a cross-realm error', () => {
116
+ const crossRealmError = runInNewContext(
117
+ `new TypeError('cross-realm error', {
118
+ cause: new Error('cross-realm cause')
119
+ })`
120
+ )
121
+ expect(crossRealmError).not.toBeInstanceOf(Error)
122
+
123
+ expect(coerceInput(crossRealmError)).toMatchObject({
124
+ type: 'TypeError',
125
+ value: 'cross-realm error',
126
+ stack: crossRealmError.stack,
127
+ synthetic: false,
128
+ cause: {
129
+ type: 'Error',
130
+ value: 'cross-realm cause',
131
+ stack: crossRealmError.cause.stack,
132
+ synthetic: false,
133
+ },
134
+ })
135
+ })
136
+
113
137
  it('should handle error with error cause', () => {
114
138
  const secondError = new CustomTestError('My original error')
115
139
  const firstError = new CustomTestError('My wrapped error', secondError)
@@ -225,5 +249,23 @@ describe('ErrorPropertiesBuilder', () => {
225
249
  stack: buriedError.stack,
226
250
  })
227
251
  })
252
+
253
+ it('should preserve an error-like stack from a CustomEvent wrapping a PromiseRejectionEvent', () => {
254
+ const stack = 'TypeError: Extension context invalidated.\n at extension.js:1:2'
255
+ const customEvent = new CustomEvent('unhandledrejection', {
256
+ detail: {
257
+ reason: {
258
+ name: 'TypeError',
259
+ message: 'Extension context invalidated.',
260
+ stack,
261
+ },
262
+ },
263
+ })
264
+
265
+ expect(coerceInput(customEvent)).toMatchObject({
266
+ value: "'TypeError' captured as exception with message: 'Extension context invalidated.'",
267
+ stack,
268
+ })
269
+ })
228
270
  })
229
271
  })
@@ -3,6 +3,7 @@ import { FetchLike } from '../types'
3
3
  export * from './bot-detection'
4
4
  export * from './browser-utils'
5
5
  export * from './bucketed-rate-limiter'
6
+ export * from './json-utils'
6
7
  export * from './number-utils'
7
8
  export * from './string-utils'
8
9
  export * from './type-utils'
@@ -0,0 +1,115 @@
1
+ import { toJsonSafeValue } from './json-utils'
2
+
3
+ describe('toJsonSafeValue', () => {
4
+ it('converts circular and unsupported values while preserving shared references', () => {
5
+ const shared = { id: 1 }
6
+ const value: Record<string, unknown> = {
7
+ count: BigInt(42),
8
+ callback: () => undefined,
9
+ symbol: Symbol('test'),
10
+ invalidNumber: Number.NaN,
11
+ loneSurrogate: '\ud800',
12
+ validSurrogatePair: '😀',
13
+ first: shared,
14
+ second: shared,
15
+ }
16
+ value.self = value
17
+
18
+ expect(toJsonSafeValue(value)).toEqual({
19
+ count: '42',
20
+ callback: '[Function]',
21
+ symbol: 'Symbol(test)',
22
+ invalidNumber: null,
23
+ loneSurrogate: '�',
24
+ validSurrogatePair: '😀',
25
+ first: { id: 1 },
26
+ second: { id: 1 },
27
+ self: '[Circular]',
28
+ })
29
+ })
30
+
31
+ it('sanitizes toJSON results and falls back when toJSON throws', () => {
32
+ const serializableToJSON = jest.fn(() => ({ count: BigInt(2) }))
33
+ const serializable = Object.create({ toJSON: serializableToJSON })
34
+ const throwingToJSON = jest.fn(() => {
35
+ throw new Error('cannot serialize')
36
+ })
37
+ const selfReturningValue: { toJSON: () => unknown } = {
38
+ toJSON: () => selfReturningValue,
39
+ }
40
+ const getTimeOverride = jest.fn(() => 0)
41
+ const toISOStringOverride = jest.fn(() => BigInt(2))
42
+ const date = new Date('2025-01-02T03:04:05.000Z')
43
+ Object.defineProperties(date, {
44
+ getTime: { value: getTimeOverride },
45
+ toISOString: { value: toISOStringOverride },
46
+ })
47
+
48
+ expect(
49
+ toJsonSafeValue({
50
+ serializable,
51
+ throwing: { value: 'kept', toJSON: throwingToJSON },
52
+ selfReturningValue,
53
+ date,
54
+ })
55
+ ).toEqual({
56
+ serializable: { count: '2' },
57
+ throwing: { value: 'kept', toJSON: '[Function]' },
58
+ selfReturningValue: '[Circular]',
59
+ date: '2025-01-02T03:04:05.000Z',
60
+ })
61
+ expect(serializableToJSON).toHaveBeenCalledTimes(1)
62
+ expect(throwingToJSON).toHaveBeenCalledTimes(1)
63
+ expect(getTimeOverride).not.toHaveBeenCalled()
64
+ expect(toISOStringOverride).not.toHaveBeenCalled()
65
+ })
66
+
67
+ it('stops before traversing inherited enumerable properties', () => {
68
+ const prototype = Object.fromEntries(Array.from({ length: 2_000 }, (_, index) => [`key-${index}`, index]))
69
+ const target = Object.assign(Object.create(prototype), { own: 'kept' })
70
+ const getOwnPropertyDescriptor = jest.fn((object: object, key: string | symbol) =>
71
+ Object.getOwnPropertyDescriptor(object, key)
72
+ )
73
+ const value = new Proxy(target, { getOwnPropertyDescriptor })
74
+
75
+ expect(toJsonSafeValue(value)).toEqual({ own: 'kept' })
76
+ expect(getOwnPropertyDescriptor.mock.calls.length).toBeLessThan(10)
77
+ })
78
+
79
+ it('returns a fallback for values that throw during traversal', () => {
80
+ const value = new Proxy(
81
+ {},
82
+ {
83
+ ownKeys() {
84
+ throw new Error('cannot inspect')
85
+ },
86
+ }
87
+ )
88
+
89
+ expect(toJsonSafeValue(value)).toBe('[Unserializable]')
90
+ })
91
+
92
+ it('bounds deeply nested, oversized, and node-heavy values', () => {
93
+ const deepValue: Record<string, unknown> = {}
94
+ let cursor = deepValue
95
+ for (let depth = 0; depth < 100; depth++) {
96
+ const child: Record<string, unknown> = {}
97
+ cursor.child = child
98
+ cursor = child
99
+ }
100
+ const oversizedArray = Array.from({ length: 2_000 }, (_, index) => index)
101
+ const oversizedObject = Object.fromEntries(Array.from({ length: 2_000 }, (_, index) => [`key-${index}`, index]))
102
+ const nodeHeavyValue = Array.from({ length: 1_000 }, () => Array.from({ length: 20 }, () => true))
103
+
104
+ expect(JSON.stringify(toJsonSafeValue(deepValue))).toContain('[Truncated]')
105
+ expect(toJsonSafeValue(oversizedArray)).toEqual([...oversizedArray.slice(0, 1_000), '[Truncated]'])
106
+
107
+ const safeObject = toJsonSafeValue(oversizedObject) as Record<string, unknown>
108
+ expect(Object.keys(safeObject)).toHaveLength(1_001)
109
+ expect(safeObject['[Truncated]']).toBe('Additional properties omitted')
110
+
111
+ const safeNodeHeavyValue = toJsonSafeValue(nodeHeavyValue) as unknown[]
112
+ expect(safeNodeHeavyValue).toHaveLength(478)
113
+ expect(safeNodeHeavyValue.at(-1)).toBe('[Truncated]')
114
+ })
115
+ })
@@ -0,0 +1,148 @@
1
+ const MAX_JSON_SAFE_VALUE_DEPTH = 20
2
+ const MAX_JSON_SAFE_VALUE_ITEMS = 1_000
3
+ const MAX_JSON_SAFE_VALUE_NODES = 10_000
4
+ const CIRCULAR_VALUE = '[Circular]'
5
+ const TRUNCATED_VALUE = '[Truncated]'
6
+ const UNSERIALIZABLE_VALUE = '[Unserializable]'
7
+ const FUNCTION_VALUE = '[Function]'
8
+
9
+ const dateGetTime = Date.prototype.getTime
10
+ const dateToISOString = Date.prototype.toISOString
11
+ const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
12
+
13
+ interface JsonSafeValueConversionState {
14
+ ancestors: WeakSet<object>
15
+ remainingNodes: number
16
+ }
17
+
18
+ function sanitizeString(value: string): string {
19
+ let output = ''
20
+ for (let index = 0; index < value.length; index++) {
21
+ const codeUnit = value.charCodeAt(index)
22
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
23
+ const nextCodeUnit = value.charCodeAt(index + 1)
24
+ if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
25
+ output += value[index] + value[index + 1]
26
+ index++
27
+ } else {
28
+ output += '\ufffd'
29
+ }
30
+ } else {
31
+ output += codeUnit >= 0xdc00 && codeUnit <= 0xdfff ? '\ufffd' : value[index]
32
+ }
33
+ }
34
+ return output
35
+ }
36
+
37
+ /**
38
+ * Converts an arbitrary value, sanitizing `toJSON` results when available.
39
+ * The limits keep pathological values from causing unbounded recursion or traversal.
40
+ */
41
+ export function toJsonSafeValue(value: unknown): unknown {
42
+ const state: JsonSafeValueConversionState = {
43
+ ancestors: new WeakSet(),
44
+ remainingNodes: MAX_JSON_SAFE_VALUE_NODES,
45
+ }
46
+
47
+ const convert = (current: unknown, depth: number): unknown => {
48
+ if (state.remainingNodes <= 0) {
49
+ return TRUNCATED_VALUE
50
+ }
51
+ state.remainingNodes--
52
+
53
+ try {
54
+ if (current === null || current === undefined || typeof current === 'boolean') {
55
+ return current
56
+ }
57
+ if (typeof current === 'string') {
58
+ return sanitizeString(current)
59
+ }
60
+ if (typeof current === 'number') {
61
+ return Number.isFinite(current) ? current : null
62
+ }
63
+ if (typeof current === 'bigint') {
64
+ return current.toString()
65
+ }
66
+ if (typeof current === 'function') {
67
+ return FUNCTION_VALUE
68
+ }
69
+ if (typeof current === 'symbol') {
70
+ return current.description ? `Symbol(${current.description})` : 'Symbol()'
71
+ }
72
+ if (depth >= MAX_JSON_SAFE_VALUE_DEPTH) {
73
+ return TRUNCATED_VALUE
74
+ }
75
+
76
+ if (state.ancestors.has(current)) {
77
+ return CIRCULAR_VALUE
78
+ }
79
+
80
+ state.ancestors.add(current)
81
+ try {
82
+ if (current instanceof Date) {
83
+ return Number.isFinite(dateGetTime.call(current)) ? dateToISOString.call(current) : null
84
+ }
85
+
86
+ let hasToJSONResult = false
87
+ let toJSONResult: unknown
88
+ try {
89
+ const toJSON = (current as { toJSON?: unknown }).toJSON
90
+ if (typeof toJSON === 'function') {
91
+ toJSONResult = toJSON.call(current)
92
+ hasToJSONResult = true
93
+ }
94
+ } catch {
95
+ hasToJSONResult = false
96
+ }
97
+ if (hasToJSONResult) {
98
+ return convert(toJSONResult, depth + 1)
99
+ }
100
+
101
+ if (Array.isArray(current)) {
102
+ const itemCount = Math.min(current.length, MAX_JSON_SAFE_VALUE_ITEMS)
103
+ const output: unknown[] = []
104
+ let index = 0
105
+ for (; index < itemCount && state.remainingNodes > 0; index++) {
106
+ output.push(convert(current[index], depth + 1))
107
+ }
108
+ if (current.length > index) {
109
+ output.push(TRUNCATED_VALUE)
110
+ }
111
+ return output
112
+ }
113
+
114
+ const output: Record<string, unknown> = {}
115
+ let itemCount = 0
116
+ let truncated = false
117
+ for (const key in current) {
118
+ // for...in visits own enumerable keys before walking the prototype chain.
119
+ if (!propertyIsEnumerable.call(current, key)) {
120
+ break
121
+ }
122
+ if (itemCount >= MAX_JSON_SAFE_VALUE_ITEMS || state.remainingNodes <= 0) {
123
+ truncated = true
124
+ break
125
+ }
126
+ const converted = convert((current as Record<string, unknown>)[key], depth + 1)
127
+ Object.defineProperty(output, key, {
128
+ value: converted,
129
+ enumerable: true,
130
+ configurable: true,
131
+ writable: true,
132
+ })
133
+ itemCount++
134
+ }
135
+ if (truncated) {
136
+ output[TRUNCATED_VALUE] = 'Additional properties omitted'
137
+ }
138
+ return output
139
+ } finally {
140
+ state.ancestors.delete(current)
141
+ }
142
+ } catch {
143
+ return UNSERIALIZABLE_VALUE
144
+ }
145
+ }
146
+
147
+ return convert(value, 0)
148
+ }