@zuplo/cli 7.7.11 → 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.
- package/node_modules/@posthog/core/dist/error-tracking/coercers/error-coercer.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/coercers/error-coercer.js +11 -3
- package/node_modules/@posthog/core/dist/error-tracking/coercers/error-coercer.mjs +11 -3
- package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.d.ts +1 -0
- package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.js +130 -34
- package/node_modules/@posthog/core/dist/error-tracking/error-properties-builder.mjs +131 -35
- package/node_modules/@posthog/core/dist/error-tracking/types.d.ts +3 -1
- package/node_modules/@posthog/core/dist/error-tracking/types.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +6 -0
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.js +3 -0
- package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +3 -0
- package/node_modules/@posthog/core/package.json +1 -1
- package/node_modules/@posthog/core/src/error-tracking/coercers/error-coercer.ts +13 -3
- package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.aggregate-errors.spec.ts +412 -0
- package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.metadata.spec.ts +318 -0
- package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.ts +173 -45
- package/node_modules/@posthog/core/src/error-tracking/types.ts +3 -1
- package/node_modules/@posthog/core/src/posthog-core-stateless.ts +19 -0
- package/node_modules/@posthog/types/dist/posthog-config.d.ts +2 -1
- package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/posthog.d.ts +1 -1
- package/node_modules/@posthog/types/package.json +1 -1
- package/node_modules/@posthog/types/src/posthog-config.ts +2 -1
- package/node_modules/@posthog/types/src/posthog.ts +1 -1
- package/node_modules/@zuplo/core/package.json +1 -1
- package/node_modules/@zuplo/graphql/package.json +1 -1
- package/node_modules/@zuplo/openapi-tools/package.json +1 -1
- package/node_modules/@zuplo/otel/package.json +1 -1
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/node_modules/brace-expansion/dist/commonjs/index.d.ts +4 -0
- package/node_modules/brace-expansion/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/brace-expansion/dist/commonjs/index.js +69 -25
- package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
- package/node_modules/brace-expansion/dist/esm/index.d.ts +4 -0
- package/node_modules/brace-expansion/dist/esm/index.d.ts.map +1 -1
- package/node_modules/brace-expansion/dist/esm/index.js +68 -24
- package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
- package/node_modules/brace-expansion/package.json +2 -1
- package/node_modules/fast-uri/index.js +10 -6
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/security-normalization.test.js +38 -0
- package/node_modules/hono/README.md +1 -1
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +5 -4
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +5 -4
- package/node_modules/hono/dist/cjs/context.js +1 -1
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +1 -1
- package/node_modules/hono/dist/cjs/jsx/dom/render.js +44 -8
- package/node_modules/hono/dist/cjs/middleware/language/language.js +4 -1
- package/node_modules/hono/dist/cjs/request.js +4 -1
- package/node_modules/hono/dist/cjs/utils/accept.js +4 -7
- package/node_modules/hono/dist/context.js +1 -1
- package/node_modules/hono/dist/helper/accepts/accepts.js +1 -1
- package/node_modules/hono/dist/jsx/dom/render.js +44 -8
- package/node_modules/hono/dist/middleware/language/language.js +4 -1
- package/node_modules/hono/dist/request.js +4 -1
- package/node_modules/hono/dist/types/helper/proxy/index.d.ts +1 -1
- package/node_modules/hono/dist/types/helper/route/index.d.ts +2 -2
- package/node_modules/hono/dist/types/middleware/combine/index.d.ts +3 -1
- package/node_modules/hono/dist/types/utils/types.d.ts +1 -1
- package/node_modules/hono/dist/utils/accept.js +4 -7
- package/node_modules/hono/package.json +1 -1
- package/node_modules/ip-address/README.md +114 -114
- package/node_modules/ip-address/dist/common.d.ts +5 -0
- package/node_modules/ip-address/dist/common.js +12 -0
- package/node_modules/ip-address/dist/common.js.map +1 -1
- package/node_modules/ip-address/dist/ipv4.d.ts +5 -1
- package/node_modules/ip-address/dist/ipv4.js +12 -1
- package/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/ip-address/dist/ipv6.d.ts +5 -1
- package/node_modules/ip-address/dist/ipv6.js +15 -1
- package/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/ip-address/package.json +1 -1
- package/node_modules/proxy-addr/HISTORY.md +60 -54
- package/node_modules/proxy-addr/README.md +1 -1
- package/node_modules/proxy-addr/index.js +32 -0
- package/node_modules/proxy-addr/package.json +8 -4
- package/package.json +11 -11
package/node_modules/@posthog/core/src/error-tracking/error-properties-builder.metadata.spec.ts
ADDED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
275
|
+
const createContext = (depth: number, wrapperDepth = 0): CoercingContext => ({
|
|
149
276
|
...hint,
|
|
150
|
-
//
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
281
|
+
const exception = coerce(input, depth, wrapperDepth + 1)
|
|
282
|
+
if (!exception) {
|
|
283
|
+
throw skipped
|
|
284
|
+
}
|
|
285
|
+
return exception
|
|
160
286
|
},
|
|
161
|
-
|
|
162
|
-
|
|
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?:
|
|
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' },
|
|
@@ -1464,7 +1464,8 @@ export interface PostHogConfig {
|
|
|
1464
1464
|
*/
|
|
1465
1465
|
identity_distinct_id?: string;
|
|
1466
1466
|
/**
|
|
1467
|
-
* HMAC-SHA256 of `identity_distinct_id
|
|
1467
|
+
* HMAC-SHA256 of `identity_distinct_id`, signed with the Secret API key from Support settings.
|
|
1468
|
+
* Project secret API keys (project settings) and personal API keys are rejected.
|
|
1468
1469
|
* Must be provided together with `identity_distinct_id`.
|
|
1469
1470
|
*/
|
|
1470
1471
|
identity_hash?: string;
|