@zuplo/cli 7.7.11 → 7.7.13

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 (85) hide show
  1. package/node_modules/@posthog/core/dist/error-tracking/coercers/error-coercer.d.ts.map +1 -1
  2. package/node_modules/@posthog/core/dist/error-tracking/coercers/error-coercer.js +11 -3
  3. package/node_modules/@posthog/core/dist/error-tracking/coercers/error-coercer.mjs +11 -3
  4. package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.d.ts +1 -0
  5. package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.d.ts.map +1 -1
  6. package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.js +130 -34
  7. package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.mjs +131 -35
  8. package/node_modules/@posthog/core/dist/error-tracking/types.d.ts +3 -1
  9. package/node_modules/@posthog/core/dist/error-tracking/types.d.ts.map +1 -1
  10. package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +6 -0
  11. package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
  12. package/node_modules/@posthog/core/dist/posthog-core-stateless.js +3 -0
  13. package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +3 -0
  14. package/node_modules/@posthog/core/package.json +1 -1
  15. package/node_modules/@posthog/core/src/error-tracking/coercers/error-coercer.ts +13 -3
  16. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.aggregate-errors.spec.ts +412 -0
  17. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.metadata.spec.ts +318 -0
  18. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.ts +173 -45
  19. package/node_modules/@posthog/core/src/error-tracking/types.ts +3 -1
  20. package/node_modules/@posthog/core/src/posthog-core-stateless.ts +19 -0
  21. package/node_modules/@posthog/types/dist/posthog-config.d.ts +2 -1
  22. package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
  23. package/node_modules/@posthog/types/dist/posthog.d.ts +1 -1
  24. package/node_modules/@posthog/types/package.json +1 -1
  25. package/node_modules/@posthog/types/src/posthog-config.ts +2 -1
  26. package/node_modules/@posthog/types/src/posthog.ts +1 -1
  27. package/node_modules/@zuplo/core/package.json +1 -1
  28. package/node_modules/@zuplo/graphql/package.json +1 -1
  29. package/node_modules/@zuplo/openapi-tools/package.json +1 -1
  30. package/node_modules/@zuplo/otel/package.json +1 -1
  31. package/node_modules/@zuplo/runtime/out/esm/{chunk-6EJ3W3BS.js → chunk-VUNRXOEM.js} +43 -43
  32. package/node_modules/@zuplo/runtime/out/esm/{chunk-6EJ3W3BS.js.map → chunk-VUNRXOEM.js.map} +1 -1
  33. package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
  34. package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
  35. package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
  36. package/node_modules/@zuplo/runtime/package.json +1 -1
  37. package/node_modules/brace-expansion/dist/commonjs/index.d.ts +4 -0
  38. package/node_modules/brace-expansion/dist/commonjs/index.d.ts.map +1 -1
  39. package/node_modules/brace-expansion/dist/commonjs/index.js +69 -25
  40. package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
  41. package/node_modules/brace-expansion/dist/esm/index.d.ts +4 -0
  42. package/node_modules/brace-expansion/dist/esm/index.d.ts.map +1 -1
  43. package/node_modules/brace-expansion/dist/esm/index.js +68 -24
  44. package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
  45. package/node_modules/brace-expansion/package.json +2 -1
  46. package/node_modules/fast-uri/index.js +10 -6
  47. package/node_modules/fast-uri/package.json +1 -1
  48. package/node_modules/fast-uri/test/security-normalization.test.js +38 -0
  49. package/node_modules/hono/README.md +1 -1
  50. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +5 -4
  51. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +5 -4
  52. package/node_modules/hono/dist/cjs/context.js +1 -1
  53. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +1 -1
  54. package/node_modules/hono/dist/cjs/jsx/dom/render.js +44 -8
  55. package/node_modules/hono/dist/cjs/middleware/language/language.js +4 -1
  56. package/node_modules/hono/dist/cjs/request.js +4 -1
  57. package/node_modules/hono/dist/cjs/utils/accept.js +4 -7
  58. package/node_modules/hono/dist/context.js +1 -1
  59. package/node_modules/hono/dist/helper/accepts/accepts.js +1 -1
  60. package/node_modules/hono/dist/jsx/dom/render.js +44 -8
  61. package/node_modules/hono/dist/middleware/language/language.js +4 -1
  62. package/node_modules/hono/dist/request.js +4 -1
  63. package/node_modules/hono/dist/types/helper/proxy/index.d.ts +1 -1
  64. package/node_modules/hono/dist/types/helper/route/index.d.ts +2 -2
  65. package/node_modules/hono/dist/types/middleware/combine/index.d.ts +3 -1
  66. package/node_modules/hono/dist/types/utils/types.d.ts +1 -1
  67. package/node_modules/hono/dist/utils/accept.js +4 -7
  68. package/node_modules/hono/package.json +1 -1
  69. package/node_modules/ip-address/README.md +114 -114
  70. package/node_modules/ip-address/dist/common.d.ts +5 -0
  71. package/node_modules/ip-address/dist/common.js +12 -0
  72. package/node_modules/ip-address/dist/common.js.map +1 -1
  73. package/node_modules/ip-address/dist/ipv4.d.ts +10 -3
  74. package/node_modules/ip-address/dist/ipv4.js +20 -5
  75. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  76. package/node_modules/ip-address/dist/ipv6.d.ts +5 -1
  77. package/node_modules/ip-address/dist/ipv6.js +18 -3
  78. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  79. package/node_modules/ip-address/package.json +1 -1
  80. package/node_modules/proxy-addr/HISTORY.md +60 -54
  81. package/node_modules/proxy-addr/README.md +1 -1
  82. package/node_modules/proxy-addr/index.js +32 -0
  83. package/node_modules/proxy-addr/package.json +8 -4
  84. package/package.json +11 -11
  85. /package/node_modules/@zuplo/runtime/out/esm/{chunk-6EJ3W3BS.js.LEGAL.txt → chunk-VUNRXOEM.js.LEGAL.txt} +0 -0
@@ -12,11 +12,12 @@ export class ErrorCoercer implements ErrorTrackingCoercer<Error> {
12
12
  // `TypeError`). Fall back to the synthetic exception captured at the call
13
13
  // site so the frames still point at the caller. Mark it synthetic so the
14
14
  // parser trims the SDK's own frames.
15
- const synthetic = stack === undefined
15
+ const replacementStack = stack === undefined ? ctx.syntheticException?.stack : undefined
16
+ const synthetic = !!replacementStack
16
17
  return {
17
18
  type: this.getType(err),
18
19
  value: this.getMessage(err, ctx),
19
- stack: stack ?? ctx.syntheticException?.stack,
20
+ stack: stack ?? replacementStack,
20
21
  cause: err.cause ? ctx.next(err.cause) : undefined,
21
22
  synthetic,
22
23
  }
@@ -37,6 +38,15 @@ export class ErrorCoercer implements ErrorTrackingCoercer<Error> {
37
38
  }
38
39
 
39
40
  private getStack(err: Error & { stacktrace?: string }): string | undefined {
40
- return err.stacktrace || err.stack || undefined
41
+ try {
42
+ const stacktrace = err.stacktrace
43
+ if (typeof stacktrace === 'string' && stacktrace.length > 0) {
44
+ return stacktrace
45
+ }
46
+ const stack = err.stack
47
+ return typeof stack === 'string' && stack.length > 0 ? stack : undefined
48
+ } catch {
49
+ return undefined
50
+ }
41
51
  }
42
52
  }
@@ -0,0 +1,412 @@
1
+ import { runInNewContext } from 'node:vm'
2
+
3
+ import {
4
+ ErrorCoercer,
5
+ ErrorEventCoercer,
6
+ ObjectCoercer,
7
+ PromiseRejectionEventCoercer,
8
+ StringCoercer,
9
+ PrimitiveCoercer,
10
+ } from './coercers'
11
+ import { StackFrame } from './types'
12
+ import { ErrorPropertiesBuilder } from './error-properties-builder'
13
+ import { chromeStackLineParser, createStackParser, nodeStackLineParser } from './parsers'
14
+
15
+ // Aggregate children share one flat exception list with per-entry relationship metadata.
16
+ describe('ErrorPropertiesBuilder AggregateError children', () => {
17
+ const builder = new ErrorPropertiesBuilder(
18
+ [
19
+ new ErrorEventCoercer(),
20
+ new PromiseRejectionEventCoercer(),
21
+ new ErrorCoercer(),
22
+ new ObjectCoercer(),
23
+ new StringCoercer(),
24
+ new PrimitiveCoercer(),
25
+ ],
26
+ createStackParser('web:javascript', chromeStackLineParser)
27
+ )
28
+
29
+ it('retains children and siblings when a member stack cannot be parsed', () => {
30
+ const nodeBuilder = new ErrorPropertiesBuilder(
31
+ [new ErrorCoercer()],
32
+ createStackParser('node:javascript', nodeStackLineParser)
33
+ )
34
+ const child = new AggregateError([new Error('grandchild')], 'malformed stack')
35
+ child.stack = 'Error: malformed stack\n at run (/tmp/%E0%A4%A.js:1:1)'
36
+ const entries = nodeBuilder.buildFromUnknown(
37
+ new AggregateError([child, new Error('sibling')], 'root')
38
+ ).$exception_list
39
+
40
+ expect(entries.map((entry) => entry.value)).toEqual(['root', 'malformed stack', 'grandchild', 'sibling'])
41
+ expect(entries[1].stacktrace).toBeUndefined()
42
+ expect(entries[2].mechanism).toMatchObject({ exception_id: 2, parent_id: 1, source: 'member' })
43
+ expect(entries[3].stacktrace?.frames.length).toBeGreaterThan(0)
44
+ })
45
+
46
+ it('retains aggregate members when root coercion fails', () => {
47
+ const root = new AggregateError([new Error('first'), new Error('second')], 'root')
48
+ Object.defineProperty(root, 'message', {
49
+ get: () => {
50
+ throw new Error('unreadable message')
51
+ },
52
+ })
53
+ const entries = builder.buildFromUnknown(root).$exception_list
54
+
55
+ expect(entries.map((entry) => entry.value)).toEqual(['Unknown error', 'first', 'second'])
56
+ expect(entries[0].mechanism.synthetic).toBe(true)
57
+ expect(entries[1].mechanism).toMatchObject({ exception_id: 1, parent_id: 0, source: 'member' })
58
+ expect(entries[2].mechanism).toMatchObject({ exception_id: 2, parent_id: 0, source: 'member' })
59
+ })
60
+
61
+ it('captures rejected alternatives and their frames from native Promise.any', async () => {
62
+ const first = new TypeError('first alternative')
63
+ const second = new RangeError('second alternative')
64
+ const aggregate = await Promise.any([Promise.reject(first), Promise.reject(second)]).catch((error) => error)
65
+
66
+ expect(aggregate).toBeInstanceOf(AggregateError)
67
+ expect(aggregate.errors).toEqual([first, second])
68
+ const exceptions = builder.buildFromUnknown(aggregate).$exception_list
69
+
70
+ expect(exceptions[0].type).toBe('AggregateError')
71
+ for (const child of [first, second]) {
72
+ const directFrames = builder.buildFromUnknown(child).$exception_list[0].stacktrace?.frames
73
+ expect(directFrames?.length).toBeGreaterThan(0)
74
+ const captured = exceptions.find((exception) => exception.value === child.message)
75
+ expect(captured).toBeDefined()
76
+ expect(captured?.stacktrace?.frames).toEqual(directFrames)
77
+ }
78
+ })
79
+
80
+ it('captures array-valued errors from a custom-named cross-realm aggregate', () => {
81
+ const aggregate = runInNewContext(`(() => {
82
+ const error = new AggregateError([new TypeError('cross-realm alternative')], 'group')
83
+ error.name = 'CustomGroupError'
84
+ return error
85
+ })()`)
86
+ expect(aggregate).not.toBeInstanceOf(Error)
87
+ const exceptions = builder.buildFromUnknown(aggregate).$exception_list
88
+
89
+ expect(exceptions[0]).toMatchObject({ type: 'CustomGroupError', value: 'group' })
90
+ expect(exceptions).toContainEqual(expect.objectContaining({ type: 'TypeError', value: 'cross-realm alternative' }))
91
+ })
92
+
93
+ it.each(['ValidationError', 'AggregateError'])('does not treat a domain error named %s as an aggregate', (name) => {
94
+ const cause = new Error('underlying cause')
95
+ const domainError = Object.assign(new Error('invalid input', { cause }), {
96
+ name,
97
+ errors: [{ path: 'email', message: 'invalid email' }, new Error('validation detail')],
98
+ })
99
+ const exceptions = builder.buildFromUnknown(domainError).$exception_list
100
+
101
+ expect(exceptions.map((exception) => exception.value)).toEqual(['invalid input', 'underlying cause'])
102
+ expect(exceptions[1].mechanism).toMatchObject({ exception_id: 1, parent_id: 0, source: 'cause' })
103
+ })
104
+
105
+ it('does not read the errors accessor of an ordinary error', () => {
106
+ const domainError = new Error('invalid input')
107
+ const getErrors = vi.fn(() => [new Error('validation detail')])
108
+ Object.defineProperty(domainError, 'errors', { get: getErrors })
109
+
110
+ expect(builder.buildFromUnknown(domainError).$exception_list).toHaveLength(1)
111
+ expect(getErrors).not.toHaveBeenCalled()
112
+ })
113
+
114
+ it('does not traverse domain-error details inside a native aggregate', () => {
115
+ const domainError = Object.assign(new Error('invalid input'), { errors: [new Error('validation detail')] })
116
+ const aggregate = new AggregateError([domainError, new Error('next member')], 'group')
117
+ const exceptions = builder.buildFromUnknown(aggregate).$exception_list
118
+
119
+ expect(exceptions.map((exception) => exception.value)).toEqual(['group', 'invalid input', 'next member'])
120
+ expect(exceptions[2].mechanism).toMatchObject({ exception_id: 2, parent_id: 0, source: 'member' })
121
+ })
122
+
123
+ it('does not treat cross-realm validation errors as aggregates', () => {
124
+ const domainError = runInNewContext(`(() => {
125
+ class ValidationError extends Error {
126
+ errors = [{ message: 'invalid email' }]
127
+ }
128
+ return new ValidationError('invalid input')
129
+ })()`)
130
+
131
+ expect(domainError).not.toBeInstanceOf(Error)
132
+ expect(builder.buildFromUnknown(domainError).$exception_list).toHaveLength(1)
133
+ })
134
+
135
+ it('preserves custom-named cross-realm aggregate subclasses', () => {
136
+ const aggregate = runInNewContext(`(() => {
137
+ class CustomGroupError extends AggregateError {}
138
+ const error = new CustomGroupError([new Error('member')], 'group')
139
+ error.name = 'CustomGroupError'
140
+ return error
141
+ })()`)
142
+ const exceptions = builder.buildFromUnknown(aggregate).$exception_list
143
+
144
+ expect(aggregate).not.toBeInstanceOf(AggregateError)
145
+ expect(exceptions.map((exception) => exception.value)).toEqual(['group', 'member'])
146
+ expect(exceptions[1].mechanism).toMatchObject({ exception_id: 1, parent_id: 0, source: 'member' })
147
+ })
148
+
149
+ it('bounds aggregate detection of a cyclic proxy prototype without losing the root', () => {
150
+ const errorBuilder = new ErrorPropertiesBuilder(
151
+ [new ErrorCoercer()],
152
+ createStackParser('web:javascript', chromeStackLineParser)
153
+ )
154
+ let inspections = 0
155
+ const input: Error = new Proxy(new Error('malformed prototype'), {
156
+ get: (target, key) => (key === Symbol.toStringTag ? 'Error' : Reflect.get(target, key)),
157
+ getPrototypeOf: () => {
158
+ inspections++
159
+ return input
160
+ },
161
+ })
162
+
163
+ expect(errorBuilder.buildFromUnknown(input).$exception_list).toMatchObject([{ value: 'malformed prototype' }])
164
+ expect(inspections).toBeGreaterThan(0)
165
+ expect(inspections).toBeLessThanOrEqual(101)
166
+ })
167
+
168
+ it('retains both the ordinary cause and aggregate alternatives', () => {
169
+ const aggregate = new AggregateError([new Error('alternative')], 'group', { cause: new Error('cause') })
170
+ const exceptions = builder.buildFromUnknown(aggregate, {
171
+ mechanism: { handled: false, type: 'onunhandledrejection' },
172
+ }).$exception_list
173
+
174
+ expect(exceptions[0]).toMatchObject({ value: 'group', mechanism: { handled: false } })
175
+ expect(exceptions).toContainEqual(
176
+ expect.objectContaining({
177
+ value: 'cause',
178
+ mechanism: { type: 'chained', source: 'cause', synthetic: false, exception_id: 1, parent_id: 0 },
179
+ })
180
+ )
181
+ expect(exceptions).toContainEqual(expect.objectContaining({ value: 'alternative' }))
182
+ })
183
+
184
+ it('keeps root, causes, then children in depth-first input order with their own stacks', () => {
185
+ const cause = new Error('cause', { cause: new Error('cause origin') })
186
+ const child = new Error('first', { cause: new Error('first cause') })
187
+ const nested = new AggregateError([new Error('nested child')], 'nested', { cause: new Error('nested cause') })
188
+ const aggregate = new AggregateError([child, nested, new Error('last')], 'root', { cause })
189
+ const inputs = [
190
+ aggregate,
191
+ cause,
192
+ cause.cause,
193
+ child,
194
+ child.cause,
195
+ nested,
196
+ nested.cause,
197
+ nested.errors[0],
198
+ aggregate.errors[2],
199
+ ] as Error[]
200
+ for (const [index, input] of inputs.entries()) {
201
+ input.stack = `Error: ${input.message}\n at frame${index} (https://example.com/file${index}.js:12:34)`
202
+ Object.freeze(input)
203
+ }
204
+ Object.freeze(aggregate.errors)
205
+ const properties = builder.buildFromUnknown(aggregate, {
206
+ mechanism: { type: 'onunhandledrejection', handled: false },
207
+ })
208
+ expect(Object.keys(properties).sort()).toEqual(['$exception_level', '$exception_list'])
209
+ expect(properties.$exception_list.map((exception) => exception.value)).toEqual(inputs.map((input) => input.message))
210
+ properties.$exception_list.forEach((exception, index) => {
211
+ expect(Object.keys(exception).sort()).toEqual(['mechanism', 'stacktrace', 'type', 'value'])
212
+ expect(exception.mechanism).toEqual(
213
+ index === 0
214
+ ? { type: 'onunhandledrejection', handled: false, synthetic: false, exception_id: 0 }
215
+ : {
216
+ type: 'chained',
217
+ source: ['cause', 'cause', 'member', 'cause', 'member', 'cause', 'member', 'member'][index - 1],
218
+ synthetic: false,
219
+ exception_id: index,
220
+ parent_id: [0, 1, 0, 3, 0, 5, 5, 0][index - 1],
221
+ }
222
+ )
223
+ expect(exception.stacktrace?.frames).toEqual([
224
+ expect.objectContaining({ filename: `https://example.com/file${index}.js`, lineno: 12, colno: 34 }),
225
+ ])
226
+ })
227
+ })
228
+
229
+ it('retains deep ordinary cause chains without inventing nested handled state or repeating cycles', () => {
230
+ let error = new Error('6')
231
+ for (let index = 5; index >= 0; index--) {
232
+ error = new Error(String(index), { cause: error })
233
+ }
234
+ const exceptions = builder.buildFromUnknown(error, { mechanism: { handled: false } }).$exception_list
235
+ expect(exceptions.map((exception) => exception.value)).toEqual(['0', '1', '2', '3', '4', '5', '6'])
236
+ expect(exceptions.map((exception) => exception.mechanism?.handled)).toEqual([
237
+ false,
238
+ undefined,
239
+ undefined,
240
+ undefined,
241
+ undefined,
242
+ undefined,
243
+ undefined,
244
+ ])
245
+ error.cause = error
246
+ expect(builder.buildFromUnknown(error).$exception_list.map((exception) => exception.value)).toEqual(['0'])
247
+ })
248
+
249
+ it('retains cause and member edges beyond the old depth bound', () => {
250
+ const tooDeep = new AggregateError([new Error('deep child')], 'depth4', { cause: new Error('deep cause') })
251
+ const depth3 = new Error('depth3', { cause: tooDeep })
252
+ const depth2 = new AggregateError([depth3], 'depth2')
253
+ const depth1 = new Error('depth1', { cause: depth2 })
254
+ const root = new AggregateError([depth1, new Error('sibling')], 'root')
255
+ expect(builder.buildFromUnknown(root).$exception_list.map((exception) => exception.value)).toEqual([
256
+ 'root',
257
+ 'depth1',
258
+ 'depth2',
259
+ 'depth3',
260
+ 'depth4',
261
+ 'deep cause',
262
+ 'deep child',
263
+ 'sibling',
264
+ ])
265
+ })
266
+
267
+ it('omits ancestor cycles and emits shared children only in their first position', () => {
268
+ const shared = new Error('shared')
269
+ const root = new AggregateError([], 'root')
270
+ const nested = new AggregateError([root, shared], 'nested', { cause: root })
271
+ root.errors.push(root, nested, shared, shared)
272
+ expect(builder.buildFromUnknown(root).$exception_list.map((exception) => exception.value)).toEqual([
273
+ 'root',
274
+ 'nested',
275
+ 'shared',
276
+ ])
277
+ expect(root.errors).toEqual([root, nested, shared, shared])
278
+ })
279
+
280
+ it('bounds wide aggregate output to 50 entries including root and causes', () => {
281
+ const children = Array.from({ length: 1000 }, (_, index) => new Error(String(index)))
282
+ const unread = vi.fn(() => {
283
+ throw new Error('past the traversal budget')
284
+ })
285
+ Object.defineProperty(children, '48', { get: unread })
286
+ const root = new AggregateError([], 'root', { cause: new Error('cause') })
287
+ root.errors = children
288
+ const exceptions = builder.buildFromUnknown(root).$exception_list
289
+ expect(exceptions.map((exception) => exception.value)).toEqual([
290
+ 'root',
291
+ 'cause',
292
+ ...Array.from({ length: 48 }, (_, index) => String(index)),
293
+ ])
294
+ expect(unread).not.toHaveBeenCalled()
295
+ expect(builder.buildFromUnknown(new AggregateError([new Error('next')], 'next root')).$exception_list).toHaveLength(
296
+ 2
297
+ )
298
+ })
299
+
300
+ it('shares the emission budget across nested groups', () => {
301
+ const groups = Array.from(
302
+ { length: 100 },
303
+ (_, index) => new AggregateError([new Error(`child ${index}`)], `group ${index}`)
304
+ )
305
+ const exceptions = builder.buildFromUnknown(new AggregateError(groups, 'root')).$exception_list
306
+ expect(exceptions).toHaveLength(50)
307
+ expect(exceptions.slice(-3).map((exception) => exception.value)).toEqual(['group 23', 'child 23', 'group 24'])
308
+ })
309
+
310
+ it.each(['error event', 'rejection event', 'object'])(
311
+ 'unwraps an aggregate in an %s without duplicating entries',
312
+ (kind) => {
313
+ const aggregate = new AggregateError([new Error('child')], 'root')
314
+ const input =
315
+ kind === 'error event'
316
+ ? { [Symbol.toStringTag]: 'ErrorEvent', error: aggregate }
317
+ : kind === 'rejection event'
318
+ ? new CustomEvent('unhandledrejection', { detail: { reason: aggregate } })
319
+ : { error: aggregate }
320
+ expect(builder.buildFromUnknown(input).$exception_list.map((exception) => exception.value)).toEqual([
321
+ 'root',
322
+ 'child',
323
+ ])
324
+ }
325
+ )
326
+
327
+ it('bounds self-referencing wrappers inside aggregate children', () => {
328
+ const wrapper = { [Symbol.toStringTag]: 'ErrorEvent', error: undefined as unknown }
329
+ wrapper.error = wrapper
330
+ const root = new AggregateError([wrapper, new Error('last')], 'root')
331
+ expect(builder.buildFromUnknown(root).$exception_list.map((exception) => exception.value)).toEqual([
332
+ 'root',
333
+ 'Unknown error',
334
+ 'last',
335
+ ])
336
+ })
337
+
338
+ it.each([undefined, null, 'not an array', { length: 1, 0: new Error('not a child') }])(
339
+ 'ignores malformed errors collections: %s',
340
+ (errors) => {
341
+ const root = new AggregateError([], 'root')
342
+ root.errors = errors as any
343
+ expect(builder.buildFromUnknown(root).$exception_list.map((exception) => exception.value)).toEqual(['root'])
344
+ }
345
+ )
346
+
347
+ it('ignores a throwing errors getter without losing the root or cause', () => {
348
+ const root = new AggregateError([], 'root', { cause: new Error('cause') })
349
+ Object.defineProperty(root, 'errors', {
350
+ get() {
351
+ throw new Error('unreadable')
352
+ },
353
+ })
354
+ expect(builder.buildFromUnknown(root).$exception_list.map((exception) => exception.value)).toEqual([
355
+ 'root',
356
+ 'cause',
357
+ ])
358
+ })
359
+
360
+ it('uses existing coercers for non-error members and fallback for unreadable members', () => {
361
+ const values = ['text', 42, null, undefined, { message: 'object member' }]
362
+ const root = new AggregateError(values, 'root')
363
+ Object.defineProperty(root.errors, '5', {
364
+ get() {
365
+ throw new Error('unreadable')
366
+ },
367
+ })
368
+ const malformed = new Error('malformed')
369
+ Object.defineProperty(malformed, 'message', {
370
+ get() {
371
+ throw new Error('unreadable message')
372
+ },
373
+ })
374
+ root.errors.push(malformed, new Error('last'))
375
+ const exceptions = builder.buildFromUnknown(root, { syntheticException: new Error('capture site') }).$exception_list
376
+ expect(exceptions.slice(1, 6)).toEqual(
377
+ values.map((value, index) => ({
378
+ ...builder.buildFromUnknown(value).$exception_list[0],
379
+ mechanism: { type: 'chained', source: 'member', synthetic: true, exception_id: index + 1, parent_id: 0 },
380
+ }))
381
+ )
382
+ expect(exceptions.slice(6).map((exception) => exception.value)).toEqual(['Unknown error', 'Unknown error', 'last'])
383
+ expect(exceptions.slice(1, 8).every((exception) => !exception.stacktrace)).toBe(true)
384
+ })
385
+
386
+ it('applies chunk IDs and frame modifiers independently to every entry', async () => {
387
+ const inputs = [new AggregateError([], 'root'), new Error('cause'), new Error('child')]
388
+ inputs.forEach((input, index) => {
389
+ input.stack = `Error\n at f (https://example.com/chunk${index}.js:1:2)`
390
+ })
391
+ inputs[0].cause = inputs[1]
392
+ ;(inputs[0] as AggregateError).errors.push(inputs[2])
393
+ vi.stubGlobal('_posthogChunkIds', Object.fromEntries(inputs.map((input, index) => [input.stack, `chunk-${index}`])))
394
+ const modifier = vi.fn(async (frames: StackFrame[]) => frames.map((frame) => ({ ...frame, module: 'modified' })))
395
+ const modifiedBuilder = new ErrorPropertiesBuilder(
396
+ [new ErrorCoercer()],
397
+ createStackParser('web:javascript', chromeStackLineParser),
398
+ [modifier]
399
+ )
400
+ try {
401
+ const exceptions = await modifiedBuilder.modifyFrames(modifiedBuilder.buildFromUnknown(inputs[0]).$exception_list)
402
+ expect(modifier).toHaveBeenCalledTimes(3)
403
+ exceptions.forEach((exception, index) => {
404
+ expect(exception.stacktrace?.frames).toEqual([
405
+ expect.objectContaining({ chunk_id: `chunk-${index}`, module: 'modified' }),
406
+ ])
407
+ })
408
+ } finally {
409
+ vi.unstubAllGlobals()
410
+ }
411
+ })
412
+ })