@zuplo/cli 7.7.10 → 7.7.12

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 (98) 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 +5 -1
  13. package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +5 -1
  14. package/node_modules/@posthog/core/dist/utils/index.d.ts +1 -0
  15. package/node_modules/@posthog/core/dist/utils/index.d.ts.map +1 -1
  16. package/node_modules/@posthog/core/dist/utils/index.js +35 -18
  17. package/node_modules/@posthog/core/dist/utils/index.mjs +1 -0
  18. package/node_modules/@posthog/core/dist/utils/retry-after.d.ts.map +1 -1
  19. package/node_modules/@posthog/core/dist/utils/retry-after.js +7 -3
  20. package/node_modules/@posthog/core/dist/utils/retry-after.mjs +7 -3
  21. package/node_modules/@posthog/core/package.json +2 -2
  22. package/node_modules/@posthog/core/src/error-tracking/coercers/error-coercer.ts +13 -3
  23. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.aggregate-errors.spec.ts +412 -0
  24. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.metadata.spec.ts +318 -0
  25. package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.ts +173 -45
  26. package/node_modules/@posthog/core/src/error-tracking/types.ts +3 -1
  27. package/node_modules/@posthog/core/src/posthog-core-stateless.ts +21 -1
  28. package/node_modules/@posthog/core/src/utils/index.ts +1 -0
  29. package/node_modules/@posthog/core/src/utils/retry-after.ts +11 -6
  30. package/node_modules/@posthog/types/dist/index.d.ts +1 -1
  31. package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
  32. package/node_modules/@posthog/types/dist/posthog-config.d.ts +67 -2
  33. package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
  34. package/node_modules/@posthog/types/dist/posthog.d.ts +1 -1
  35. package/node_modules/@posthog/types/package.json +1 -1
  36. package/node_modules/@posthog/types/src/index.ts +4 -0
  37. package/node_modules/@posthog/types/src/posthog-config.ts +71 -2
  38. package/node_modules/@posthog/types/src/posthog.ts +1 -1
  39. package/node_modules/@zuplo/core/package.json +1 -1
  40. package/node_modules/@zuplo/graphql/package.json +1 -1
  41. package/node_modules/@zuplo/openapi-tools/package.json +1 -1
  42. package/node_modules/@zuplo/otel/package.json +1 -1
  43. package/node_modules/@zuplo/runtime/out/esm/{chunk-M3VFT3HF.js → chunk-6EJ3W3BS.js} +80 -80
  44. package/node_modules/@zuplo/runtime/out/esm/chunk-6EJ3W3BS.js.map +1 -0
  45. package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
  46. package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
  47. package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
  48. package/node_modules/@zuplo/runtime/package.json +1 -1
  49. package/node_modules/brace-expansion/dist/commonjs/index.d.ts +4 -0
  50. package/node_modules/brace-expansion/dist/commonjs/index.d.ts.map +1 -1
  51. package/node_modules/brace-expansion/dist/commonjs/index.js +69 -25
  52. package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
  53. package/node_modules/brace-expansion/dist/esm/index.d.ts +4 -0
  54. package/node_modules/brace-expansion/dist/esm/index.d.ts.map +1 -1
  55. package/node_modules/brace-expansion/dist/esm/index.js +68 -24
  56. package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
  57. package/node_modules/brace-expansion/package.json +2 -1
  58. package/node_modules/fast-uri/index.js +10 -6
  59. package/node_modules/fast-uri/package.json +1 -1
  60. package/node_modules/fast-uri/test/security-normalization.test.js +38 -0
  61. package/node_modules/hono/README.md +1 -1
  62. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +5 -4
  63. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +5 -4
  64. package/node_modules/hono/dist/cjs/context.js +1 -1
  65. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +1 -1
  66. package/node_modules/hono/dist/cjs/jsx/dom/render.js +44 -8
  67. package/node_modules/hono/dist/cjs/middleware/language/language.js +4 -1
  68. package/node_modules/hono/dist/cjs/request.js +4 -1
  69. package/node_modules/hono/dist/cjs/utils/accept.js +4 -7
  70. package/node_modules/hono/dist/context.js +1 -1
  71. package/node_modules/hono/dist/helper/accepts/accepts.js +1 -1
  72. package/node_modules/hono/dist/jsx/dom/render.js +44 -8
  73. package/node_modules/hono/dist/middleware/language/language.js +4 -1
  74. package/node_modules/hono/dist/request.js +4 -1
  75. package/node_modules/hono/dist/types/helper/proxy/index.d.ts +1 -1
  76. package/node_modules/hono/dist/types/helper/route/index.d.ts +2 -2
  77. package/node_modules/hono/dist/types/middleware/combine/index.d.ts +3 -1
  78. package/node_modules/hono/dist/types/utils/types.d.ts +1 -1
  79. package/node_modules/hono/dist/utils/accept.js +4 -7
  80. package/node_modules/hono/package.json +1 -1
  81. package/node_modules/ip-address/README.md +114 -114
  82. package/node_modules/ip-address/dist/common.d.ts +5 -0
  83. package/node_modules/ip-address/dist/common.js +12 -0
  84. package/node_modules/ip-address/dist/common.js.map +1 -1
  85. package/node_modules/ip-address/dist/ipv4.d.ts +5 -1
  86. package/node_modules/ip-address/dist/ipv4.js +12 -1
  87. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  88. package/node_modules/ip-address/dist/ipv6.d.ts +5 -1
  89. package/node_modules/ip-address/dist/ipv6.js +15 -1
  90. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  91. package/node_modules/ip-address/package.json +1 -1
  92. package/node_modules/proxy-addr/HISTORY.md +60 -54
  93. package/node_modules/proxy-addr/README.md +1 -1
  94. package/node_modules/proxy-addr/index.js +32 -0
  95. package/node_modules/proxy-addr/package.json +8 -4
  96. package/package.json +11 -11
  97. package/node_modules/@zuplo/runtime/out/esm/chunk-M3VFT3HF.js.map +0 -1
  98. /package/node_modules/@zuplo/runtime/out/esm/{chunk-M3VFT3HF.js.LEGAL.txt → chunk-6EJ3W3BS.js.LEGAL.txt} +0 -0
@@ -0,0 +1,318 @@
1
+ import { ErrorCoercer, ErrorEventCoercer, ObjectCoercer, PrimitiveCoercer, StringCoercer } from './coercers'
2
+ import { ErrorPropertiesBuilder } from './error-properties-builder'
3
+ import { chromeStackLineParser, createStackParser } from './parsers'
4
+
5
+ // Canonical nested_cause and aggregate examples from the exception-event-metadata specification.
6
+ describe('ErrorPropertiesBuilder relationship metadata', () => {
7
+ const builder = new ErrorPropertiesBuilder(
8
+ [new ErrorEventCoercer(), new ErrorCoercer(), new ObjectCoercer(), new StringCoercer(), new PrimitiveCoercer()],
9
+ createStackParser('web:javascript', chromeStackLineParser)
10
+ )
11
+ function error(type: string, value: string, cause?: unknown): Error {
12
+ const result = new Error(value, { cause })
13
+ result.name = type
14
+ result.stack = undefined
15
+ return result
16
+ }
17
+
18
+ it('matches the canonical cause example without inventing nested handled state', () => {
19
+ expect(
20
+ builder.buildFromUnknown(error('CheckoutError', 'Checkout failed', error('DatabaseError', 'Connection failed')))
21
+ ).toEqual({
22
+ $exception_level: 'error',
23
+ $exception_list: [
24
+ {
25
+ type: 'CheckoutError',
26
+ value: 'Checkout failed',
27
+ mechanism: { type: 'generic', handled: true, synthetic: false, exception_id: 0 },
28
+ },
29
+ {
30
+ type: 'DatabaseError',
31
+ value: 'Connection failed',
32
+ mechanism: { type: 'chained', source: 'cause', synthetic: false, exception_id: 1, parent_id: 0 },
33
+ },
34
+ ],
35
+ })
36
+ })
37
+
38
+ it('matches the canonical aggregate example including the second member cause', () => {
39
+ const root = new AggregateError(
40
+ [
41
+ error('NetworkError', 'Network failed'),
42
+ error('ValidationError', 'Validation failed', error('ParseError', 'Invalid value')),
43
+ ],
44
+ 'Two operations failed'
45
+ )
46
+ root.stack = undefined
47
+ expect(builder.buildFromUnknown(root, { mechanism: { type: 'task', handled: false } }).$exception_list).toEqual([
48
+ {
49
+ type: 'AggregateError',
50
+ value: 'Two operations failed',
51
+ mechanism: { type: 'task', handled: false, synthetic: false, exception_id: 0 },
52
+ },
53
+ {
54
+ type: 'NetworkError',
55
+ value: 'Network failed',
56
+ mechanism: { type: 'chained', source: 'member', synthetic: false, exception_id: 1, parent_id: 0 },
57
+ },
58
+ {
59
+ type: 'ValidationError',
60
+ value: 'Validation failed',
61
+ mechanism: { type: 'chained', source: 'member', synthetic: false, exception_id: 2, parent_id: 0 },
62
+ },
63
+ {
64
+ type: 'ParseError',
65
+ value: 'Invalid value',
66
+ mechanism: { type: 'chained', source: 'cause', synthetic: false, exception_id: 3, parent_id: 2 },
67
+ },
68
+ ])
69
+ })
70
+
71
+ it('retains the first 50 entries of a deep cause chain with deterministic valid IDs', () => {
72
+ let root = error('Error', '60')
73
+ for (let index = 59; index >= 0; index--) root = error('Error', String(index), root)
74
+ const first = builder.buildFromUnknown(root).$exception_list
75
+ expect(first).toEqual(builder.buildFromUnknown(root).$exception_list)
76
+ expect(first.map((entry) => entry.value)).toEqual(Array.from({ length: 50 }, (_, index) => String(index)))
77
+ first.forEach((entry, index) => {
78
+ expect(entry.mechanism?.exception_id).toBe(index)
79
+ if (index) expect(entry.mechanism?.parent_id).toBe(index - 1)
80
+ })
81
+ })
82
+
83
+ it('skips cycles and shared objects globally without spending the emission budget', () => {
84
+ const shared = error('Error', 'shared')
85
+ const root = new AggregateError([], 'root', { cause: shared })
86
+ shared.cause = root
87
+ root.errors = [
88
+ ...Array(150).fill(root),
89
+ ...Array(150).fill(shared),
90
+ ...Array.from({ length: 60 }, (_, index) => error('Error', String(index))),
91
+ ]
92
+ expect(builder.buildFromUnknown(root).$exception_list.map((entry) => entry.value)).toEqual([
93
+ 'root',
94
+ 'shared',
95
+ ...Array.from({ length: 48 }, (_, index) => String(index)),
96
+ ])
97
+ })
98
+
99
+ it('does not read handled properties from user errors or inherit root provenance', () => {
100
+ const child = error('Error', 'native')
101
+ Object.defineProperty(child, 'handled', {
102
+ get() {
103
+ throw new Error('untrusted')
104
+ },
105
+ })
106
+ const root = new AggregateError([child, 'converted'], 'root')
107
+ root.stack = undefined
108
+ const entries = builder.buildFromUnknown(root, {
109
+ syntheticException: new Error('capture'),
110
+ mechanism: { handled: false, type: 'middleware' },
111
+ }).$exception_list
112
+ expect(entries.map((entry) => entry.mechanism)).toEqual([
113
+ { type: 'middleware', handled: false, synthetic: true, exception_id: 0 },
114
+ { type: 'chained', source: 'member', synthetic: false, exception_id: 1, parent_id: 0 },
115
+ { type: 'chained', source: 'member', synthetic: true, exception_id: 2, parent_id: 0 },
116
+ ])
117
+ })
118
+
119
+ it('preserves typed capture metadata but assigns root linkage at final assembly', () => {
120
+ const root = error('Error', 'root', error('Error', 'child'))
121
+ const entries = builder.buildFromUnknown(root, {
122
+ mechanism: {
123
+ type: 'custom.integration',
124
+ handled: false,
125
+ synthetic: true,
126
+ exception_id: 81,
127
+ parent_id: 80,
128
+ source: 'cause',
129
+ },
130
+ }).$exception_list
131
+ expect(entries[0].mechanism).toEqual({
132
+ type: 'custom.integration',
133
+ handled: false,
134
+ synthetic: true,
135
+ exception_id: 0,
136
+ })
137
+ expect(entries[1].mechanism).toEqual({
138
+ type: 'chained',
139
+ source: 'cause',
140
+ synthetic: false,
141
+ exception_id: 1,
142
+ parent_id: 0,
143
+ })
144
+ })
145
+ it.each([undefined, '', 123])('keeps a native child without a usable stack non-synthetic: %s', (stack) => {
146
+ const child = error('Error', 'child')
147
+ child.stack = stack as string | undefined
148
+ const entries = builder.buildFromUnknown(new Error('root', { cause: child })).$exception_list
149
+ expect(entries[1].mechanism?.synthetic).toBe(false)
150
+ expect(entries[1].stacktrace).toBeUndefined()
151
+ })
152
+
153
+ it('keeps a native child with a throwing stack accessor non-synthetic', () => {
154
+ const child = error('Error', 'child')
155
+ Object.defineProperty(child, 'stack', {
156
+ get() {
157
+ throw new Error('unreadable')
158
+ },
159
+ })
160
+ const entries = builder.buildFromUnknown(new Error('root', { cause: child })).$exception_list
161
+ expect(entries[1]).toEqual({
162
+ type: 'Error',
163
+ value: 'child',
164
+ mechanism: { type: 'chained', source: 'cause', synthetic: false, exception_id: 1, parent_id: 0 },
165
+ })
166
+ })
167
+
168
+ it.each(['yes', null, 0, {}, []])('omits invalid handled metadata: %j', (handled) => {
169
+ const entries = builder.buildFromUnknown(error('Error', 'root'), {
170
+ mechanism: { type: '', handled, synthetic: true } as any,
171
+ }).$exception_list
172
+ expect(entries[0].mechanism).toEqual({ type: 'generic', synthetic: true, exception_id: 0 })
173
+ expect(entries[0].mechanism).not.toHaveProperty('handled')
174
+ })
175
+
176
+ it.each([undefined, true, false])(
177
+ 'preserves the manual capture default and boolean handled metadata: %s',
178
+ (handled) => {
179
+ const entries = builder.buildFromUnknown(error('Error', 'root'), { mechanism: { handled } }).$exception_list
180
+ expect(entries[0].mechanism.handled).toBe(handled === undefined ? true : handled)
181
+ }
182
+ )
183
+
184
+ it('preserves all supplied mechanism fields in the low-level coercion context', () => {
185
+ const mechanism = {
186
+ type: 'custom',
187
+ handled: false,
188
+ synthetic: true,
189
+ source: 'inner',
190
+ exception_id: 7,
191
+ parent_id: 6,
192
+ }
193
+ const context = builder.buildCoercingContext(mechanism, { mechanism })
194
+ expect(context.mechanism).toEqual(mechanism)
195
+ context.apply(error('Error', 'root'))
196
+ expect(context.mechanism).toEqual(mechanism)
197
+ })
198
+
199
+ it('deduplicates shared exceptions reached through distinct wrappers', () => {
200
+ const shared = error('Error', 'shared')
201
+ const root = new AggregateError([shared, { error: shared }, { error: shared }, error('Error', 'last')], 'root')
202
+ const entries = builder.buildFromUnknown(root).$exception_list
203
+ expect(entries.map((entry) => entry.value)).toEqual(['root', 'shared', 'last'])
204
+ expect(entries.map((entry) => entry.mechanism?.exception_id)).toEqual([0, 1, 2])
205
+ })
206
+
207
+ it('resets the wrapper limit on real cause edges', () => {
208
+ let root = error('Error', '8')
209
+ for (let index = 7; index >= 0; index--) root = error('Error', String(index), { error: root })
210
+ expect(builder.buildFromUnknown(root).$exception_list.map((entry) => entry.value)).toEqual(
211
+ Array.from({ length: 9 }, (_, index) => String(index))
212
+ )
213
+ })
214
+
215
+ it('bounds consecutive forwarding wrappers without discarding the next member', () => {
216
+ let wrapper: unknown = error('Error', 'unreachable')
217
+ for (let index = 0; index < 10; index++) wrapper = { [Symbol.toStringTag]: 'ErrorEvent', error: wrapper }
218
+ const entries = builder.buildFromUnknown(
219
+ new AggregateError([wrapper, error('Error', 'last')], 'root')
220
+ ).$exception_list
221
+ expect(entries.map((entry) => entry.value)).toEqual(['root', 'Unknown error', 'last'])
222
+ expect(entries.map((entry) => entry.mechanism?.exception_id)).toEqual([0, 1, 2])
223
+ })
224
+
225
+ it.each([NaN, Infinity, -1, 1.5, 0x100000000, '2'])('ignores invalid proxy collection lengths: %s', (length) => {
226
+ const root = new AggregateError([], 'root')
227
+ const readMember = vi.fn()
228
+ root.errors = new Proxy([], { get: (_target, key) => (key === 'length' ? length : readMember(key)) })
229
+ expect(builder.buildFromUnknown(root).$exception_list.map((entry) => entry.value)).toEqual(['root'])
230
+ expect(readMember).not.toHaveBeenCalled()
231
+ })
232
+
233
+ it('reads array length once and ignores later growth', () => {
234
+ const members = [error('Error', 'first')]
235
+ const getLength = vi.fn(() => members.length)
236
+ const root = new AggregateError([], 'root')
237
+ root.errors = new Proxy(members, {
238
+ get: (target, key) => {
239
+ if (key === 'length') return getLength()
240
+ if (key === '0') members.push(error('Error', 'not in snapshot'))
241
+ return Reflect.get(target, key)
242
+ },
243
+ })
244
+ expect(builder.buildFromUnknown(root).$exception_list.map((entry) => entry.value)).toEqual(['root', 'first'])
245
+ expect(getLength).toHaveBeenCalledTimes(1)
246
+ })
247
+
248
+ it('preserves root and cause when reading the collection length throws', () => {
249
+ const root = new AggregateError([], 'root', { cause: error('Error', 'cause') })
250
+ root.errors = new Proxy([], {
251
+ get() {
252
+ throw new Error('unreadable length')
253
+ },
254
+ })
255
+ expect(builder.buildFromUnknown(root).$exception_list.map((entry) => entry.value)).toEqual(['root', 'cause'])
256
+ })
257
+ it('caps huge proxy member collections before reading beyond 1000 attempts and resets between captures', () => {
258
+ const root = new AggregateError([], 'root')
259
+ const readMember = vi.fn((index: number) => {
260
+ // The old implementation also terminates safely: after 1000 duplicate reads,
261
+ // fallback entries fill its emission budget rather than scanning the huge length.
262
+ if (index >= 1000) throw new Error('past the inspection budget')
263
+ return root
264
+ })
265
+ root.errors = new Proxy([], {
266
+ get: (_target, key) => (key === 'length' ? 0xffffffff : readMember(Number(key))),
267
+ })
268
+ for (let capture = 0; capture < 2; capture++) {
269
+ readMember.mockClear()
270
+ expect(builder.buildFromUnknown(root).$exception_list.map((entry) => entry.value)).toEqual(['root'])
271
+ expect(readMember).toHaveBeenCalledTimes(1000)
272
+ expect(readMember).toHaveBeenLastCalledWith(999)
273
+ }
274
+ expect(
275
+ builder
276
+ .buildFromUnknown(new AggregateError([error('Error', 'fresh child')], 'fresh root'))
277
+ .$exception_list.map((entry) => entry.value)
278
+ ).toEqual(['fresh root', 'fresh child'])
279
+ })
280
+
281
+ it('shares attempted member inspections across groups, including throwing getters, without limiting cause edges', () => {
282
+ const root = new AggregateError([], 'root')
283
+ const first = new AggregateError(Array(600).fill(root), 'first group')
284
+ Object.defineProperty(first.errors, '0', {
285
+ get() {
286
+ throw new Error('unreadable member')
287
+ },
288
+ })
289
+ let cause = error('Error', 'cause 8')
290
+ for (let index = 7; index >= 0; index--) cause = error('Error', `cause ${index}`, cause)
291
+ const second = new AggregateError([...Array(397).fill(root), cause, error('Error', 'past budget')], 'second group')
292
+ const readLastRootMember = vi.fn(() => error('Error', 'unread root member'))
293
+ root.errors = [first, second]
294
+ Object.defineProperty(root.errors, '2', { get: readLastRootMember })
295
+ const entries = builder.buildFromUnknown(root).$exception_list
296
+ // 1 first-group access + 600 members + 1 second-group access + 398 members.
297
+ expect(entries.map((entry) => entry.value)).toEqual([
298
+ 'root',
299
+ 'first group',
300
+ 'Unknown error',
301
+ 'second group',
302
+ ...Array.from({ length: 9 }, (_, index) => `cause ${index}`),
303
+ ])
304
+ expect(readLastRootMember).not.toHaveBeenCalled()
305
+ entries.forEach((entry, index) => {
306
+ expect(entry.mechanism?.exception_id).toBe(index)
307
+ if (index > 0) {
308
+ expect(entry.mechanism).toEqual({
309
+ type: 'chained',
310
+ source: index > 4 ? 'cause' : 'member',
311
+ synthetic: index === 2,
312
+ exception_id: index,
313
+ parent_id: index === 1 || index === 3 ? 0 : index - 1,
314
+ })
315
+ }
316
+ })
317
+ })
318
+ })
@@ -1,4 +1,4 @@
1
- import { isArray } from '@/utils'
1
+ import { isArray, isError } from '@/utils'
2
2
  import { getFilenameToChunkIdMap } from './chunk-ids'
3
3
  import {
4
4
  ErrorProperties,
@@ -17,7 +17,20 @@ import {
17
17
  Exception,
18
18
  } from './types'
19
19
 
20
- const MAX_CAUSE_RECURSION = 4
20
+ const MAX_EXCEPTIONS = 50
21
+ const MAX_AGGREGATE_MEMBER_INSPECTIONS = 1000
22
+ const MAX_ERROR_PROTOTYPE_DEPTH = 100
23
+ // Forwarding wrappers do not represent tree edges or consume exception slots.
24
+ const MAX_WRAPPER_RECURSION = 4
25
+
26
+ // Internal collections only: the event remains a flat ExceptionList.
27
+ interface ExceptionWithChildren extends ExceptionLike {
28
+ errors?: ExceptionWithChildren[]
29
+ }
30
+
31
+ interface ParsedExceptionWithChildren extends ParsedException {
32
+ errors?: ParsedExceptionWithChildren[]
33
+ }
21
34
 
22
35
  export class ErrorPropertiesBuilder {
23
36
  constructor(
@@ -61,16 +74,20 @@ export class ErrorPropertiesBuilder {
61
74
  }
62
75
  }
63
76
 
64
- private parseStacktrace(err: ExceptionLike, ctx: ParsingContext): ParsedException {
77
+ private parseStacktrace(err: ExceptionWithChildren, ctx: ParsingContext): ParsedExceptionWithChildren {
65
78
  let cause: ParsedException | undefined = undefined
66
79
  if (err.cause != null) {
67
80
  cause = this.parseStacktrace(err.cause, ctx)
68
81
  }
69
82
  let stack: StackFrame[] | undefined = undefined
70
83
  if (err.stack != '' && err.stack != null) {
71
- stack = this.applyChunkIds(this.stackParser(err.stack, err.synthetic ? ctx.skipFirstLines : 0), ctx.chunkIdMap)
84
+ try {
85
+ stack = this.applyChunkIds(this.stackParser(err.stack, err.synthetic ? ctx.skipFirstLines : 0), ctx.chunkIdMap)
86
+ } catch {
87
+ // A malformed stack must not discard this exception or its relatives.
88
+ }
72
89
  }
73
- return { ...err, cause, stack }
90
+ return { ...err, cause, stack, errors: err.errors?.map((child) => this.parseStacktrace(child, ctx)) }
74
91
  }
75
92
 
76
93
  private applyChunkIds(frames: StackFrame[], chunkIdMap?: ChunkIdMapType): StackFrame[] {
@@ -99,32 +116,46 @@ export class ErrorPropertiesBuilder {
99
116
  return newFrames
100
117
  }
101
118
 
102
- private convertToExceptionList(exceptionWithStack: ParsedException, mechanism: Mechanism): ExceptionList {
103
- const currentException: Exception = {
104
- type: exceptionWithStack.type,
105
- value: exceptionWithStack.value,
106
- mechanism: {
107
- type: mechanism.type ?? 'generic',
108
- handled: mechanism.handled ?? true,
109
- synthetic: exceptionWithStack.synthetic ?? false,
110
- },
111
- }
112
- if (exceptionWithStack.stack) {
113
- currentException.stacktrace = {
114
- type: 'raw',
115
- frames: exceptionWithStack.stack,
119
+ private convertToExceptionList(exceptionWithStack: ParsedExceptionWithChildren, mechanism: Mechanism): ExceptionList {
120
+ const exceptionList: ExceptionList = []
121
+ const append = (exception: ParsedExceptionWithChildren, parentId?: number, source?: string): void => {
122
+ const exceptionId = exceptionList.length
123
+ const entryMechanism: Mechanism =
124
+ parentId === undefined
125
+ ? {
126
+ type: typeof mechanism.type === 'string' && mechanism.type.length > 0 ? mechanism.type : 'generic',
127
+ ...(typeof mechanism.handled === 'boolean'
128
+ ? { handled: mechanism.handled }
129
+ : mechanism.handled === undefined
130
+ ? { handled: true }
131
+ : {}),
132
+ synthetic: typeof mechanism.synthetic === 'boolean' ? mechanism.synthetic : exception.synthetic,
133
+ exception_id: exceptionId,
134
+ }
135
+ : {
136
+ type: 'chained',
137
+ source,
138
+ synthetic: exception.synthetic,
139
+ exception_id: exceptionId,
140
+ parent_id: parentId,
141
+ }
142
+ const currentException: Exception = {
143
+ type: exception.type,
144
+ value: exception.value,
145
+ mechanism: entryMechanism,
146
+ }
147
+ if (exception.stack) {
148
+ currentException.stacktrace = { type: 'raw', frames: exception.stack }
149
+ }
150
+ exceptionList.push(currentException)
151
+ if (exception.cause) {
152
+ append(exception.cause, exceptionId, 'cause')
153
+ }
154
+ for (const child of exception.errors ?? []) {
155
+ append(child, exceptionId, 'member')
116
156
  }
117
157
  }
118
- const exceptionList: ExceptionList = [currentException]
119
- if (exceptionWithStack.cause != null) {
120
- // Cause errors are necessarily handled
121
- exceptionList.push(
122
- ...this.convertToExceptionList(exceptionWithStack.cause, {
123
- ...mechanism,
124
- handled: true,
125
- })
126
- )
127
- }
158
+ append(exceptionWithStack)
128
159
  return exceptionList
129
160
  }
130
161
 
@@ -136,29 +167,126 @@ export class ErrorPropertiesBuilder {
136
167
  return context
137
168
  }
138
169
 
170
+ private getAggregateErrors(input: unknown): unknown[] | undefined {
171
+ try {
172
+ if (isError(input)) {
173
+ // AggregateError shares Error's toString tag; instanceof misses other realms.
174
+ let prototype = Object.getPrototypeOf(input)
175
+ for (let depth = 0; prototype && depth < MAX_ERROR_PROTOTYPE_DEPTH; depth++) {
176
+ const constructor = Object.getOwnPropertyDescriptor(prototype, 'constructor')?.value
177
+ if (typeof constructor === 'function' && constructor.name === 'AggregateError') {
178
+ const errors = (input as Error & { errors?: unknown }).errors
179
+ return isArray(errors) ? errors : undefined
180
+ }
181
+ prototype = Object.getPrototypeOf(prototype)
182
+ }
183
+ }
184
+ } catch {
185
+ // Malformed prototypes or accessors must not discard the root exception.
186
+ }
187
+ return undefined
188
+ }
189
+
139
190
  public buildCoercingContext(mechanism: Mechanism, hint: EventHint, depth: number = 0): CoercingContext {
140
- const coerce = (input: unknown, depth: number) => {
141
- if (depth <= MAX_CAUSE_RECURSION) {
142
- const ctx = this.buildCoercingContext(mechanism, hint, depth)
143
- return this.applyCoercers(input, ctx)
144
- } else {
191
+ let count = 0
192
+ let memberInspections = 0
193
+ let hasAggregate = false
194
+ const seen = new Set<unknown>()
195
+ const wrappers: unknown[] = []
196
+ const skipped = {}
197
+ const coerce = (input: unknown, depth: number, wrapperDepth = 0): ExceptionWithChildren | undefined => {
198
+ const ctx = createContext(depth, wrapperDepth)
199
+ const forward = ctx.apply
200
+ ctx.apply = (nextInput) => {
201
+ wrappers.push(input)
202
+ try {
203
+ return forward(nextInput)
204
+ } finally {
205
+ wrappers.pop()
206
+ }
207
+ }
208
+ if (wrapperDepth > MAX_WRAPPER_RECURSION || (wrapperDepth > 0 && wrappers.indexOf(input) !== -1)) {
209
+ return this.coerceFallback(ctx)
210
+ }
211
+ const isReference = (typeof input === 'object' && input !== null) || typeof input === 'function'
212
+ if ((wrapperDepth === 0 && count >= MAX_EXCEPTIONS) || (isReference && seen.has(input))) {
145
213
  return undefined
146
214
  }
215
+ if (isReference) {
216
+ seen.add(input)
217
+ }
218
+ if (wrapperDepth === 0) {
219
+ count++
220
+ }
221
+ const errors = this.getAggregateErrors(input)
222
+ hasAggregate ||= !!errors
223
+ let exception: ExceptionLike | undefined
224
+ try {
225
+ exception = this.applyCoercers(input, ctx)
226
+ if (!exception) {
227
+ throw skipped
228
+ }
229
+ } catch (error) {
230
+ if (error === skipped) {
231
+ if (wrapperDepth === 0) {
232
+ count--
233
+ }
234
+ return undefined
235
+ }
236
+ if (!hasAggregate) {
237
+ throw error
238
+ }
239
+ exception = this.coerceFallback(ctx)
240
+ }
241
+ if (!errors || count >= MAX_EXCEPTIONS) {
242
+ return exception
243
+ }
244
+ // Snapshot finite array length. A separate inspection budget bounds duplicate
245
+ // and cyclic members, which can truncate pathological inputs before 50 entries.
246
+ let length: number
247
+ try {
248
+ length = errors.length
249
+ } catch {
250
+ return exception
251
+ }
252
+ if (!Number.isInteger(length) || length < 0 || length > 0xffffffff) {
253
+ return exception
254
+ }
255
+ const children: ExceptionWithChildren[] = []
256
+ for (
257
+ let index = 0;
258
+ count < MAX_EXCEPTIONS && memberInspections < MAX_AGGREGATE_MEMBER_INSPECTIONS && index < length;
259
+ index++
260
+ ) {
261
+ memberInspections++
262
+ let child: ExceptionWithChildren | undefined
263
+ try {
264
+ child = ctx.next(errors[index])
265
+ } catch {
266
+ count++
267
+ child = this.coerceFallback(createContext(depth + 1))
268
+ }
269
+ if (child) {
270
+ children.push(child)
271
+ }
272
+ }
273
+ return { ...exception, errors: children }
147
274
  }
148
- const context: CoercingContext = {
275
+ const createContext = (depth: number, wrapperDepth = 0): CoercingContext => ({
149
276
  ...hint,
150
- // Do not propagate synthetic exception as it doesn't make sense
277
+ // Capture-boundary metadata and replacement stacks belong only to the root.
151
278
  syntheticException: depth == 0 ? hint.syntheticException : undefined,
152
- mechanism,
153
- // `coerce` only widens to `undefined` past MAX_CAUSE_RECURSION; `apply` reuses this
154
- // context's depth, which was already validated when the context was built
279
+ mechanism: depth == 0 ? mechanism : {},
155
280
  apply: (input: unknown) => {
156
- return coerce(input, depth) as ExceptionLike
157
- },
158
- next: (input: unknown) => {
159
- return coerce(input, depth + 1)
281
+ const exception = coerce(input, depth, wrapperDepth + 1)
282
+ if (!exception) {
283
+ throw skipped
284
+ }
285
+ return exception
160
286
  },
161
- }
162
- return context
287
+ next: (input: unknown) => coerce(input, depth + 1),
288
+ })
289
+ const context = createContext(depth)
290
+ return { ...context, apply: (input) => coerce(input, depth) ?? this.coerceFallback(context) }
163
291
  }
164
292
  }
@@ -44,8 +44,10 @@ export interface Exception {
44
44
  export type ExceptionList = Exception[]
45
45
 
46
46
  export interface Mechanism {
47
+ exception_id?: number
48
+ parent_id?: number
47
49
  handled?: boolean
48
- type?: 'generic' | 'onunhandledrejection' | 'onuncaughtexception' | 'onconsole' | 'middleware'
50
+ type?: string
49
51
  source?: string
50
52
  synthetic?: boolean
51
53
  }
@@ -404,6 +404,15 @@ export abstract class PostHogCoreStateless {
404
404
  abstract getLibraryVersion(): string
405
405
  abstract getCustomUserAgent(): string | void
406
406
 
407
+ /**
408
+ * The runtime this client evaluates flags in, sent as `evaluation_runtime` on `/flags`
409
+ * requests so the server filters by a declared runtime instead of inferring one from
410
+ * request headers. Undefined leaves the server's inference in place.
411
+ */
412
+ protected getEvaluationRuntime(): 'all' | 'client' | 'server' | undefined {
413
+ return undefined
414
+ }
415
+
407
416
  // This is our abstracted storage. Each implementation should handle its own
408
417
  abstract getPersistedProperty<T>(key: PostHogPersistedProperty): T | undefined
409
418
  abstract setPersistedProperty<T>(key: PostHogPersistedProperty, value: T | null): void
@@ -806,6 +815,16 @@ export abstract class PostHogCoreStateless {
806
815
  requestData.evaluation_contexts = this.evaluationContexts
807
816
  }
808
817
 
818
+ // State the runtime explicitly so the server doesn't have to infer it from the request.
819
+ // Without it, `/flags` reads the User-Agent first and falls back to browser headers such as
820
+ // `sec-fetch-mode`, which Node's fetch always sends. An unrecognized User-Agent therefore
821
+ // resolves to the client runtime rather than to "unknown", and `server` flags are dropped.
822
+ // Flags marked `all`, and flags with no runtime, are returned either way.
823
+ const evaluationRuntime = this.getEvaluationRuntime()
824
+ if (evaluationRuntime) {
825
+ requestData.evaluation_runtime = evaluationRuntime
826
+ }
827
+
809
828
  const fetchOptions: PostHogFetchOptions = {
810
829
  method: 'POST',
811
830
  headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
@@ -1935,7 +1954,8 @@ export abstract class PostHogCoreStateless {
1935
1954
  // We only throw on HTTP errors if we're not in no-cors mode.
1936
1955
  // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
1937
1956
  const isNoCors = options.mode === 'no-cors'
1938
- if (!isNoCors && (res.status < 200 || res.status >= 400)) {
1957
+ const maxSuccessStatus = responseHandling.type === 'successful-write' ? 300 : 400
1958
+ if (!isNoCors && (res.status < 200 || res.status >= maxSuccessStatus)) {
1939
1959
  // Read error bodies lazily so retryable statuses are retried immediately. The
1940
1960
  // getter still uses this attempt's deadline when diagnostics request the body.
1941
1961
  throw new PostHogFetchHttpError(res, reqByteLength, requestDeadline, ctrl)
@@ -7,6 +7,7 @@ export * from './bucketed-rate-limiter'
7
7
  // Named rather than `export *`: the budgets, markers and `sanitizeString` are
8
8
  // shared with the OTLP encoder but are not public API.
9
9
  export { toJsonSafeValue } from './json-utils'
10
+ export { parseRetryAfterMs } from './retry-after'
10
11
  export * from './number-utils'
11
12
  export * from './string-utils'
12
13
  export * from './type-utils'
@@ -30,14 +30,19 @@ export function parseRetryAfterMs(value: unknown, now: number = Date.now()): num
30
30
  // a comma of its own ("Wed, 21 Oct 2015 07:28:00 GMT").
31
31
  const trimmed = /^\d+\s*,/.test(raw) ? raw.slice(0, raw.indexOf(',')).trim() : raw
32
32
  // Integer seconds. Not parseFloat: "10 minutes" must not read as 10 seconds.
33
- const seconds = /^\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN
34
- if (!Number.isFinite(seconds) && /^[+-]?[\d.]+$/.test(trimmed)) {
35
- // Numeric but not delta-seconds, so it is malformed. `Date.parse` reads
36
- // "-5", "+5" and "5.5" as dates in 2001 rather than rejecting them, which
37
- // on a device whose clock predates that would surface as a real wait.
33
+ if (/^\d+$/.test(trimmed)) {
34
+ // Cap before multiplication: even a valid integer can overflow to Infinity.
35
+ const ms = Math.min(Number(trimmed), MAX_RETRY_AFTER_MS / 1000) * 1000
36
+ return ms > 0 ? ms : undefined
37
+ }
38
+ // HTTP dates start with a weekday. Do not let Date.parse guess a date from
39
+ // malformed seconds, ISO timestamps or month-first dates.
40
+ if (!/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)[a-z]*[ ,]/.test(trimmed)) {
38
41
  return undefined
39
42
  }
40
- const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(trimmed) - now
43
+ // The obsolete asctime HTTP-date omits the timezone, but is still GMT.
44
+ const date = /^\w{3} \w{3} /.test(trimmed) ? trimmed + ' GMT' : trimmed
45
+ const ms = Date.parse(date) - now
41
46
  if (!Number.isFinite(ms) || ms <= 0) {
42
47
  return undefined
43
48
  }