@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.
Files changed (143) hide show
  1. package/node_modules/@posthog/core/dist/error-tracking/parsers/base.d.ts.map +1 -1
  2. package/node_modules/@posthog/core/dist/error-tracking/parsers/base.js +19 -2
  3. package/node_modules/@posthog/core/dist/error-tracking/parsers/base.mjs +19 -2
  4. package/node_modules/@posthog/core/dist/index.d.ts +7 -0
  5. package/node_modules/@posthog/core/dist/index.d.ts.map +1 -1
  6. package/node_modules/@posthog/core/dist/index.js +82 -21
  7. package/node_modules/@posthog/core/dist/index.mjs +4 -0
  8. package/node_modules/@posthog/core/dist/logs/index.d.ts +3 -2
  9. package/node_modules/@posthog/core/dist/logs/index.d.ts.map +1 -1
  10. package/node_modules/@posthog/core/dist/logs/index.js +28 -22
  11. package/node_modules/@posthog/core/dist/logs/index.mjs +29 -23
  12. package/node_modules/@posthog/core/dist/logs/logs-utils.js +1 -1
  13. package/node_modules/@posthog/core/dist/logs/logs-utils.mjs +2 -2
  14. package/node_modules/@posthog/core/dist/logs/types.d.ts +2 -2
  15. package/node_modules/@posthog/core/dist/metrics/index.d.ts +5 -2
  16. package/node_modules/@posthog/core/dist/metrics/index.d.ts.map +1 -1
  17. package/node_modules/@posthog/core/dist/metrics/index.js +28 -14
  18. package/node_modules/@posthog/core/dist/metrics/index.mjs +29 -15
  19. package/node_modules/@posthog/core/dist/metrics/metrics-utils.d.ts.map +1 -1
  20. package/node_modules/@posthog/core/dist/metrics/metrics-utils.js +1 -2
  21. package/node_modules/@posthog/core/dist/metrics/metrics-utils.mjs +2 -3
  22. package/node_modules/@posthog/core/dist/metrics/types.d.ts +7 -0
  23. package/node_modules/@posthog/core/dist/metrics/types.d.ts.map +1 -1
  24. package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts +32 -4
  25. package/node_modules/@posthog/core/dist/posthog-core-stateless.d.ts.map +1 -1
  26. package/node_modules/@posthog/core/dist/posthog-core-stateless.js +68 -20
  27. package/node_modules/@posthog/core/dist/posthog-core-stateless.mjs +68 -20
  28. package/node_modules/@posthog/core/dist/traces/config.d.ts +13 -0
  29. package/node_modules/@posthog/core/dist/traces/config.d.ts.map +1 -0
  30. package/node_modules/@posthog/core/dist/traces/config.js +101 -0
  31. package/node_modules/@posthog/core/dist/traces/config.mjs +63 -0
  32. package/node_modules/@posthog/core/dist/traces/context.d.ts +18 -0
  33. package/node_modules/@posthog/core/dist/traces/context.d.ts.map +1 -0
  34. package/node_modules/@posthog/core/dist/traces/context.js +53 -0
  35. package/node_modules/@posthog/core/dist/traces/context.mjs +15 -0
  36. package/node_modules/@posthog/core/dist/traces/ids.d.ts +12 -0
  37. package/node_modules/@posthog/core/dist/traces/ids.d.ts.map +1 -0
  38. package/node_modules/@posthog/core/dist/traces/ids.js +92 -0
  39. package/node_modules/@posthog/core/dist/traces/ids.mjs +42 -0
  40. package/node_modules/@posthog/core/dist/traces/index.d.ts +150 -0
  41. package/node_modules/@posthog/core/dist/traces/index.d.ts.map +1 -0
  42. package/node_modules/@posthog/core/dist/traces/index.js +551 -0
  43. package/node_modules/@posthog/core/dist/traces/index.mjs +513 -0
  44. package/node_modules/@posthog/core/dist/traces/otlp.d.ts +24 -0
  45. package/node_modules/@posthog/core/dist/traces/otlp.d.ts.map +1 -0
  46. package/node_modules/@posthog/core/dist/traces/otlp.js +158 -0
  47. package/node_modules/@posthog/core/dist/traces/otlp.mjs +108 -0
  48. package/node_modules/@posthog/core/dist/traces/sanitize.d.ts +32 -0
  49. package/node_modules/@posthog/core/dist/traces/sanitize.d.ts.map +1 -0
  50. package/node_modules/@posthog/core/dist/traces/sanitize.js +93 -0
  51. package/node_modules/@posthog/core/dist/traces/sanitize.mjs +43 -0
  52. package/node_modules/@posthog/core/dist/traces/span.d.ts +189 -0
  53. package/node_modules/@posthog/core/dist/traces/span.d.ts.map +1 -0
  54. package/node_modules/@posthog/core/dist/traces/span.js +533 -0
  55. package/node_modules/@posthog/core/dist/traces/span.mjs +462 -0
  56. package/node_modules/@posthog/core/dist/traces/traceparent.d.ts +51 -0
  57. package/node_modules/@posthog/core/dist/traces/traceparent.d.ts.map +1 -0
  58. package/node_modules/@posthog/core/dist/traces/traceparent.js +116 -0
  59. package/node_modules/@posthog/core/dist/traces/traceparent.mjs +63 -0
  60. package/node_modules/@posthog/core/dist/traces/types.d.ts +114 -0
  61. package/node_modules/@posthog/core/dist/traces/types.d.ts.map +1 -0
  62. package/node_modules/@posthog/core/dist/traces/types.js +18 -0
  63. package/node_modules/@posthog/core/dist/traces/types.mjs +1 -0
  64. package/node_modules/@posthog/core/dist/utils/backoff.d.ts +23 -0
  65. package/node_modules/@posthog/core/dist/utils/backoff.d.ts.map +1 -0
  66. package/node_modules/@posthog/core/dist/utils/backoff.js +65 -0
  67. package/node_modules/@posthog/core/dist/utils/backoff.mjs +14 -0
  68. package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts +26 -0
  69. package/node_modules/@posthog/core/dist/utils/flush-timer.d.ts.map +1 -0
  70. package/node_modules/@posthog/core/dist/utils/flush-timer.js +67 -0
  71. package/node_modules/@posthog/core/dist/utils/flush-timer.mjs +29 -0
  72. package/node_modules/@posthog/core/dist/utils/json-utils.d.ts +7 -0
  73. package/node_modules/@posthog/core/dist/utils/json-utils.d.ts.map +1 -1
  74. package/node_modules/@posthog/core/dist/utils/json-utils.js +27 -0
  75. package/node_modules/@posthog/core/dist/utils/json-utils.mjs +25 -1
  76. package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts +13 -2
  77. package/node_modules/@posthog/core/dist/utils/otlp-resource.d.ts.map +1 -1
  78. package/node_modules/@posthog/core/dist/utils/otlp-resource.js +34 -7
  79. package/node_modules/@posthog/core/dist/utils/otlp-resource.mjs +26 -2
  80. package/node_modules/@posthog/core/dist/utils/retry-after.d.ts +55 -0
  81. package/node_modules/@posthog/core/dist/utils/retry-after.d.ts.map +1 -0
  82. package/node_modules/@posthog/core/dist/utils/retry-after.js +95 -0
  83. package/node_modules/@posthog/core/dist/utils/retry-after.mjs +50 -0
  84. package/node_modules/@posthog/core/package.json +3 -3
  85. package/node_modules/@posthog/core/src/error-tracking/parsers/base.spec.ts +77 -7
  86. package/node_modules/@posthog/core/src/error-tracking/parsers/base.ts +42 -19
  87. package/node_modules/@posthog/core/src/index.ts +18 -0
  88. package/node_modules/@posthog/core/src/logs/index.spec.ts +490 -2
  89. package/node_modules/@posthog/core/src/logs/index.ts +63 -31
  90. package/node_modules/@posthog/core/src/logs/logs-utils.ts +2 -2
  91. package/node_modules/@posthog/core/src/logs/types.ts +2 -2
  92. package/node_modules/@posthog/core/src/metrics/index.spec.ts +304 -0
  93. package/node_modules/@posthog/core/src/metrics/index.ts +53 -16
  94. package/node_modules/@posthog/core/src/metrics/metrics-utils.ts +2 -3
  95. package/node_modules/@posthog/core/src/metrics/types.ts +10 -2
  96. package/node_modules/@posthog/core/src/posthog-core-stateless.ts +157 -38
  97. package/node_modules/@posthog/core/src/traces/config.spec.ts +271 -0
  98. package/node_modules/@posthog/core/src/traces/config.ts +142 -0
  99. package/node_modules/@posthog/core/src/traces/context.ts +30 -0
  100. package/node_modules/@posthog/core/src/traces/ids.spec.ts +115 -0
  101. package/node_modules/@posthog/core/src/traces/ids.ts +75 -0
  102. package/node_modules/@posthog/core/src/traces/index.spec.ts +3187 -0
  103. package/node_modules/@posthog/core/src/traces/index.ts +1060 -0
  104. package/node_modules/@posthog/core/src/traces/live-spans.spec.ts +69 -0
  105. package/node_modules/@posthog/core/src/traces/otlp.spec.ts +368 -0
  106. package/node_modules/@posthog/core/src/traces/otlp.ts +191 -0
  107. package/node_modules/@posthog/core/src/traces/sanitize.ts +116 -0
  108. package/node_modules/@posthog/core/src/traces/span.spec.ts +1122 -0
  109. package/node_modules/@posthog/core/src/traces/span.ts +822 -0
  110. package/node_modules/@posthog/core/src/traces/traceparent.spec.ts +184 -0
  111. package/node_modules/@posthog/core/src/traces/traceparent.ts +172 -0
  112. package/node_modules/@posthog/core/src/traces/types.ts +140 -0
  113. package/node_modules/@posthog/core/src/utils/backoff.ts +44 -0
  114. package/node_modules/@posthog/core/src/utils/flush-timer.ts +50 -0
  115. package/node_modules/@posthog/core/src/utils/json-utils.ts +33 -0
  116. package/node_modules/@posthog/core/src/utils/otlp-resource.spec.ts +55 -9
  117. package/node_modules/@posthog/core/src/utils/otlp-resource.ts +45 -4
  118. package/node_modules/@posthog/core/src/utils/retry-after.ts +135 -0
  119. package/node_modules/@posthog/types/dist/index.d.ts +1 -0
  120. package/node_modules/@posthog/types/dist/index.d.ts.map +1 -1
  121. package/node_modules/@posthog/types/dist/posthog-config.d.ts +10 -5
  122. package/node_modules/@posthog/types/dist/posthog-config.d.ts.map +1 -1
  123. package/node_modules/@posthog/types/dist/traces.d.ts +387 -0
  124. package/node_modules/@posthog/types/dist/traces.d.ts.map +1 -0
  125. package/node_modules/@posthog/types/dist/traces.js +18 -0
  126. package/node_modules/@posthog/types/dist/traces.mjs +1 -0
  127. package/node_modules/@posthog/types/package.json +1 -1
  128. package/node_modules/@posthog/types/src/index.ts +19 -0
  129. package/node_modules/@posthog/types/src/posthog-config.ts +10 -5
  130. package/node_modules/@posthog/types/src/traces.ts +429 -0
  131. package/node_modules/@zuplo/core/package.json +1 -1
  132. package/node_modules/@zuplo/graphql/package.json +1 -1
  133. package/node_modules/@zuplo/openapi-tools/package.json +1 -1
  134. package/node_modules/@zuplo/otel/package.json +1 -1
  135. package/node_modules/@zuplo/runtime/out/esm/{chunk-DRQ5CMVC.js → chunk-KNAYKBEE.js} +80 -80
  136. package/node_modules/@zuplo/runtime/out/esm/chunk-KNAYKBEE.js.map +1 -0
  137. package/node_modules/@zuplo/runtime/out/esm/index.js +1 -1
  138. package/node_modules/@zuplo/runtime/out/esm/mcp-gateway/index.js +1 -1
  139. package/node_modules/@zuplo/runtime/out/esm/mocks/index.js +1 -1
  140. package/node_modules/@zuplo/runtime/package.json +1 -1
  141. package/package.json +11 -11
  142. package/node_modules/@zuplo/runtime/out/esm/chunk-DRQ5CMVC.js.map +0 -1
  143. /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
+ }