@zuplo/cli 7.7.2 → 7.7.4

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.
Files changed (42) hide show
  1. package/node_modules/@posthog/core/LICENSE +417 -0
  2. package/node_modules/@posthog/core/dist/error-tracking/parsers/index.d.ts.map +1 -1
  3. package/node_modules/@posthog/core/dist/error-tracking/parsers/index.js +87 -2
  4. package/node_modules/@posthog/core/dist/error-tracking/parsers/index.mjs +87 -2
  5. package/node_modules/@posthog/core/dist/utils/index.d.ts +2 -0
  6. package/node_modules/@posthog/core/dist/utils/index.d.ts.map +1 -1
  7. package/node_modules/@posthog/core/dist/utils/index.js +36 -20
  8. package/node_modules/@posthog/core/dist/utils/index.mjs +5 -1
  9. package/node_modules/@posthog/core/package.json +2 -2
  10. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.parse.spec.ts +3 -1
  11. package/node_modules/@posthog/core/src/error-tracking/parsers/index.spec.ts +270 -0
  12. package/node_modules/@posthog/core/src/error-tracking/parsers/index.ts +197 -2
  13. package/node_modules/@posthog/core/src/utils/index.ts +6 -0
  14. package/node_modules/@posthog/types/LICENSE +64 -0
  15. package/node_modules/@posthog/types/dist/posthog-config.d.ts +6 -0
  16. package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
  17. package/node_modules/@posthog/types/package.json +1 -1
  18. package/node_modules/@posthog/types/src/posthog-config.ts +6 -0
  19. package/node_modules/@zuplo/core/customer.cli.minified.js +185 -185
  20. package/node_modules/@zuplo/core/index.minified.js +190 -190
  21. package/node_modules/@zuplo/core/package.json +1 -1
  22. package/node_modules/@zuplo/graphql/package.json +1 -1
  23. package/node_modules/@zuplo/openapi-tools/package.json +2 -2
  24. package/node_modules/@zuplo/otel/package.json +1 -1
  25. package/node_modules/@zuplo/runtime/out/esm/{chunk-BXPXENPX.js → chunk-Z6XBRCGQ.js} +80 -80
  26. package/node_modules/@zuplo/runtime/out/esm/chunk-Z6XBRCGQ.js.map +1 -0
  27. package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
  28. package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
  29. package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
  30. package/node_modules/@zuplo/runtime/package.json +1 -1
  31. package/node_modules/js-yaml/dist/js-yaml.js +16 -5
  32. package/node_modules/js-yaml/dist/js-yaml.js.map +1 -1
  33. package/node_modules/js-yaml/dist/js-yaml.min.js +1 -1
  34. package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -1
  35. package/node_modules/js-yaml/dist/js-yaml.mjs +14 -6
  36. package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -1
  37. package/node_modules/js-yaml/lib/loader.js +16 -3
  38. package/node_modules/js-yaml/lib/type/omap.js +3 -3
  39. package/node_modules/js-yaml/package.json +1 -1
  40. package/package.json +12 -12
  41. package/node_modules/@zuplo/runtime/out/esm/chunk-BXPXENPX.js.map +0 -1
  42. /package/node_modules/@zuplo/runtime/out/esm/{chunk-BXPXENPX.js.LEGAL.txt → chunk-Z6XBRCGQ.js.LEGAL.txt} +0 -0
@@ -81,9 +81,10 @@ function __webpack_require__(moduleId) {
81
81
  var __webpack_exports__ = {};
82
82
  (()=>{
83
83
  __webpack_require__.r(__webpack_exports__);
84
- var _bot_detection__rspack_import_0 = __webpack_require__("./bot-detection");
84
+ var _type_utils__rspack_import_0 = __webpack_require__("./type-utils");
85
+ var _bot_detection__rspack_import_1 = __webpack_require__("./bot-detection");
85
86
  var __rspack_reexport = {};
86
- for(const __rspack_import_key in _bot_detection__rspack_import_0)if ([
87
+ for(const __rspack_import_key in _bot_detection__rspack_import_1)if ([
87
88
  "UUID_REGEX",
88
89
  "currentTimestamp",
89
90
  "getEventUuid",
@@ -101,12 +102,13 @@ var __webpack_exports__ = {};
101
102
  "retriable",
102
103
  "isPromise",
103
104
  "getFetch",
105
+ "isUrl",
104
106
  "toJsonSafeValue"
105
- ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_bot_detection__rspack_import_0[__rspack_import_key];
107
+ ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_bot_detection__rspack_import_1[__rspack_import_key];
106
108
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
107
- var _browser_utils__rspack_import_1 = __webpack_require__("./browser-utils");
109
+ var _browser_utils__rspack_import_2 = __webpack_require__("./browser-utils");
108
110
  var __rspack_reexport = {};
109
- for(const __rspack_import_key in _browser_utils__rspack_import_1)if ([
111
+ for(const __rspack_import_key in _browser_utils__rspack_import_2)if ([
110
112
  "UUID_REGEX",
111
113
  "currentTimestamp",
112
114
  "getEventUuid",
@@ -124,12 +126,13 @@ var __webpack_exports__ = {};
124
126
  "retriable",
125
127
  "isPromise",
126
128
  "getFetch",
129
+ "isUrl",
127
130
  "toJsonSafeValue"
128
- ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_browser_utils__rspack_import_1[__rspack_import_key];
131
+ ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_browser_utils__rspack_import_2[__rspack_import_key];
129
132
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
130
- var _bucketed_rate_limiter__rspack_import_2 = __webpack_require__("./bucketed-rate-limiter");
133
+ var _bucketed_rate_limiter__rspack_import_3 = __webpack_require__("./bucketed-rate-limiter");
131
134
  var __rspack_reexport = {};
132
- for(const __rspack_import_key in _bucketed_rate_limiter__rspack_import_2)if ([
135
+ for(const __rspack_import_key in _bucketed_rate_limiter__rspack_import_3)if ([
133
136
  "UUID_REGEX",
134
137
  "currentTimestamp",
135
138
  "getEventUuid",
@@ -147,13 +150,14 @@ var __webpack_exports__ = {};
147
150
  "retriable",
148
151
  "isPromise",
149
152
  "getFetch",
153
+ "isUrl",
150
154
  "toJsonSafeValue"
151
- ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_bucketed_rate_limiter__rspack_import_2[__rspack_import_key];
155
+ ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_bucketed_rate_limiter__rspack_import_3[__rspack_import_key];
152
156
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
153
- var _json_utils__rspack_import_3 = __webpack_require__("./json-utils");
154
- var _number_utils__rspack_import_4 = __webpack_require__("./number-utils");
157
+ var _json_utils__rspack_import_4 = __webpack_require__("./json-utils");
158
+ var _number_utils__rspack_import_5 = __webpack_require__("./number-utils");
155
159
  var __rspack_reexport = {};
156
- for(const __rspack_import_key in _number_utils__rspack_import_4)if ([
160
+ for(const __rspack_import_key in _number_utils__rspack_import_5)if ([
157
161
  "UUID_REGEX",
158
162
  "currentTimestamp",
159
163
  "getEventUuid",
@@ -171,12 +175,13 @@ var __webpack_exports__ = {};
171
175
  "retriable",
172
176
  "isPromise",
173
177
  "getFetch",
178
+ "isUrl",
174
179
  "toJsonSafeValue"
175
- ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_number_utils__rspack_import_4[__rspack_import_key];
180
+ ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_number_utils__rspack_import_5[__rspack_import_key];
176
181
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
177
- var _string_utils__rspack_import_5 = __webpack_require__("./string-utils");
182
+ var _string_utils__rspack_import_6 = __webpack_require__("./string-utils");
178
183
  var __rspack_reexport = {};
179
- for(const __rspack_import_key in _string_utils__rspack_import_5)if ([
184
+ for(const __rspack_import_key in _string_utils__rspack_import_6)if ([
180
185
  "UUID_REGEX",
181
186
  "currentTimestamp",
182
187
  "getEventUuid",
@@ -194,12 +199,12 @@ var __webpack_exports__ = {};
194
199
  "retriable",
195
200
  "isPromise",
196
201
  "getFetch",
202
+ "isUrl",
197
203
  "toJsonSafeValue"
198
- ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_string_utils__rspack_import_5[__rspack_import_key];
204
+ ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_string_utils__rspack_import_6[__rspack_import_key];
199
205
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
200
- var _type_utils__rspack_import_6 = __webpack_require__("./type-utils");
201
206
  var __rspack_reexport = {};
202
- for(const __rspack_import_key in _type_utils__rspack_import_6)if ([
207
+ for(const __rspack_import_key in _type_utils__rspack_import_0)if ([
203
208
  "UUID_REGEX",
204
209
  "currentTimestamp",
205
210
  "getEventUuid",
@@ -217,8 +222,9 @@ var __webpack_exports__ = {};
217
222
  "retriable",
218
223
  "isPromise",
219
224
  "getFetch",
225
+ "isUrl",
220
226
  "toJsonSafeValue"
221
- ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_type_utils__rspack_import_6[__rspack_import_key];
227
+ ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_type_utils__rspack_import_0[__rspack_import_key];
222
228
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
223
229
  var _promise_queue__rspack_import_7 = __webpack_require__("./promise-queue");
224
230
  var __rspack_reexport = {};
@@ -240,6 +246,7 @@ var __webpack_exports__ = {};
240
246
  "retriable",
241
247
  "isPromise",
242
248
  "getFetch",
249
+ "isUrl",
243
250
  "toJsonSafeValue"
244
251
  ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_promise_queue__rspack_import_7[__rspack_import_key];
245
252
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
@@ -263,6 +270,7 @@ var __webpack_exports__ = {};
263
270
  "retriable",
264
271
  "isPromise",
265
272
  "getFetch",
273
+ "isUrl",
266
274
  "toJsonSafeValue"
267
275
  ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_logger__rspack_import_8[__rspack_import_key];
268
276
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
@@ -286,6 +294,7 @@ var __webpack_exports__ = {};
286
294
  "retriable",
287
295
  "isPromise",
288
296
  "getFetch",
297
+ "isUrl",
289
298
  "toJsonSafeValue"
290
299
  ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_user_agent_utils__rspack_import_9[__rspack_import_key];
291
300
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
@@ -309,6 +318,7 @@ var __webpack_exports__ = {};
309
318
  "retriable",
310
319
  "isPromise",
311
320
  "getFetch",
321
+ "isUrl",
312
322
  "toJsonSafeValue"
313
323
  ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_webview_app_utils__rspack_import_10[__rspack_import_key];
314
324
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
@@ -339,6 +349,9 @@ var __webpack_exports__ = {};
339
349
  if (0 === truthyValue.trim().length) return true;
340
350
  return false;
341
351
  }
352
+ function isUrl(value) {
353
+ return (0, _type_utils__rspack_import_0.isString)(value) && /^(https?:\/\/|\/|\.\.?\/)/i.test(value.trim());
354
+ }
342
355
  function removeTrailingSlash(url) {
343
356
  return url?.replace(/\/+$/, '');
344
357
  }
@@ -412,13 +425,14 @@ var __webpack_exports__ = {};
412
425
  currentTimestamp: ()=>currentTimestamp,
413
426
  getEventUuid: ()=>getEventUuid,
414
427
  getFetch: ()=>getFetch,
428
+ isUrl: ()=>isUrl,
415
429
  isValidUUID: ()=>isValidUUID,
416
430
  raceWithTimeout: ()=>raceWithTimeout,
417
431
  removeTrailingSlash: ()=>removeTrailingSlash,
418
432
  retriable: ()=>retriable,
419
433
  safeSetTimeout: ()=>safeSetTimeout,
420
434
  stripUrlHash: ()=>stripUrlHash,
421
- toJsonSafeValue: ()=>_json_utils__rspack_import_3.toJsonSafeValue
435
+ toJsonSafeValue: ()=>_json_utils__rspack_import_4.toJsonSafeValue
422
436
  }, {
423
437
  STRING_FORMAT: STRING_FORMAT,
424
438
  UUID_REGEX: UUID_REGEX,
@@ -435,6 +449,7 @@ exports.currentTimestamp = __webpack_exports__.currentTimestamp;
435
449
  exports.getEventUuid = __webpack_exports__.getEventUuid;
436
450
  exports.getFetch = __webpack_exports__.getFetch;
437
451
  exports.isPromise = __webpack_exports__.isPromise;
452
+ exports.isUrl = __webpack_exports__.isUrl;
438
453
  exports.isValidUUID = __webpack_exports__.isValidUUID;
439
454
  exports.raceWithTimeout = __webpack_exports__.raceWithTimeout;
440
455
  exports.removeTrailingSlash = __webpack_exports__.removeTrailingSlash;
@@ -453,6 +468,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
453
468
  "getEventUuid",
454
469
  "getFetch",
455
470
  "isPromise",
471
+ "isUrl",
456
472
  "isValidUUID",
457
473
  "raceWithTimeout",
458
474
  "removeTrailingSlash",
@@ -1,3 +1,4 @@
1
+ import { isString } from "./type-utils.mjs";
1
2
  export * from "./bot-detection.mjs";
2
3
  export * from "./browser-utils.mjs";
3
4
  export * from "./bucketed-rate-limiter.mjs";
@@ -35,6 +36,9 @@ function isEmpty(truthyValue) {
35
36
  if (0 === truthyValue.trim().length) return true;
36
37
  return false;
37
38
  }
39
+ function isUrl(value) {
40
+ return isString(value) && /^(https?:\/\/|\/|\.\.?\/)/i.test(value.trim());
41
+ }
38
42
  function removeTrailingSlash(url) {
39
43
  return url?.replace(/\/+$/, '');
40
44
  }
@@ -101,4 +105,4 @@ function allSettled(promises) {
101
105
  }))));
102
106
  }
103
107
  export { toJsonSafeValue } from "./json-utils.mjs";
104
- export { STRING_FORMAT, UUID_REGEX, allSettled, assert, createNamedError, currentISOTime, currentTimestamp, getEventUuid, getFetch, isPromise, isValidUUID, raceWithTimeout, removeTrailingSlash, retriable, safeSetTimeout, stripUrlHash };
108
+ export { STRING_FORMAT, UUID_REGEX, allSettled, assert, createNamedError, currentISOTime, currentTimestamp, getEventUuid, getFetch, isPromise, isUrl, isValidUUID, raceWithTimeout, removeTrailingSlash, retriable, safeSetTimeout, stripUrlHash };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/core",
3
- "version": "1.51.0",
3
+ "version": "1.52.0",
4
4
  "bugs": {
5
5
  "url": "https://github.com/PostHog/posthog-js/issues"
6
6
  },
@@ -70,7 +70,7 @@
70
70
  }
71
71
  },
72
72
  "dependencies": {
73
- "@posthog/types": "^1.409.1"
73
+ "@posthog/types": "^1.409.4"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@rslib/core": "0.23.2",
@@ -29,7 +29,9 @@ describe('ErrorPropertiesBuilder', () => {
29
29
  const syntheticError = new Error()
30
30
  const frames = parseStack(syntheticError)
31
31
  expect(frames).toBeDefined()
32
- expect(frames).toHaveLength(15)
32
+ // The test runner calls itself once per nested describe block, and the parser keeps one copy
33
+ // of a repeated frame cycle, so those repeats count as a single frame here.
34
+ expect(frames).toHaveLength(13)
33
35
  })
34
36
  })
35
37
  })
@@ -0,0 +1,270 @@
1
+ import { createDefaultStackParser } from './index'
2
+
3
+ describe('createDefaultStackParser repeated cycle collapsing', () => {
4
+ const parse = createDefaultStackParser()
5
+
6
+ const OVERFLOW = 'RangeError: Maximum call stack size exceeded'
7
+
8
+ const frame = (func: string, line: number, column: number = 10): string =>
9
+ ` at ${func} (https://posthog.com/app.js:${line}:${column})`
10
+
11
+ function recursion(cycle: string[], repeats: number): string[] {
12
+ const frames: string[] = []
13
+
14
+ for (let i = 0; i < repeats; i++) {
15
+ cycle.forEach((func, index) => frames.push(frame(func, index + 1)))
16
+ }
17
+
18
+ return frames
19
+ }
20
+
21
+ // The runtime lists the innermost frame first and cuts the stack wherever the depth ran out, so
22
+ // `dropInnermost` is how many frames of the cycle that cut took off the innermost end.
23
+ function overflowStack(cycle: string[], repeats: number, dropInnermost: number = 0): string {
24
+ return [
25
+ OVERFLOW,
26
+ ...recursion(cycle, repeats).slice(dropInnermost),
27
+ frame('handleClick', 90),
28
+ frame('main', 91),
29
+ ].join('\n')
30
+ }
31
+
32
+ function names(stack: string): (string | undefined)[] {
33
+ return parse(stack).map((f) => f.function)
34
+ }
35
+
36
+ it('keeps one copy of a recursion cycle', () => {
37
+ expect(names(overflowStack(['a', 'b'], 30))).toEqual(['main', 'handleClick', 'b', 'a'])
38
+ })
39
+
40
+ it('keeps one copy when a single function calls itself', () => {
41
+ expect(names(overflowStack(['a'], 60))).toEqual(['main', 'handleClick', 'a'])
42
+ })
43
+
44
+ it('keeps the outer frames a recursion used to push past the frame limit', () => {
45
+ // Without collapsing, the frame limit is spent on the cycle and the frames that name the
46
+ // caller never reach the event.
47
+ const frames = names(overflowStack(['a', 'b'], 400))
48
+ expect(frames).toContain('main')
49
+ expect(frames).toContain('handleClick')
50
+ })
51
+
52
+ it('gives the same frames wherever the runtime cut the recursion', () => {
53
+ // The overflow can happen in either function of the pair, which used to change the frames and
54
+ // so open a new issue for every throw.
55
+ const pair = names(overflowStack(['a', 'b'], 30))
56
+ expect(names(overflowStack(['a', 'b'], 30, 1))).toEqual(pair)
57
+
58
+ const triple = names(overflowStack(['a', 'b', 'c'], 30))
59
+ expect(names(overflowStack(['a', 'b', 'c'], 30, 1))).toEqual(triple)
60
+ expect(names(overflowStack(['a', 'b', 'c'], 30, 2))).toEqual(triple)
61
+ })
62
+
63
+ it('gives the same frames when the runtime cut everything outside the recursion', () => {
64
+ // The runtime keeps the innermost frames, so a deep recursion can push every other frame out of
65
+ // the stack. Nothing then says which function of the cycle ran out of stack first.
66
+ const recursionOnly = (dropInnermost: number): (string | undefined)[] =>
67
+ names([OVERFLOW, ...recursion(['a', 'b'], 40).slice(dropInnermost)].join('\n'))
68
+
69
+ expect(recursionOnly(1)).toEqual(recursionOnly(0))
70
+ })
71
+
72
+ it('gives the same frames when the stack holds a second repeated section', () => {
73
+ // Stacks repeat outside the recursion too, such as a test runner that calls itself once per
74
+ // nested block. The frames of that section have to keep the recursion's own partial copy.
75
+ const withOuterRepeat = (dropInnermost: number): (string | undefined)[] =>
76
+ names(
77
+ [
78
+ OVERFLOW,
79
+ ...recursion(['a', 'b'], 30).slice(dropInnermost),
80
+ frame('handleClick', 90),
81
+ frame('walk', 91),
82
+ frame('walk', 91),
83
+ frame('main', 92),
84
+ ].join('\n')
85
+ )
86
+
87
+ expect(withOuterRepeat(0)).toEqual(['main', 'walk', 'handleClick', 'b', 'a'])
88
+ expect(withOuterRepeat(1)).toEqual(withOuterRepeat(0))
89
+ })
90
+
91
+ it('gives the same frames when a cycle that repeats a call site is all that is left', () => {
92
+ // One copy of a cycle can hold the same call site twice, and the innermost frame carries a
93
+ // column of its own, so a single frame does not say which rotation of the copy this throw is.
94
+ const cycle = [frame('a', 1, 10), frame('b', 2), frame('a', 1, 20), frame('c', 4)]
95
+ const lines: string[] = []
96
+
97
+ for (let i = 0; i < 30; i++) {
98
+ lines.push(...cycle)
99
+ }
100
+
101
+ const fromCut = (dropInnermost: number): (string | undefined)[] =>
102
+ names([OVERFLOW, ...lines.slice(dropInnermost)].join('\n'))
103
+
104
+ expect(fromCut(0)).toEqual(['c', 'a', 'b', 'a'])
105
+ expect(fromCut(1)).toEqual(fromCut(0))
106
+ expect(fromCut(2)).toEqual(fromCut(0))
107
+ expect(fromCut(3)).toEqual(fromCut(0))
108
+ })
109
+
110
+ it('reads the outer frames a leftover copy would have pushed past the frame limit', () => {
111
+ // The leftover copy is dropped once the whole stack is read, so it must not hold a slot of the
112
+ // frame limit while the parser is still reading the frames outside the recursion.
113
+ const callers: string[] = []
114
+
115
+ for (let i = 0; i < 48; i++) {
116
+ callers.push(frame(`c${i}`, i + 10))
117
+ }
118
+
119
+ const withCallers = (dropInnermost: number): (string | undefined)[] =>
120
+ names([OVERFLOW, ...recursion(['a', 'b'], 30).slice(dropInnermost), ...callers].join('\n'))
121
+
122
+ expect(withCallers(0)).toHaveLength(50)
123
+ expect(withCallers(1)).toEqual(withCallers(0))
124
+
125
+ // A distinct throw location is not a partial copy and must use a slot of the frame budget.
126
+ const withThrowColumn = [OVERFLOW, frame('a', 1, 4), ...recursion(['a', 'b'], 30).slice(1), ...callers].join('\n')
127
+ const frames = parse(withThrowColumn)
128
+ expect(frames).toHaveLength(50)
129
+ expect(frames.map((f) => f.function)).toEqual([...withCallers(0).slice(1), 'a'])
130
+ expect(frames[frames.length - 1]?.colno).toBe(4)
131
+ })
132
+
133
+ it('gives the same frames when the recursion path calls one function twice in a row', () => {
134
+ // A minified bundle holds one line number per function, so the two calls to `b` are told apart
135
+ // by their column alone. The kept copy holds both of them, and every cut agrees on it.
136
+ const cycle = [frame('a', 1), frame('b', 2, 10), frame('b', 2, 20)]
137
+ const lines: string[] = []
138
+
139
+ for (let i = 0; i < 30; i++) {
140
+ lines.push(...cycle)
141
+ }
142
+
143
+ const fromCut = (dropInnermost: number): (string | undefined)[] =>
144
+ names([OVERFLOW, ...lines.slice(dropInnermost), frame('handleClick', 90), frame('main', 91)].join('\n'))
145
+
146
+ expect(fromCut(0)).toEqual(['main', 'handleClick', 'b', 'b', 'a'])
147
+ expect(fromCut(1)).toEqual(fromCut(0))
148
+ expect(fromCut(2)).toEqual(fromCut(0))
149
+ })
150
+
151
+ it('keeps minified frames that share a name and a line but not a column', () => {
152
+ // A minified bundle puts every function on one line and hands out the same short names, so the
153
+ // column is all that tells two of them apart. Nothing repeats here, so no frame may be dropped.
154
+ const stack = [
155
+ 'Error: boom',
156
+ frame('i', 1, 310),
157
+ frame('t', 1, 4821),
158
+ frame('t', 1, 9137),
159
+ frame('r', 1, 2210),
160
+ frame('onClick', 1, 640),
161
+ ].join('\n')
162
+
163
+ expect(parse(stack).map((f) => `${f.function}:${f.colno}`)).toEqual([
164
+ 'onClick:640',
165
+ 'r:2210',
166
+ 't:9137',
167
+ 't:4821',
168
+ 'i:310',
169
+ ])
170
+ })
171
+
172
+ it.each(['Error: boom', OVERFLOW])('preserves the unnamed throw location for %s', (header) => {
173
+ // Even a confirmed recursion cannot establish that a different innermost column is redundant.
174
+ // Keep that location for both ordinary errors and overflow stacks.
175
+ const anonymous = (column: number): string => ` at https://posthog.com/app.js:1:${column}`
176
+
177
+ const withThrowColumn = (column: number): string[] => {
178
+ const lines = [anonymous(column)]
179
+
180
+ for (let i = 1; i < 40; i++) {
181
+ lines.push(anonymous(10))
182
+ }
183
+
184
+ return parse([header, ...lines, frame('handleClick', 90), frame('main', 91)].join('\n')).map(
185
+ (f) => `${f.function}:${f.colno}`
186
+ )
187
+ }
188
+
189
+ expect(withThrowColumn(4)).toEqual(['main:10', 'handleClick:10', '?:10', '?:4'])
190
+ expect(withThrowColumn(5)).toEqual(['main:10', 'handleClick:10', '?:10', '?:5'])
191
+ expect(withThrowColumn(10)).toEqual(['main:10', 'handleClick:10', '?:10'])
192
+ })
193
+
194
+ it('preserves the column of the call that ran out of stack', () => {
195
+ // Do not guess whether a different column is a redundant overflow location or a distinct throw.
196
+ const stack = [OVERFLOW, frame('a', 1, 4), ...recursion(['a', 'b'], 3).slice(1), frame('handleClick', 90)].join(
197
+ '\n'
198
+ )
199
+
200
+ expect(parse(stack).map((f) => `${f.function}:${f.colno}`)).toEqual(['handleClick:10', 'b:10', 'a:10', 'a:4'])
201
+ })
202
+
203
+ it('keeps frames that repeat without a complete second cycle', () => {
204
+ const stack = ['Error: boom', frame('a', 1), frame('b', 2), frame('a', 1), frame('main', 91)].join('\n')
205
+ expect(names(stack)).toEqual(['main', 'a', 'b', 'a'])
206
+ })
207
+
208
+ it('invalidates a cycle record subsumed by a later collapse', () => {
209
+ const stack = ['Error: boom', frame('a', 1), frame('b', 2), frame('a', 1), frame('a', 1), frame('b', 2)].join('\n')
210
+
211
+ expect(names(stack)).toEqual(['b', 'a'])
212
+ })
213
+
214
+ it('keeps disjoint cycle records when a later collapse subsumes another record', () => {
215
+ const stack = [
216
+ 'Error: boom',
217
+ frame('inner', 90),
218
+ frame('inner', 90),
219
+ frame('a', 1),
220
+ frame('b', 2),
221
+ frame('a', 1),
222
+ frame('a', 1),
223
+ frame('b', 2),
224
+ frame('outer', 91),
225
+ frame('outer', 91),
226
+ ].join('\n')
227
+
228
+ expect(names(stack)).toEqual(['outer', 'b', 'a', 'inner'])
229
+ })
230
+
231
+ it.each([4, 5])('preserves the throw column %i of an ordinary recursive error', (column) => {
232
+ const stack = [
233
+ 'Error: boom',
234
+ frame('recurse', 1, column),
235
+ frame('recurse', 1, 10),
236
+ frame('recurse', 1, 10),
237
+ frame('main', 91),
238
+ ].join('\n')
239
+
240
+ expect(parse(stack).map((f) => `${f.function}:${f.colno}`)).toEqual(['main:10', 'recurse:10', `recurse:${column}`])
241
+ })
242
+
243
+ it('keeps distinct innermost minified call sites for an ordinary error', () => {
244
+ const stack = ['Error: boom', frame('t', 1, 100), frame('t', 1, 200), frame('main', 91)].join('\n')
245
+
246
+ expect(parse(stack).map((f) => `${f.function}:${f.colno}`)).toEqual(['main:10', 't:200', 't:100'])
247
+ })
248
+
249
+ it('canonicalizes recursion-only cycles whose rotations differ only by columns', () => {
250
+ const cycle = [frame('a', 1, 10), frame('b', 1, 20), frame('a', 1, 30), frame('b', 1, 40)]
251
+ const lines: string[] = []
252
+
253
+ for (let i = 0; i < 30; i++) {
254
+ lines.push(...cycle)
255
+ }
256
+
257
+ const fromCut = (dropInnermost: number): string[] =>
258
+ parse([OVERFLOW, ...lines.slice(dropInnermost)].join('\n')).map((f) => `${f.function}:${f.colno}`)
259
+
260
+ expect(fromCut(0)).toEqual(['b:40', 'a:30', 'b:20', 'a:10'])
261
+ expect(fromCut(1)).toEqual(fromCut(0))
262
+ expect(fromCut(2)).toEqual(fromCut(0))
263
+ expect(fromCut(3)).toEqual(fromCut(0))
264
+ })
265
+
266
+ it('keeps calls to one function from different call sites', () => {
267
+ const stack = ['Error: boom', frame('a', 1), frame('a', 2), frame('a', 3), frame('main', 91)].join('\n')
268
+ expect(names(stack)).toEqual(['main', 'a', 'a', 'a'])
269
+ })
270
+ })