@zuplo/cli 7.7.8 → 7.7.9
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/parsers/base.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/parsers/base.js +19 -2
- package/node_modules/@posthog/core/dist/error-tracking/parsers/base.mjs +19 -2
- package/node_modules/@posthog/core/dist/index.d.ts +7 -0
- package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/index.js +82 -21
- package/node_modules/@posthog/core/dist/index.mjs +4 -0
- package/node_modules/@posthog/core/dist/logs/index.d.ts +3 -2
- package/node_modules/@posthog/core/dist/logs/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/logs/index.js +28 -22
- package/node_modules/@posthog/core/dist/logs/index.mjs +29 -23
- package/node_modules/@posthog/core/dist/logs/logs-utils.js +1 -1
- package/node_modules/@posthog/core/dist/logs/logs-utils.mjs +2 -2
- package/node_modules/@posthog/core/dist/logs/types.d.ts +2 -2
- package/node_modules/@posthog/core/dist/metrics/index.d.ts +5 -2
- package/node_modules/@posthog/core/dist/metrics/index.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/metrics/index.js +28 -14
- package/node_modules/@posthog/core/dist/metrics/index.mjs +29 -15
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.js +1 -2
- package/node_modules/@posthog/core/dist/metrics/metrics-utils.mjs +2 -3
- package/node_modules/@posthog/core/dist/metrics/types.d.ts +7 -0
- package/node_modules/@posthog/core/dist/metrics/types.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +32 -4
- package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/posthog-core-stateless.js +68 -20
- package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +68 -20
- package/node_modules/@posthog/core/dist/traces/config.d.ts +13 -0
- package/node_modules/@posthog/core/dist/traces/config.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/config.js +101 -0
- package/node_modules/@posthog/core/dist/traces/config.mjs +63 -0
- package/node_modules/@posthog/core/dist/traces/context.d.ts +18 -0
- package/node_modules/@posthog/core/dist/traces/context.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/context.js +53 -0
- package/node_modules/@posthog/core/dist/traces/context.mjs +15 -0
- package/node_modules/@posthog/core/dist/traces/ids.d.ts +12 -0
- package/node_modules/@posthog/core/dist/traces/ids.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/ids.js +92 -0
- package/node_modules/@posthog/core/dist/traces/ids.mjs +42 -0
- package/node_modules/@posthog/core/dist/traces/index.d.ts +150 -0
- package/node_modules/@posthog/core/dist/traces/index.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/index.js +551 -0
- package/node_modules/@posthog/core/dist/traces/index.mjs +513 -0
- package/node_modules/@posthog/core/dist/traces/otlp.d.ts +24 -0
- package/node_modules/@posthog/core/dist/traces/otlp.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/otlp.js +158 -0
- package/node_modules/@posthog/core/dist/traces/otlp.mjs +108 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.d.ts +32 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.js +93 -0
- package/node_modules/@posthog/core/dist/traces/sanitize.mjs +43 -0
- package/node_modules/@posthog/core/dist/traces/span.d.ts +189 -0
- package/node_modules/@posthog/core/dist/traces/span.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/span.js +533 -0
- package/node_modules/@posthog/core/dist/traces/span.mjs +462 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.d.ts +51 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.js +116 -0
- package/node_modules/@posthog/core/dist/traces/traceparent.mjs +63 -0
- package/node_modules/@posthog/core/dist/traces/types.d.ts +114 -0
- package/node_modules/@posthog/core/dist/traces/types.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/traces/types.js +18 -0
- package/node_modules/@posthog/core/dist/traces/types.mjs +1 -0
- package/node_modules/@posthog/core/dist/utils/backoff.d.ts +23 -0
- package/node_modules/@posthog/core/dist/utils/backoff.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/utils/backoff.js +65 -0
- package/node_modules/@posthog/core/dist/utils/backoff.mjs +14 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts +26 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.js +67 -0
- package/node_modules/@posthog/core/dist/utils/flush-timer.mjs +29 -0
- package/node_modules/@posthog/core/dist/utils/json-utils.d.ts +7 -0
- package/node_modules/@posthog/core/dist/utils/json-utils.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/utils/json-utils.js +27 -0
- package/node_modules/@posthog/core/dist/utils/json-utils.mjs +25 -1
- package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts +13 -2
- package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts.map +1 -1
- package/node_modules/@posthog/core/dist/utils/otlp-resource.js +34 -7
- package/node_modules/@posthog/core/dist/utils/otlp-resource.mjs +26 -2
- package/node_modules/@posthog/core/dist/utils/retry-after.d.ts +55 -0
- package/node_modules/@posthog/core/dist/utils/retry-after.d.ts.map +1 -0
- package/node_modules/@posthog/core/dist/utils/retry-after.js +95 -0
- package/node_modules/@posthog/core/dist/utils/retry-after.mjs +50 -0
- package/node_modules/@posthog/core/package.json +3 -3
- package/node_modules/@posthog/core/src/error-tracking/parsers/base.spec.ts +77 -7
- package/node_modules/@posthog/core/src/error-tracking/parsers/base.ts +42 -19
- package/node_modules/@posthog/core/src/index.ts +18 -0
- package/node_modules/@posthog/core/src/logs/index.spec.ts +490 -2
- package/node_modules/@posthog/core/src/logs/index.ts +63 -31
- package/node_modules/@posthog/core/src/logs/logs-utils.ts +2 -2
- package/node_modules/@posthog/core/src/logs/types.ts +2 -2
- package/node_modules/@posthog/core/src/metrics/index.spec.ts +304 -0
- package/node_modules/@posthog/core/src/metrics/index.ts +53 -16
- package/node_modules/@posthog/core/src/metrics/metrics-utils.ts +2 -3
- package/node_modules/@posthog/core/src/metrics/types.ts +10 -2
- package/node_modules/@posthog/core/src/posthog-core-stateless.ts +157 -38
- package/node_modules/@posthog/core/src/traces/config.spec.ts +271 -0
- package/node_modules/@posthog/core/src/traces/config.ts +142 -0
- package/node_modules/@posthog/core/src/traces/context.ts +30 -0
- package/node_modules/@posthog/core/src/traces/ids.spec.ts +115 -0
- package/node_modules/@posthog/core/src/traces/ids.ts +75 -0
- package/node_modules/@posthog/core/src/traces/index.spec.ts +3187 -0
- package/node_modules/@posthog/core/src/traces/index.ts +1060 -0
- package/node_modules/@posthog/core/src/traces/live-spans.spec.ts +69 -0
- package/node_modules/@posthog/core/src/traces/otlp.spec.ts +368 -0
- package/node_modules/@posthog/core/src/traces/otlp.ts +191 -0
- package/node_modules/@posthog/core/src/traces/sanitize.ts +116 -0
- package/node_modules/@posthog/core/src/traces/span.spec.ts +1122 -0
- package/node_modules/@posthog/core/src/traces/span.ts +822 -0
- package/node_modules/@posthog/core/src/traces/traceparent.spec.ts +184 -0
- package/node_modules/@posthog/core/src/traces/traceparent.ts +172 -0
- package/node_modules/@posthog/core/src/traces/types.ts +140 -0
- package/node_modules/@posthog/core/src/utils/backoff.ts +44 -0
- package/node_modules/@posthog/core/src/utils/flush-timer.ts +50 -0
- package/node_modules/@posthog/core/src/utils/json-utils.ts +33 -0
- package/node_modules/@posthog/core/src/utils/otlp-resource.spec.ts +55 -9
- package/node_modules/@posthog/core/src/utils/otlp-resource.ts +45 -4
- package/node_modules/@posthog/core/src/utils/retry-after.ts +135 -0
- package/node_modules/@posthog/types/dist/index.d.ts +1 -0
- package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/posthog-config.d.ts +10 -5
- package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
- package/node_modules/@posthog/types/dist/traces.d.ts +387 -0
- package/node_modules/@posthog/types/dist/traces.d.ts.map +1 -0
- package/node_modules/@posthog/types/dist/traces.js +18 -0
- package/node_modules/@posthog/types/dist/traces.mjs +1 -0
- package/node_modules/@posthog/types/package.json +1 -1
- package/node_modules/@posthog/types/src/index.ts +19 -0
- package/node_modules/@posthog/types/src/posthog-config.ts +10 -5
- package/node_modules/@posthog/types/src/traces.ts +429 -0
- 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/out/esm/{chunk-DRQ5CMVC.js → chunk-KNAYKBEE.js} +80 -80
- package/node_modules/@zuplo/runtime/out/esm/chunk-KNAYKBEE.js.map +1 -0
- package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
- package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
- package/node_modules/@zuplo/runtime/package.json +1 -1
- package/package.json +11 -11
- package/node_modules/@zuplo/runtime/out/esm/chunk-DRQ5CMVC.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-DRQ5CMVC.js.LEGAL.txt → chunk-KNAYKBEE.js.LEGAL.txt} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { formatTraceparent, normalizeTraceparent, parseTraceparent, sanitizeTracestate } from './traceparent'
|
|
2
|
+
|
|
3
|
+
const TRACE_ID = '4bf92f3577b34da6a3ce929d0e0e4736'
|
|
4
|
+
const SPAN_ID = '00f067aa0ba902b7'
|
|
5
|
+
|
|
6
|
+
describe('traceparent', () => {
|
|
7
|
+
describe('parseTraceparent', () => {
|
|
8
|
+
it('parses a sampled header', () => {
|
|
9
|
+
expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01`)).toEqual({
|
|
10
|
+
traceId: TRACE_ID,
|
|
11
|
+
spanId: SPAN_ID,
|
|
12
|
+
flags: '01',
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('continues the trace even when the caller sampled it out, and keeps the flag', () => {
|
|
17
|
+
// Every captured span is recorded, so honouring an inbound `00` by
|
|
18
|
+
// dropping the parentage would orphan our own spans. The flag itself is
|
|
19
|
+
// kept, so what we propagate onward still says what the caller decided.
|
|
20
|
+
expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID}-00`)).toEqual({
|
|
21
|
+
traceId: TRACE_ID,
|
|
22
|
+
spanId: SPAN_ID,
|
|
23
|
+
flags: '00',
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it.each([
|
|
28
|
+
['a reserved bit alongside sampled', '05', '01'],
|
|
29
|
+
['reserved bits with sampled unset', '04', '00'],
|
|
30
|
+
['every bit set', 'ff', '01'],
|
|
31
|
+
])('zeroes %s, which version 00 does not define', (_label, inbound, expected) => {
|
|
32
|
+
// We re-emit under version `00`, and W3C requires a vendor to zero every
|
|
33
|
+
// flag that version does not define rather than forward it.
|
|
34
|
+
expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID}-${inbound}`)?.flags).toBe(expected)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('accepts a future version with extra fields', () => {
|
|
38
|
+
expect(parseTraceparent(`01-${TRACE_ID}-${SPAN_ID}-01-something`)).toEqual({
|
|
39
|
+
traceId: TRACE_ID,
|
|
40
|
+
spanId: SPAN_ID,
|
|
41
|
+
flags: '01',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('rejects version 00 with extra fields, which only a higher version may carry', () => {
|
|
46
|
+
expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01-something`)).toBeUndefined()
|
|
47
|
+
expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01-`)).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('trims surrounding whitespace', () => {
|
|
51
|
+
expect(parseTraceparent(` 00-${TRACE_ID}-${SPAN_ID}-01 `)).toEqual({
|
|
52
|
+
traceId: TRACE_ID,
|
|
53
|
+
spanId: SPAN_ID,
|
|
54
|
+
flags: '01',
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('rejects uppercase hex, which W3C requires a vendor to ignore', () => {
|
|
59
|
+
expect(parseTraceparent(`00-${TRACE_ID.toUpperCase()}-${SPAN_ID}-01`)).toBeUndefined()
|
|
60
|
+
expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID.toUpperCase()}-01`)).toBeUndefined()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it.each([
|
|
64
|
+
['garbage', 'garbage'],
|
|
65
|
+
['an empty string', ''],
|
|
66
|
+
['version ff', `ff-${TRACE_ID}-${SPAN_ID}-01`],
|
|
67
|
+
['an all-zero trace id', `00-${'0'.repeat(32)}-${SPAN_ID}-01`],
|
|
68
|
+
['an all-zero span id', `00-${TRACE_ID}-${'0'.repeat(16)}-01`],
|
|
69
|
+
['a short trace id', `00-abc-${SPAN_ID}-01`],
|
|
70
|
+
['a missing field', `00-${TRACE_ID}-${SPAN_ID}`],
|
|
71
|
+
['a non-string', 42],
|
|
72
|
+
['undefined', undefined],
|
|
73
|
+
])('returns undefined for %s', (_name, value) => {
|
|
74
|
+
expect(parseTraceparent(value)).toBeUndefined()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('formatTraceparent', () => {
|
|
79
|
+
it('sets the sampled flag on a trace started here', () => {
|
|
80
|
+
expect(formatTraceparent(TRACE_ID, SPAN_ID)).toBe(`00-${TRACE_ID}-${SPAN_ID}-01`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('propagates the flags byte it was given', () => {
|
|
84
|
+
expect(formatTraceparent(TRACE_ID, SPAN_ID, '00')).toBe(`00-${TRACE_ID}-${SPAN_ID}-00`)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('round-trips through the parser', () => {
|
|
88
|
+
expect(parseTraceparent(formatTraceparent(TRACE_ID, SPAN_ID))).toEqual({
|
|
89
|
+
traceId: TRACE_ID,
|
|
90
|
+
spanId: SPAN_ID,
|
|
91
|
+
flags: '01',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('sanitizeTracestate', () => {
|
|
97
|
+
it('preserves a valid vendor list unchanged', () => {
|
|
98
|
+
expect(sanitizeTracestate('vendor=abc,other=def')).toBe('vendor=abc,other=def')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('trims surrounding whitespace', () => {
|
|
102
|
+
expect(sanitizeTracestate(' vendor=abc ')).toBe('vendor=abc')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it.each([
|
|
106
|
+
['an empty string', ''],
|
|
107
|
+
['a member without a value', 'vendor'],
|
|
108
|
+
['a non-string', 42],
|
|
109
|
+
['undefined', undefined],
|
|
110
|
+
[
|
|
111
|
+
'more than 32 members, which the list grammar does not admit',
|
|
112
|
+
Array.from({ length: 33 }, (_v, i) => `k${i}=v`).join(','),
|
|
113
|
+
],
|
|
114
|
+
['a single member longer than the whole limit', `vendor=${'a'.repeat(600)}`],
|
|
115
|
+
])('discards %s', (_name, value) => {
|
|
116
|
+
expect(sanitizeTracestate(value)).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('drops the largest members first when a valid list is too long', () => {
|
|
120
|
+
// W3C names members over 128 characters as the ones to drop first, so the
|
|
121
|
+
// small entries survive even though they sit to the right of the big one.
|
|
122
|
+
const big = `big=${'a'.repeat(200)}`
|
|
123
|
+
const small = Array.from({ length: 4 }, (_v, i) => `k${i}=${'b'.repeat(80)}`)
|
|
124
|
+
|
|
125
|
+
expect(sanitizeTracestate([big, ...small].join(','))).toBe(small.join(','))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('drops from the right once no member is oversized', () => {
|
|
129
|
+
// 102 characters each, so the fifth crosses 512 and the first four stay.
|
|
130
|
+
const members = Array.from({ length: 6 }, (_v, i) => `k${i}=${'a'.repeat(100)}`)
|
|
131
|
+
expect(sanitizeTracestate(members.join(','))).toBe(members.slice(0, 4).join(','))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('leaves a valid header inside the limit exactly as received', () => {
|
|
135
|
+
const members = Array.from({ length: 32 }, (_v, i) => `k${i}=v`)
|
|
136
|
+
expect(sanitizeTracestate(members.join(','))).toBe(members.join(','))
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('tracestate character safety', () => {
|
|
142
|
+
it('discards a value carrying CRLF or a lone surrogate', () => {
|
|
143
|
+
expect(sanitizeTracestate('vendor=abc\r\nx-injected: 1')).toBeUndefined()
|
|
144
|
+
expect(sanitizeTracestate('vendor=\ud800')).toBeUndefined()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('keeps a tab-separated vendor list, which W3C allows', () => {
|
|
148
|
+
expect(sanitizeTracestate('rojo=00f067aa0ba902b7,\tcongo=t61rcWkgMzE')).toBe(
|
|
149
|
+
'rojo=00f067aa0ba902b7,\tcongo=t61rcWkgMzE'
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('keeps an ordinary vendor list', () => {
|
|
154
|
+
expect(sanitizeTracestate('rojo=00f067aa0ba902b7,congo=t61rcWkgMzE')).toBe(
|
|
155
|
+
'rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('normalizeTraceparent', () => {
|
|
161
|
+
it('carries version and flags through as received', () => {
|
|
162
|
+
expect(normalizeTraceparent(`00-${TRACE_ID}-${SPAN_ID}-00`)).toBe(`00-${TRACE_ID}-${SPAN_ID}-00`)
|
|
163
|
+
expect(normalizeTraceparent(`01-${TRACE_ID}-${SPAN_ID}-01`)).toBe(`01-${TRACE_ID}-${SPAN_ID}-01`)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it("keeps a higher version's trailing fields, so a peer that reads them still can", () => {
|
|
167
|
+
expect(normalizeTraceparent(`01-${TRACE_ID}-${SPAN_ID}-01-extra`)).toBe(`01-${TRACE_ID}-${SPAN_ID}-01-extra`)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('trims surrounding whitespace', () => {
|
|
171
|
+
expect(normalizeTraceparent(` 00-${TRACE_ID}-${SPAN_ID}-01 `)).toBe(`00-${TRACE_ID}-${SPAN_ID}-01`)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it.each([
|
|
175
|
+
['a malformed header', 'not-a-traceparent'],
|
|
176
|
+
['the invalid ff version', `ff-${TRACE_ID}-${SPAN_ID}-01`],
|
|
177
|
+
['an all-zero trace id', `00-${'0'.repeat(32)}-${SPAN_ID}-01`],
|
|
178
|
+
['version 00 with trailing fields', `00-${TRACE_ID}-${SPAN_ID}-01-extra`],
|
|
179
|
+
['an uppercase trace id', `00-${TRACE_ID.toUpperCase()}-${SPAN_ID}-01`],
|
|
180
|
+
['a non-string', ['a', 'b']],
|
|
181
|
+
])('rejects %s', (_name, value) => {
|
|
182
|
+
expect(normalizeTraceparent(value)).toBeUndefined()
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { isValidSpanId, isValidTraceId } from './ids'
|
|
2
|
+
|
|
3
|
+
export interface RemoteSpanContext {
|
|
4
|
+
traceId: string
|
|
5
|
+
spanId: string
|
|
6
|
+
/** The inbound trace-flags byte, e.g. `01` sampled, `00` sampled out. */
|
|
7
|
+
flags: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Version `ff` is invalid per the spec, and a higher version may append fields
|
|
11
|
+
// after the first four, so the trailing group captures them rather than failing
|
|
12
|
+
// the match.
|
|
13
|
+
const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses an incoming `traceparent` header value, returning `undefined` for
|
|
17
|
+
* anything malformed so a bad header starts a fresh root rather than throwing.
|
|
18
|
+
*
|
|
19
|
+
* A trace the caller sampled out (`00`) is still continued — PostHog records
|
|
20
|
+
* every captured span — but the inbound flag rides along, so what this SDK
|
|
21
|
+
* propagates onward says what the caller decided rather than overriding it.
|
|
22
|
+
*/
|
|
23
|
+
export function parseTraceparent(value: unknown): RemoteSpanContext | undefined {
|
|
24
|
+
const fields = matchTraceparent(value)
|
|
25
|
+
return fields && { traceId: fields.traceId, spanId: fields.spanId, flags: definedFlags(fields.flags) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keeps only the flags version `00` defines — the sampled bit. A span continuing
|
|
30
|
+
* this trace re-emits the byte under version `00`, and W3C requires a vendor to
|
|
31
|
+
* zero every flag that version does not define rather than forward one it cannot
|
|
32
|
+
* interpret.
|
|
33
|
+
*/
|
|
34
|
+
function definedFlags(flags: string): string {
|
|
35
|
+
return parseInt(flags, 16) & 0x01 ? TRACE_FLAGS_SAMPLED : TRACE_FLAGS_UNSAMPLED
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TraceparentFields {
|
|
39
|
+
version: string
|
|
40
|
+
traceId: string
|
|
41
|
+
spanId: string
|
|
42
|
+
flags: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function matchTraceparent(value: unknown): TraceparentFields | undefined {
|
|
46
|
+
if (typeof value !== 'string') {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
// W3C spells every field lowercase hex and requires a vendor to ignore a
|
|
50
|
+
// `traceparent` whose ids are not, so folding the case here would continue a
|
|
51
|
+
// trace that a conformant peer restarts.
|
|
52
|
+
const match = TRACEPARENT_RE.exec(value.trim())
|
|
53
|
+
if (!match) {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
const [, version, traceId, spanId, flags, trailing] = match
|
|
57
|
+
if (version === 'ff') {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
// Version `00` is defined as exactly `trace-id "-" parent-id "-" trace-flags`.
|
|
61
|
+
// W3C scopes the tolerate-what-you-don't-know rule to a *higher* version, so a
|
|
62
|
+
// version `00` header with anything appended is malformed.
|
|
63
|
+
if (version === '00' && trailing) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
if (!isValidTraceId(traceId) || !isValidSpanId(spanId)) {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
return { version, traceId, spanId, flags }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The inbound `traceparent` as received, or `undefined` when it is malformed.
|
|
74
|
+
*
|
|
75
|
+
* Returned whole rather than rebuilt: a version above `00` may append fields
|
|
76
|
+
* this SDK does not read, and rebuilding would forward a header still labelled
|
|
77
|
+
* with that version but missing what the version defines.
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeTraceparent(value: unknown): string | undefined {
|
|
80
|
+
return matchTraceparent(value) && (value as string).trim()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A `traceparent` as the string it is, unwrapping the one-element array Node's
|
|
85
|
+
* `headersDistinct` hands over. A longer array is two different inbound values,
|
|
86
|
+
* and picking either would be a guess.
|
|
87
|
+
*/
|
|
88
|
+
export function traceparentHeader(value: unknown): unknown {
|
|
89
|
+
return Array.isArray(value) && value.length === 1 ? value[0] : value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The W3C sampled bit, set on a trace this SDK started. */
|
|
93
|
+
export const TRACE_FLAGS_SAMPLED = '01'
|
|
94
|
+
|
|
95
|
+
/** The same byte with the sampled bit clear, for a trace the caller sampled out. */
|
|
96
|
+
const TRACE_FLAGS_UNSAMPLED = '00'
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds the `traceparent` header value for a span. A span continuing a remote
|
|
100
|
+
* trace propagates the flags byte it was handed: a downstream parent-based
|
|
101
|
+
* sampler must see the decision the head sampler actually made, not one this
|
|
102
|
+
* SDK invented. A trace started here is sampled, because it is recorded.
|
|
103
|
+
*/
|
|
104
|
+
export function formatTraceparent(traceId: string, spanId: string, flags: string = TRACE_FLAGS_SAMPLED): string {
|
|
105
|
+
return `00-${traceId}-${spanId}-${flags}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// tracestate is a comma-separated list of at most 32 `key=value` members, and
|
|
109
|
+
// is carried opaquely — we never interpret the vendor entries.
|
|
110
|
+
const TRACESTATE_MAX_MEMBERS = 32
|
|
111
|
+
const TRACESTATE_MAX_LENGTH = 512
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validates an incoming `tracestate` far enough to know it is safe to echo back.
|
|
115
|
+
* A malformed one is discarded without invalidating its traceparent, so a bad
|
|
116
|
+
* vendor entry never costs us the trace continuation. More than 32 members is
|
|
117
|
+
* malformed: W3C's list grammar admits no more.
|
|
118
|
+
*
|
|
119
|
+
* A valid header over the length W3C asks us to propagate is trimmed instead,
|
|
120
|
+
* by whole members. Members over 128 characters go first — W3C names those as
|
|
121
|
+
* the ones to drop — and the rest from the right, so the entries nearest the
|
|
122
|
+
* caller survive.
|
|
123
|
+
*/
|
|
124
|
+
export function sanitizeTracestate(value: unknown): string | undefined {
|
|
125
|
+
if (typeof value !== 'string') {
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
const trimmed = value.trim()
|
|
129
|
+
if (!trimmed) {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
// W3C restricts tracestate to printable ASCII plus HTAB as optional whitespace.
|
|
133
|
+
// A CRLF would make the caller's own propagation throw, and a lone surrogate
|
|
134
|
+
// refuses the whole OTLP request.
|
|
135
|
+
if (/[^\x20-\x7e\t]/.test(trimmed)) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
const members = trimmed.split(',')
|
|
139
|
+
if (members.length > TRACESTATE_MAX_MEMBERS) {
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
for (const member of members) {
|
|
143
|
+
// An empty member is tolerated by the spec (list optional-white-space), but
|
|
144
|
+
// a member without a `=` is not a key/value pair at all.
|
|
145
|
+
if (member.trim() && !member.includes('=')) {
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (trimmed.length <= TRACESTATE_MAX_LENGTH) {
|
|
150
|
+
return trimmed
|
|
151
|
+
}
|
|
152
|
+
return trimToLength(members)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// W3C's own guidance for which members to drop when a list is too long.
|
|
156
|
+
const TRACESTATE_LARGE_MEMBER_LENGTH = 128
|
|
157
|
+
|
|
158
|
+
/** The members that fit, dropping the largest first and then from the right. */
|
|
159
|
+
function trimToLength(members: string[]): string | undefined {
|
|
160
|
+
const kept = [...members]
|
|
161
|
+
const joinedLength = (): number => kept.reduce((total, member) => total + member.length, 0) + kept.length - 1
|
|
162
|
+
|
|
163
|
+
for (let index = kept.length - 1; index >= 0 && joinedLength() > TRACESTATE_MAX_LENGTH; index--) {
|
|
164
|
+
if (kept[index].length > TRACESTATE_LARGE_MEMBER_LENGTH) {
|
|
165
|
+
kept.splice(index, 1)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
while (kept.length && joinedLength() > TRACESTATE_MAX_LENGTH) {
|
|
169
|
+
kept.pop()
|
|
170
|
+
}
|
|
171
|
+
return kept.length ? kept.join(',') : undefined
|
|
172
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Span,
|
|
3
|
+
SpanAttributes,
|
|
4
|
+
SpanAttributeValue,
|
|
5
|
+
SpanKind,
|
|
6
|
+
SpanStatusCode,
|
|
7
|
+
SpanTimeInput,
|
|
8
|
+
StartSpanOptions,
|
|
9
|
+
TracesConfig,
|
|
10
|
+
BeforeSpanSendFn,
|
|
11
|
+
OtlpSpan,
|
|
12
|
+
OtlpSpanEvent,
|
|
13
|
+
OtlpSpanKeyValue,
|
|
14
|
+
OtlpSpanStatus,
|
|
15
|
+
OtlpTracesPayload,
|
|
16
|
+
} from '@posthog/types'
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
BeforeSpanSendFn,
|
|
20
|
+
OtlpTracesPayload,
|
|
21
|
+
Span,
|
|
22
|
+
SpanAttributes,
|
|
23
|
+
SpanKind,
|
|
24
|
+
SpanRecord as HookSpanRecord,
|
|
25
|
+
SpanStatusCode,
|
|
26
|
+
TracesConfig,
|
|
27
|
+
} from '@posthog/types'
|
|
28
|
+
|
|
29
|
+
/** Same tagged outcome shape as `SendLogsBatchOutcome` — one policy for all three signals. */
|
|
30
|
+
export type SendTracesBatchOutcome =
|
|
31
|
+
| { kind: 'ok' }
|
|
32
|
+
| { kind: 'retry-later'; error: unknown; retryAfterMs?: number }
|
|
33
|
+
| {
|
|
34
|
+
kind: 'too-large'
|
|
35
|
+
/**
|
|
36
|
+
* True when the SDK measured the body itself rather than the endpoint
|
|
37
|
+
* refusing it, so the caller can split this drain without lowering the
|
|
38
|
+
* batch size it keeps between them.
|
|
39
|
+
*/
|
|
40
|
+
measuredLocally?: boolean
|
|
41
|
+
}
|
|
42
|
+
| { kind: 'fatal'; error: unknown }
|
|
43
|
+
|
|
44
|
+
/** The minimal host surface `PostHogTraces` depends on; `PostHogCoreStateless` satisfies it structurally. */
|
|
45
|
+
export interface TracesHost {
|
|
46
|
+
readonly isDisabled: boolean
|
|
47
|
+
readonly optedOut: boolean
|
|
48
|
+
_sendTracesBatch(payload: OtlpTracesPayload): Promise<SendTracesBatchOutcome>
|
|
49
|
+
getLibraryId(): string
|
|
50
|
+
getLibraryVersion(): string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* PostHog context snapshotted onto every span at start, so traces join back to
|
|
55
|
+
* persons and sessions. Each SDK fills the fields that apply to it; absent
|
|
56
|
+
* fields add no attribute. Internal to `@posthog/core`.
|
|
57
|
+
*
|
|
58
|
+
* @internal Exposed for cross-package use within this SDK; not part of the stable public API.
|
|
59
|
+
*/
|
|
60
|
+
export interface TraceSdkContext {
|
|
61
|
+
distinctId?: string
|
|
62
|
+
sessionId?: string
|
|
63
|
+
/** Web-only — current page URL. */
|
|
64
|
+
currentUrl?: string
|
|
65
|
+
/** Mobile-only — current screen / view name. */
|
|
66
|
+
screenName?: string
|
|
67
|
+
/** Mobile-only — app foreground/background state. */
|
|
68
|
+
appState?: 'foreground' | 'background'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SpanEventRecord {
|
|
72
|
+
name: string
|
|
73
|
+
/** ms epoch. */
|
|
74
|
+
timestamp: number
|
|
75
|
+
attributes?: SpanAttributes
|
|
76
|
+
/**
|
|
77
|
+
* How many of this event's attributes the cap discarded.
|
|
78
|
+
*
|
|
79
|
+
* Unlike the span-level counts, this one is carried on an object a
|
|
80
|
+
* `beforeSpanSend` hook holds: the public event type omits it, so a hook that
|
|
81
|
+
* rebuilds its events returns them without it. Events have no identity to
|
|
82
|
+
* match a rebuilt array back against, so what a hook drops here stays dropped.
|
|
83
|
+
*/
|
|
84
|
+
droppedAttributesCount?: number
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A finished span as the SDK carries it, which is the hook-visible record plus
|
|
89
|
+
* the fields no hook may rewrite. Declaring only the additions keeps the shared
|
|
90
|
+
* half from drifting; a field added here rather than to the public record is a
|
|
91
|
+
* field `beforeSpanSend` cannot see, and so cannot corrupt.
|
|
92
|
+
*/
|
|
93
|
+
export interface SpanRecord extends HookSpanRecord {
|
|
94
|
+
/** The hook-visible event plus the SDK's own per-event drop count. */
|
|
95
|
+
events: SpanEventRecord[]
|
|
96
|
+
traceState?: string
|
|
97
|
+
/** The W3C trace-flags byte this span propagates, e.g. `01` sampled. */
|
|
98
|
+
traceFlags: string
|
|
99
|
+
/** True when the parent came from a `traceparent` header rather than a local handle. */
|
|
100
|
+
parentIsRemote: boolean
|
|
101
|
+
droppedAttributesCount?: number
|
|
102
|
+
droppedEventsCount?: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Tracks which span is active, so spans nest without manual parent plumbing. The
|
|
107
|
+
* mechanism is platform-specific and stays out of core: node injects an
|
|
108
|
+
* `AsyncLocalStorage` implementation over the synchronous default.
|
|
109
|
+
*/
|
|
110
|
+
export interface SpanContextManager {
|
|
111
|
+
/** The active span, or `undefined` when none is active. */
|
|
112
|
+
active(): Span | undefined
|
|
113
|
+
/** Run `fn` with `span` active for its (synchronous and async) duration. */
|
|
114
|
+
with<T>(span: Span, fn: () => T): T
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fields `PostHogTraces` needs resolved at runtime. The host SDK applies its own
|
|
119
|
+
* defaults and hands the resolved config to the constructor.
|
|
120
|
+
*
|
|
121
|
+
* @internal Exposed for cross-package use within this SDK; not part of the stable public API.
|
|
122
|
+
*/
|
|
123
|
+
export interface ResolvedTracesConfig extends TracesConfig {
|
|
124
|
+
flushIntervalMs: number
|
|
125
|
+
maxExportBatchSize: number
|
|
126
|
+
/**
|
|
127
|
+
* Bound on the in-memory export queue. On overflow the *incoming* span is
|
|
128
|
+
* dropped rather than queued ones, whose children may already have shipped.
|
|
129
|
+
*/
|
|
130
|
+
maxQueueSize: number
|
|
131
|
+
beforeSpanSend: BeforeSpanSendFn[]
|
|
132
|
+
maxAttributesPerSpan: number
|
|
133
|
+
maxEventsPerSpan: number
|
|
134
|
+
maxAttributesPerEvent: number
|
|
135
|
+
maxAttributeValueLength: number
|
|
136
|
+
/** Bound on spans started but not yet ended. At the bound `startSpan` returns a no-op handle. */
|
|
137
|
+
maxLiveSpans: number
|
|
138
|
+
/** How long a span may stay live before it stops being accounted for and can never export. */
|
|
139
|
+
maxSpanAgeMs: number
|
|
140
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Doublings the retry delay may grow by before it stops growing. */
|
|
2
|
+
export const MAX_FLUSH_BACKOFF_EXPONENT = 6
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ceiling on the SDK's own retry delay, which the logs and traces contracts
|
|
6
|
+
* both state as "exponential backoff capped at ~30s". A host that configured a
|
|
7
|
+
* longer flush interval keeps it: the cap is there to stop the doubling running
|
|
8
|
+
* away, not to flush more often than asked.
|
|
9
|
+
*/
|
|
10
|
+
export const MAX_FLUSH_BACKOFF_MS = 30_000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How far a delay may be moved either side of its computed value. Clients that
|
|
14
|
+
* fail together otherwise retry together, and arrive at the endpoint as one
|
|
15
|
+
* burst each time it comes back — which is what OTel asks jitter to prevent.
|
|
16
|
+
*
|
|
17
|
+
* A quarter is enough to spread a fleet without a retry landing so early that
|
|
18
|
+
* it beats the interval the host configured, or so late that a recovered
|
|
19
|
+
* endpoint sits idle.
|
|
20
|
+
*/
|
|
21
|
+
const JITTER = 0.25
|
|
22
|
+
|
|
23
|
+
/** A multiplier in `[1 - JITTER, 1 + JITTER]`, drawn once per failure by the caller. */
|
|
24
|
+
export function drawJitter(): number {
|
|
25
|
+
return 1 - JITTER + Math.random() * JITTER * 2
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** No jitter, for the delay a queue uses when nothing has failed. */
|
|
29
|
+
export const NO_JITTER = 1
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The delay before retrying an export, `baseMs` doubled once per failure past
|
|
33
|
+
* the first and capped at `maxMs`.
|
|
34
|
+
*
|
|
35
|
+
* `jitter` is applied to the SDK's own delay only. A `Retry-After` the endpoint
|
|
36
|
+
* sent is a floor underneath it, applied by the caller: spreading a fleet must
|
|
37
|
+
* never move a retry earlier than the endpoint asked for.
|
|
38
|
+
*/
|
|
39
|
+
export function backoffDelayMs(baseMs: number, failures: number, jitter: number, maxMs?: number): number {
|
|
40
|
+
const exponent = Math.min(Math.max(0, failures - 1), MAX_FLUSH_BACKOFF_EXPONENT)
|
|
41
|
+
const delay = baseMs * 2 ** exponent
|
|
42
|
+
const capped = maxMs === undefined ? delay : Math.min(delay, Math.max(maxMs, baseMs))
|
|
43
|
+
return Math.round(capped * jitter)
|
|
44
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { safeSetTimeout } from './index'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pending flush timer of an export queue, and when it is due.
|
|
5
|
+
*
|
|
6
|
+
* The queues arm this from two kinds of place: one that must not push a pending
|
|
7
|
+
* flush further out (every capture reaches it), and one that must not pull a
|
|
8
|
+
* flush back in front of a wait the endpoint asked for. Holding the deadline
|
|
9
|
+
* next to the handle is what lets the second kind compare against the first.
|
|
10
|
+
*/
|
|
11
|
+
export class FlushTimer {
|
|
12
|
+
private _timer?: ReturnType<typeof safeSetTimeout>
|
|
13
|
+
private _firesAt = 0
|
|
14
|
+
|
|
15
|
+
/** @param _onFire runs when the timer elapses, after the handle is released. */
|
|
16
|
+
constructor(private readonly _onFire: () => void) {}
|
|
17
|
+
|
|
18
|
+
/** Whether a flush is already scheduled. */
|
|
19
|
+
get pending(): boolean {
|
|
20
|
+
return !!this._timer
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Schedules a flush in `delayMs`, replacing any timer already pending. */
|
|
24
|
+
arm(delayMs: number): void {
|
|
25
|
+
this.clear()
|
|
26
|
+
this._firesAt = Date.now() + delayMs
|
|
27
|
+
this._timer = safeSetTimeout(() => {
|
|
28
|
+
this._timer = undefined
|
|
29
|
+
this._onFire()
|
|
30
|
+
}, delayMs)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Arms only if it moves the flush later, so a timer armed for a longer wait
|
|
35
|
+
* survives a caller asking for a shorter one.
|
|
36
|
+
*/
|
|
37
|
+
armNoEarlierThan(delayMs: number): void {
|
|
38
|
+
if (this._timer && Date.now() + delayMs <= this._firesAt) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
this.arm(delayMs)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
if (this._timer) {
|
|
46
|
+
clearTimeout(this._timer)
|
|
47
|
+
this._timer = undefined
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -153,3 +153,36 @@ export function toJsonSafeValue(value: unknown): unknown {
|
|
|
153
153
|
|
|
154
154
|
return convert(value, 0)
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Copies caller-supplied attributes onto `target`, own enumerable keys only.
|
|
159
|
+
*
|
|
160
|
+
* Read key by key rather than spread: a getter over a disposed resource or a
|
|
161
|
+
* revoked proxy throws on the read itself, before the encoder's guards see it.
|
|
162
|
+
*/
|
|
163
|
+
export function assignUserAttributes<T extends Record<string, any>>(
|
|
164
|
+
target: T,
|
|
165
|
+
source: Record<string, unknown> | undefined
|
|
166
|
+
): T {
|
|
167
|
+
if (!source) {
|
|
168
|
+
return target
|
|
169
|
+
}
|
|
170
|
+
let keys: string[] = []
|
|
171
|
+
try {
|
|
172
|
+
keys = Object.keys(source)
|
|
173
|
+
} catch {
|
|
174
|
+
keys = []
|
|
175
|
+
}
|
|
176
|
+
for (const key of keys) {
|
|
177
|
+
let value: unknown
|
|
178
|
+
try {
|
|
179
|
+
value = source[key]
|
|
180
|
+
} catch {
|
|
181
|
+
value = UNSERIALIZABLE_VALUE
|
|
182
|
+
}
|
|
183
|
+
// defineProperty, not assignment: `attributes['__proto__'] = v` hits the
|
|
184
|
+
// prototype setter and the attribute vanishes.
|
|
185
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
|
|
186
|
+
}
|
|
187
|
+
return target
|
|
188
|
+
}
|