@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,822 @@
1
+ import type { Span, SpanAttributes, SpanAttributeValue, SpanKind, SpanStatusCode, SpanTimeInput } from '@posthog/types'
2
+ import type { Logger } from '../types'
3
+ import type { SpanContextManager, SpanEventRecord, SpanRecord } from './types'
4
+ import {
5
+ formatTraceparent,
6
+ normalizeTraceparent,
7
+ sanitizeTracestate,
8
+ traceparentHeader,
9
+ TRACE_FLAGS_SAMPLED,
10
+ } from './traceparent'
11
+ import { clampEndTime, resolveSuppliedTime, sanitizeName } from './sanitize'
12
+ import { isArray, isError, isNullish } from '../utils'
13
+ import {
14
+ CIRCULAR_VALUE,
15
+ MAX_JSON_SAFE_VALUE_DEPTH,
16
+ MAX_JSON_SAFE_VALUE_ITEMS,
17
+ MAX_JSON_SAFE_VALUE_NODES,
18
+ UNSERIALIZABLE_VALUE,
19
+ assignUserAttributes,
20
+ } from '../utils/json-utils'
21
+
22
+ /**
23
+ * A monotonic millisecond reading where the platform has one, so an NTP
24
+ * correction mid-span can't produce a negative duration.
25
+ */
26
+ export function monotonicNow(): number | undefined {
27
+ const perf = (globalThis as { performance?: { now?: () => number } }).performance
28
+ return typeof perf?.now === 'function' ? perf.now() : undefined
29
+ }
30
+
31
+ export interface SpanInit {
32
+ traceId: string
33
+ spanId: string
34
+ parentSpanId?: string
35
+ traceState?: string
36
+ /** The trace-flags byte to propagate; the inbound one when continuing a remote trace. */
37
+ traceFlags?: string
38
+ /** True when the parent came from a `traceparent` header rather than a local handle. */
39
+ parentIsRemote?: boolean
40
+ name: string
41
+ kind: SpanKind
42
+ attributes: SpanAttributes
43
+ /** ms epoch. */
44
+ startTime: number
45
+ /** True when the caller supplied an explicit `startTime`. */
46
+ backdated: boolean
47
+ /** Keys the SDK attached itself. Exempt from the attribute cap and never evicted. */
48
+ autoAttributeKeys: string[]
49
+ maxAttributes: number
50
+ maxEvents: number
51
+ maxAttributesPerEvent: number
52
+ maxAttributeValueLength: number
53
+ }
54
+
55
+ export class PostHogSpan implements Span {
56
+ private readonly _traceId: string
57
+ private readonly _spanId: string
58
+ private readonly _parentSpanId?: string
59
+ private readonly _traceState?: string
60
+ private readonly _traceFlags: string
61
+ private readonly _parentIsRemote: boolean
62
+ private readonly _startTime: number
63
+ // Absent on backdated spans and on platforms with no monotonic source.
64
+ private readonly _startMono?: number
65
+
66
+ private _name: string
67
+ private _kind: SpanKind
68
+ private _attributes: SpanAttributes
69
+ private _events: SpanEventRecord[] = []
70
+ private _status?: { code: SpanStatusCode; message?: string }
71
+ private _ended = false
72
+ private readonly _autoKeys: Set<string>
73
+ private readonly _maxAttributes: number
74
+ private readonly _maxEvents: number
75
+ private readonly _maxAttributesPerEvent: number
76
+ private readonly _maxAttributeValueLength: number
77
+ private _userAttributeCount = 0
78
+ private _userEventCount = 0
79
+ private _droppedAttributes = 0
80
+ private _droppedEvents = 0
81
+
82
+ constructor(
83
+ init: SpanInit,
84
+ private readonly _onEnd: (record: SpanRecord, autoKeys: ReadonlySet<string>) => void,
85
+ private readonly _logger?: Logger
86
+ ) {
87
+ this._traceId = init.traceId
88
+ this._spanId = init.spanId
89
+ this._parentSpanId = init.parentSpanId
90
+ this._traceState = init.traceState
91
+ this._traceFlags = init.traceFlags ?? TRACE_FLAGS_SAMPLED
92
+ this._parentIsRemote = init.parentIsRemote ?? false
93
+ this._name = init.name
94
+ this._kind = init.kind
95
+ this._autoKeys = new Set(init.autoAttributeKeys)
96
+ this._maxAttributes = init.maxAttributes
97
+ this._maxEvents = init.maxEvents
98
+ this._maxAttributesPerEvent = init.maxAttributesPerEvent
99
+ this._maxAttributeValueLength = init.maxAttributeValueLength
100
+ // Null-prototype: a `__proto__` key would otherwise swap this object's prototype
101
+ // instead of becoming an entry, and `toString` and friends would read as
102
+ // already-present.
103
+ this._attributes = Object.create(null) as SpanAttributes
104
+ // Object.keys, not for...in: the latter walks the prototype chain, so a
105
+ // polluted `Object.prototype` key would become an attribute of every span.
106
+ for (const key of Object.keys(init.attributes)) {
107
+ this._writeAttribute(key, init.attributes[key])
108
+ }
109
+ this._startTime = init.startTime
110
+ this._startMono = init.backdated ? undefined : monotonicNow()
111
+ }
112
+
113
+ /**
114
+ * "Now" on this span's clock basis: start plus monotonic elapsed where we
115
+ * have it, wall clock otherwise.
116
+ */
117
+ private _now(): number {
118
+ if (this._startMono !== undefined) {
119
+ const mono = monotonicNow()
120
+ if (mono !== undefined) {
121
+ return this._startTime + Math.max(0, mono - this._startMono)
122
+ }
123
+ }
124
+ return Date.now()
125
+ }
126
+
127
+ /** Guards every mutator: operations after `end()` no-op with a debug warning. */
128
+ private _mutable(operation: string): boolean {
129
+ if (this._ended) {
130
+ this._logger?.debug(`Ignoring ${operation} on a span that has already ended`)
131
+ return false
132
+ }
133
+ return true
134
+ }
135
+
136
+ /**
137
+ * Writes an attribute unless the span is already at its user-attribute cap.
138
+ *
139
+ * Overwriting a key already on the span always succeeds — the cap counts
140
+ * distinct user keys, not writes — and SDK-attached keys never count toward
141
+ * it, so a span at the cap still carries its person and session ids.
142
+ */
143
+ private _writeAttribute(key: string, value: SpanAttributeValue): void {
144
+ // Nullish removes the key rather than occupying it: storing one would spend no
145
+ // budget and make every later write to that key free, exceeding the cap.
146
+ if (isNullish(value)) {
147
+ if (key in this._attributes && !this._autoKeys.has(key)) {
148
+ this._userAttributeCount--
149
+ }
150
+ delete this._attributes[key]
151
+ return
152
+ }
153
+ // The cap is checked before the value is bounded: walking a value the span is
154
+ // about to drop is the dominant cost of a span that overflows its cap.
155
+ if (!this._autoKeys.has(key) && !(key in this._attributes)) {
156
+ if (this._userAttributeCount >= this._maxAttributes) {
157
+ this._droppedAttributes++
158
+ return
159
+ }
160
+ this._userAttributeCount++
161
+ }
162
+ this._attributes[key] = truncateAttributeValue(value, this._maxAttributeValueLength)
163
+ }
164
+
165
+ setAttribute(key: string, value: SpanAttributeValue): this {
166
+ if (this._mutable('setAttribute')) {
167
+ this._writeAttribute(key, value)
168
+ }
169
+ return this
170
+ }
171
+
172
+ setAttributes(attributes: SpanAttributes): this {
173
+ if (this._mutable('setAttributes')) {
174
+ // Read through the shared guard first — own enumerable keys only, and a
175
+ // throwing getter costs its own key — then write each through the cap.
176
+ const safe: SpanAttributes = assignUserAttributes({}, attributes)
177
+ for (const key of Object.keys(safe)) {
178
+ this._writeAttribute(key, safe[key])
179
+ }
180
+ }
181
+ return this
182
+ }
183
+
184
+ addEvent(name: string, attributes?: SpanAttributes, timestamp?: SpanTimeInput): this {
185
+ if (this._mutable('addEvent')) {
186
+ // An exception the SDK records spends an ordinary slot like any other
187
+ // event. A span that fills its events and then throws therefore keeps its
188
+ // `error` status but loses the exception detail, which `droppedEventsCount`
189
+ // reports — enough to find the case in production if it turns out to occur.
190
+ if (this._userEventCount >= this._maxEvents) {
191
+ this._droppedEvents++
192
+ return this
193
+ }
194
+ this._userEventCount++
195
+ // Copied so a caller reusing one object across events can't mutate a recorded one.
196
+ const bounded =
197
+ attributes && boundAttributes(attributes, this._maxAttributesPerEvent, this._maxAttributeValueLength)
198
+ this._events.push({
199
+ name: sanitizeName(name, 'Span event name', this._maxAttributeValueLength, this._logger),
200
+ timestamp: resolveSuppliedTime(timestamp, this._now(), 'event timestamp', this._logger),
201
+ ...(bounded && {
202
+ attributes: bounded.attributes,
203
+ ...(bounded.dropped && { droppedAttributesCount: bounded.dropped }),
204
+ }),
205
+ })
206
+ }
207
+ return this
208
+ }
209
+
210
+ setStatus(status: SpanStatusCode, message?: string): this {
211
+ if (this._mutable('setStatus')) {
212
+ if (status !== 'ok' && status !== 'error') {
213
+ this._logger?.debug(`Ignoring unknown span status "${String(status)}"; expected "ok" or "error"`)
214
+ return this
215
+ }
216
+ // Bounded like an attribute value: a status message is one more string the
217
+ // caller controls, and one large enough takes the span past the body limit.
218
+ this._status = {
219
+ code: status,
220
+ ...(message && { message: truncateString(message, this._maxAttributeValueLength) }),
221
+ }
222
+ }
223
+ return this
224
+ }
225
+
226
+ /** True when the caller explicitly marked the span `ok`; `withSpan` treats that as final. */
227
+ get statusIsExplicitlyOk(): boolean {
228
+ return this._status?.code === 'ok'
229
+ }
230
+
231
+ recordException(error: unknown): this {
232
+ if (!this._mutable('recordException')) {
233
+ return this
234
+ }
235
+ const { type, message, stack } = describeError(error)
236
+ this.addEvent(EXCEPTION_EVENT_NAME, {
237
+ 'exception.type': type,
238
+ 'exception.message': message,
239
+ ...(stack && { 'exception.stacktrace': stack }),
240
+ })
241
+ // recordException is itself an explicit call, so it follows last-write-wins
242
+ // rather than deferring to an earlier `ok`.
243
+ return this.setStatus('error', message)
244
+ }
245
+
246
+ updateName(name: string): this {
247
+ if (this._mutable('updateName')) {
248
+ this._name = sanitizeName(name, 'Span name', this._maxAttributeValueLength, this._logger)
249
+ }
250
+ return this
251
+ }
252
+
253
+ traceparent(): string | null {
254
+ return formatTraceparent(this._traceId, this._spanId, this._traceFlags)
255
+ }
256
+
257
+ tracestate(): string | null {
258
+ return this._traceState ?? null
259
+ }
260
+
261
+ /** Context a child span inherits when this handle is its parent. */
262
+ childContext(): { traceId: string; parentSpanId: string; traceState?: string; traceFlags: string } {
263
+ return {
264
+ traceId: this._traceId,
265
+ parentSpanId: this._spanId,
266
+ traceState: this._traceState,
267
+ // A child of a continued trace keeps propagating the caller's decision.
268
+ traceFlags: this._traceFlags,
269
+ }
270
+ }
271
+
272
+ end(endTime?: SpanTimeInput): void {
273
+ if (this._ended) {
274
+ this._logger?.debug('Ignoring end() on a span that has already ended')
275
+ return
276
+ }
277
+ this._ended = true
278
+
279
+ const derived = this._now()
280
+ const resolved = resolveSuppliedTime(endTime, derived, 'end time', this._logger)
281
+
282
+ this._onEnd(
283
+ {
284
+ traceId: this._traceId,
285
+ spanId: this._spanId,
286
+ ...(this._parentSpanId && { parentSpanId: this._parentSpanId }),
287
+ ...(this._traceState && { traceState: this._traceState }),
288
+ traceFlags: this._traceFlags,
289
+ parentIsRemote: this._parentIsRemote,
290
+ name: this._name,
291
+ kind: this._kind,
292
+ ...(this._status && { status: this._status }),
293
+ // Copied out with an ordinary prototype: the store is null-prototype, but a
294
+ // record handed to user code should behave like a normal object.
295
+ attributes: { ...this._attributes },
296
+ events: this._events,
297
+ startTime: this._startTime,
298
+ endTime: clampEndTime(resolved, this._startTime),
299
+ ...(this._droppedAttributes && { droppedAttributesCount: this._droppedAttributes }),
300
+ ...(this._droppedEvents && { droppedEventsCount: this._droppedEvents }),
301
+ },
302
+ this._autoKeys
303
+ )
304
+ }
305
+ }
306
+
307
+ const EXCEPTION_EVENT_NAME = 'exception'
308
+
309
+ /** The widest value the OTLP `dropped_*_count` fields, declared `uint32`, can carry. */
310
+ const MAX_UINT32 = 0xffff_ffff
311
+
312
+ /** A value as its string form, or the encoder's marker when it refuses to produce one. */
313
+ function safeString(value: unknown): string {
314
+ try {
315
+ return typeof value === 'string' ? value : String(value)
316
+ } catch {
317
+ return UNSERIALIZABLE_VALUE
318
+ }
319
+ }
320
+
321
+ /**
322
+ * A caller-visible counter read back as a number, or 0 for anything else.
323
+ * Clamped to the `uint32` the OTLP field is declared as: a `beforeSpanSend` hook
324
+ * can write a larger number onto an event, and one that overflows the field is
325
+ * refused for the whole request.
326
+ */
327
+ export function nonNegativeCount(value: unknown): number {
328
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
329
+ return 0
330
+ }
331
+ return Math.min(Math.floor(value), MAX_UINT32)
332
+ }
333
+
334
+ /**
335
+ * The record's keys with the ones the span itself set first, in that order.
336
+ *
337
+ * `Object.keys` hoists integer-like keys to the front whatever the write order,
338
+ * so a hook adding `attributes['0']` would otherwise outrank an attribute the
339
+ * caller set before the hook ran — and the cap is documented as earliest-set-wins.
340
+ */
341
+ function orderedKeys(attributes: SpanAttributes, keysBeforeHook: readonly string[]): string[] {
342
+ if (!keysBeforeHook.length) {
343
+ return Object.keys(attributes)
344
+ }
345
+ // The encoder's own predicate: `in` would walk the prototype chain, so a key
346
+ // the caller set that collides with Object.prototype survives the hook deleting
347
+ // it and reads back as the inherited member, and `hasOwnProperty` would keep a
348
+ // key the hook hid by making it non-enumerable, which the encoder never emits.
349
+ const beforeHook = keysBeforeHook.filter((key) => Object.prototype.propertyIsEnumerable.call(attributes, key))
350
+ const seen = new Set(beforeHook)
351
+ return [...beforeHook, ...Object.keys(attributes).filter((key) => !seen.has(key))]
352
+ }
353
+
354
+ /**
355
+ * Re-applies the per-span caps to a record a `beforeSpanSend` hook has already
356
+ * seen. The hook writes to the plain record, not through the span's own guarded
357
+ * writer, so an enriching hook would otherwise push a span past the cap it was
358
+ * trimmed to and back into the 413 path the cap exists to avoid.
359
+ *
360
+ * Earliest-set entries win, matching the span-side rule; SDK-attached keys are
361
+ * exempt. Counts add to whatever the span already dropped.
362
+ */
363
+ export function applySpanLimits(
364
+ record: SpanRecord,
365
+ autoKeys: ReadonlySet<string>,
366
+ maxAttributes: number,
367
+ maxEvents: number,
368
+ maxAttributesPerEvent: number,
369
+ maxAttributeValueLength: number,
370
+ keysBeforeHook: readonly string[] = []
371
+ ): void {
372
+ let kept = 0
373
+ let droppedAttributes = 0
374
+ // Built fresh rather than edited in place: a hook is free to return a record
375
+ // whose attributes it froze, and a `delete` on one throws.
376
+ const attributes: SpanAttributes = {}
377
+ for (const key of orderedKeys(record.attributes, keysBeforeHook)) {
378
+ const value = record.attributes[key]
379
+ // Matches `_writeAttribute`: the encoder drops these, so a hook that blanks a
380
+ // value rather than deleting the key must not evict a real attribute.
381
+ if (isNullish(value)) {
382
+ continue
383
+ }
384
+ if (!autoKeys.has(key)) {
385
+ if (kept >= maxAttributes) {
386
+ droppedAttributes++
387
+ continue
388
+ }
389
+ kept++
390
+ }
391
+ Object.defineProperty(attributes, key, {
392
+ value: truncateAttributeValue(value, maxAttributeValueLength),
393
+ enumerable: true,
394
+ writable: true,
395
+ configurable: true,
396
+ })
397
+ }
398
+ record.attributes = attributes
399
+ if (droppedAttributes) {
400
+ // Coerced, not trusted: a hook can put anything in the counter, and a
401
+ // non-number there would erase the count the span itself accumulated.
402
+ record.droppedAttributesCount = nonNegativeCount(record.droppedAttributesCount) + droppedAttributes
403
+ }
404
+
405
+ // Walked rather than sliced: a hook can append events or rewrite their
406
+ // attributes, neither of which goes through `addEvent`, so each one still
407
+ // needs its attributes bounded on the way past.
408
+ let keptEvents = 0
409
+ let droppedEvents = 0
410
+ const events: SpanEventRecord[] = []
411
+ for (const event of record.events) {
412
+ if (keptEvents >= maxEvents) {
413
+ droppedEvents++
414
+ continue
415
+ }
416
+ keptEvents++
417
+ if (event.attributes) {
418
+ // A hook can widen an event as freely as it can add one, and neither goes
419
+ // through `addEvent`.
420
+ const bounded = boundAttributes(event.attributes, maxAttributesPerEvent, maxAttributeValueLength)
421
+ event.attributes = bounded.attributes
422
+ if (bounded.dropped) {
423
+ event.droppedAttributesCount = nonNegativeCount(event.droppedAttributesCount) + bounded.dropped
424
+ }
425
+ }
426
+ events.push(event)
427
+ }
428
+ record.events = events
429
+ if (droppedEvents) {
430
+ record.droppedEventsCount = nonNegativeCount(record.droppedEventsCount) + droppedEvents
431
+ }
432
+ if (record.status?.message) {
433
+ // Coerced first: a non-string would reach the encoder to be stringified at
434
+ // full length. Guarded, because a throwing `toString` here would cost the
435
+ // span, where the encoder downstream only marks the field.
436
+ record.status = {
437
+ ...record.status,
438
+ message: truncateString(safeString(record.status.message), maxAttributeValueLength),
439
+ }
440
+ }
441
+ }
442
+
443
+ /**
444
+ * An inert handle returned whenever tracing cannot run — traces unconfigured,
445
+ * SDK disabled, user opted out. Supports the full surface so caller code never
446
+ * branches, and returns `null` from `traceparent()` so an id this SDK never
447
+ * recorded cannot propagate.
448
+ */
449
+ export class NoopSpan implements Span {
450
+ setAttribute(): this {
451
+ return this
452
+ }
453
+ setAttributes(): this {
454
+ return this
455
+ }
456
+ addEvent(): this {
457
+ return this
458
+ }
459
+ setStatus(): this {
460
+ return this
461
+ }
462
+ recordException(): this {
463
+ return this
464
+ }
465
+ updateName(): this {
466
+ return this
467
+ }
468
+ traceparent(): string | null {
469
+ return null
470
+ }
471
+ tracestate(): string | null {
472
+ return null
473
+ }
474
+ end(): void {}
475
+ }
476
+
477
+ // Typed as `Span`, not `NoopSpan`: the class's methods take no parameters, so the
478
+ // concrete type would reject calls the interface allows.
479
+ export const NOOP_SPAN: Span = /* @__PURE__ */ new NoopSpan()
480
+
481
+ /**
482
+ * An inert handle that carries an inbound trace context. Records nothing, and
483
+ * echoes the `traceparent` it was handed — including the caller's version and
484
+ * sampled flag — so a service with tracing off still forwards the trace it
485
+ * received rather than severing it. The ids it propagates are the upstream
486
+ * caller's own; this SDK invents none.
487
+ */
488
+ export class PassThroughSpan extends NoopSpan {
489
+ constructor(
490
+ private readonly _traceparent: string,
491
+ private readonly _tracestate?: string
492
+ ) {
493
+ super()
494
+ }
495
+
496
+ override traceparent(): string {
497
+ return this._traceparent
498
+ }
499
+
500
+ override tracestate(): string | null {
501
+ return this._tracestate ?? null
502
+ }
503
+ }
504
+
505
+ /**
506
+ * The `stack` of whatever was thrown, as OTel's `exception.stacktrace`. Reads
507
+ * the property behind its own guard: a getter on a hostile object throws, and a
508
+ * thrown string has no stack at all. The value is bounded like any other
509
+ * attribute, by `maxAttributeValueLength`.
510
+ */
511
+ function readStack(error: unknown): { stack?: string } {
512
+ try {
513
+ const stack = (error as { stack?: unknown }).stack
514
+ return typeof stack === 'string' && stack ? { stack } : {}
515
+ } catch {
516
+ return {}
517
+ }
518
+ }
519
+
520
+ /**
521
+ * The handle to return when a span cannot be recorded: a pass-through when a
522
+ * context is available, the shared no-op otherwise. With no explicit `parent`
523
+ * the active handle supplies it, so an inbound trace survives nesting.
524
+ *
525
+ * @internal Exposed for cross-package use within this SDK; not part of the stable public API.
526
+ */
527
+ export function inertSpan(options?: { parent?: unknown; tracestate?: unknown }, active?: Span): Span {
528
+ const parent = traceparentHeader(options?.parent) ?? active
529
+ // A handle parent reports its own context, read behind a guard because a
530
+ // foreign handle's accessor may throw. A no-op reports none and stays a no-op.
531
+ const inbound = typeof parent === 'string' || parent == null ? parent : readHandle(parent, 'traceparent')
532
+ const traceparent = normalizeTraceparent(inbound)
533
+ if (!traceparent) {
534
+ return NOOP_SPAN
535
+ }
536
+ const tracestate =
537
+ typeof parent === 'string' || parent == null ? options?.tracestate : readHandle(parent, 'tracestate')
538
+ return new PassThroughSpan(traceparent, sanitizeTracestate(tracestate))
539
+ }
540
+
541
+ function readHandle(parent: unknown, method: 'traceparent' | 'tracestate'): unknown {
542
+ try {
543
+ const fn = (parent as Span)[method]
544
+ return typeof fn === 'function' ? fn.call(parent) : undefined
545
+ } catch {
546
+ return undefined
547
+ }
548
+ }
549
+
550
+ /** One walk's budget, allocated per attribute value. */
551
+ interface TruncateState {
552
+ /** Containers on the current path, so a back-reference stops the walk. */
553
+ ancestors: WeakSet<object>
554
+ /** Nodes this walk may still visit. */
555
+ remainingNodes: number
556
+ }
557
+
558
+ function truncateString(value: string, maxLength: number): string {
559
+ return value.length > maxLength ? value.slice(0, maxLength) : value
560
+ }
561
+
562
+ /**
563
+ * Bounds every string reachable from an attribute value to `maxLength`
564
+ * characters, including the strings nested inside arrays and objects. Numbers
565
+ * and booleans are bounded already.
566
+ *
567
+ * An unbounded value is the one thing the per-span caps do not stop: a single
568
+ * multi-MB attribute makes the whole span too large for the ingestion endpoint,
569
+ * and the 413 path then drops that span whole. `setAttribute('payload', { body })`
570
+ * is the usual way one arrives, so the bound has to reach inside the value.
571
+ *
572
+ * Returns the value it was given when nothing needed shortening, so the common
573
+ * case allocates nothing.
574
+ */
575
+ export function truncateAttributeValue(value: SpanAttributeValue, maxLength: number): SpanAttributeValue {
576
+ return truncateValue(value, maxLength, { ancestors: new WeakSet(), remainingNodes: MAX_JSON_SAFE_VALUE_NODES }, 0)
577
+ }
578
+
579
+ /**
580
+ * Walks under the same depth cap, node budget and ancestor set as
581
+ * `encodeAnyValue`, charging the same values. The encoder spends one budget
582
+ * across the whole attribute bag where this spends one per value, so it runs out
583
+ * no later and marks whatever this walk returned whole.
584
+ *
585
+ * Depth alone does not bound this: a value whose children point back at their
586
+ * siblings costs `fanout ** depth` visits, which is minutes of synchronous work
587
+ * inside the caller's own `setAttribute` call.
588
+ */
589
+ function truncateValue(
590
+ value: SpanAttributeValue,
591
+ maxLength: number,
592
+ state: TruncateState,
593
+ depth: number
594
+ ): SpanAttributeValue {
595
+ if (value === null || typeof value !== 'object') {
596
+ // Free, as it is in the encoder, which drops a nullish leaf without charging.
597
+ if (isNullish(value)) {
598
+ return value
599
+ }
600
+ // A shared subtree is re-walked once per path reaching it, so a leaf that
601
+ // skips the charge lets one value cost `budget * items` string copies.
602
+ if (state.remainingNodes <= 0) {
603
+ return value
604
+ }
605
+ state.remainingNodes--
606
+ return typeof value === 'string' ? truncateString(value, maxLength) : value
607
+ }
608
+ if (state.ancestors.has(value)) {
609
+ // The marker the encoder would produce, not the value itself. Handing the
610
+ // raw ancestor back puts it inside a *copied* parent, where the encoder's
611
+ // own cycle detection no longer recognises it and walks one more level of
612
+ // its strings at full length.
613
+ return CIRCULAR_VALUE
614
+ }
615
+ if (state.remainingNodes <= 0 || depth >= MAX_JSON_SAFE_VALUE_DEPTH) {
616
+ return value
617
+ }
618
+ state.remainingNodes--
619
+ state.ancestors.add(value)
620
+ try {
621
+ // A Date is emitted by the encoder from its own branch, ahead of any
622
+ // `toJSON` probe, so bounding it here would ship a truncated timestamp
623
+ // rather than a shorter one.
624
+ if (value instanceof Date) {
625
+ return value
626
+ }
627
+ // The representation the value defines for itself is what the encoder puts
628
+ // on the wire, so it is what has to be bounded — a `toJSON` returning a
629
+ // megabyte of text is invisible to a walk over the object's own keys.
630
+ const resolved = resolveToJson(value)
631
+ if (resolved.selfDescribed) {
632
+ // Resolving to nothing is the value's answer. Walking its keys anyway
633
+ // would build a plain object the encoder no longer treats as
634
+ // self-describing, putting the internals of a redacted value on the wire.
635
+ // Stored as the string the encoder builds from that same nullish result
636
+ // rather than as the value itself: the encoder probes `toJSON` a second
637
+ // time, so one that answers `null` here is free to answer with a megabyte
638
+ // there, past the bound this walk exists to apply. Left unbounded like the
639
+ // other markers — nine characters at most, and trimming it to `unde` would
640
+ // only make it unreadable.
641
+ return isNullish(resolved.value)
642
+ ? String(resolved.value)
643
+ : truncateValue(resolved.value, maxLength, state, depth + 1)
644
+ }
645
+ if (isArray(value)) {
646
+ // Only the items the encoder will emit are walked; it stops at the same
647
+ // cap, so bounding the rest is work spent on values that never ship.
648
+ const walked = Math.min(value.length, MAX_JSON_SAFE_VALUE_ITEMS)
649
+ // Accumulated rather than copied from the value: `slice()` reads every
650
+ // element, accessors past the cap included, and one of those throwing
651
+ // would reach the outer catch and cost the whole array its bound.
652
+ const boundedItems: SpanAttributeValue[] = []
653
+ for (let index = 0; index < walked; index++) {
654
+ try {
655
+ boundedItems.push(truncateValue(value[index], maxLength, state, depth + 1))
656
+ } catch {
657
+ // A throwing accessor costs its own item, as it does in the encoder.
658
+ boundedItems.push(UNSERIALIZABLE_VALUE)
659
+ }
660
+ }
661
+ // Carried so the encoder still marks what it cut.
662
+ if (value.length > walked) {
663
+ boundedItems.length = value.length
664
+ }
665
+ return boundedItems
666
+ }
667
+ const bounded: SpanAttributes = {}
668
+ // Counted the way the encoder counts, so the walk stops where its output
669
+ // does: a key it skips costs no slot, and reading past the last one it can
670
+ // emit is getter work on values that never ship.
671
+ let emittable = 0
672
+ for (const key of Object.keys(value)) {
673
+ if (emittable >= MAX_JSON_SAFE_VALUE_ITEMS) {
674
+ break
675
+ }
676
+ let boundedItem: SpanAttributeValue
677
+ try {
678
+ // Read once: re-reading to compare would run a getter a second time.
679
+ boundedItem = truncateValue((value as SpanAttributes)[key], maxLength, state, depth + 1)
680
+ } catch {
681
+ // A throwing accessor costs its own key. Reaching the walk's own catch
682
+ // would abandon the whole value unbounded, which is how a lazy ORM
683
+ // relation next to a large field puts that field on the wire whole.
684
+ boundedItem = UNSERIALIZABLE_VALUE
685
+ }
686
+ if (key && !isNullish(boundedItem)) {
687
+ emittable++
688
+ }
689
+ // defineProperty, not assignment: a nested `__proto__` key would otherwise
690
+ // swap the copy's prototype and vanish.
691
+ Object.defineProperty(bounded, key, {
692
+ value: boundedItem,
693
+ enumerable: true,
694
+ writable: true,
695
+ configurable: true,
696
+ })
697
+ }
698
+ return bounded
699
+ } catch {
700
+ // Whatever is left — a hostile `Object.keys`, a `slice` that throws — costs
701
+ // this value its bound rather than the span. Per-key reads are guarded
702
+ // above, so a single bad property does not reach here.
703
+ return value
704
+ } finally {
705
+ // Siblings pointing at the same object are duplication, not a cycle.
706
+ state.ancestors.delete(value)
707
+ }
708
+ }
709
+
710
+ /**
711
+ * The value's own serialized form. `selfDescribed` is false when it defines no
712
+ * `toJSON`, or when reading one throws — both fall through to the plain walk,
713
+ * as they do in the encoder.
714
+ */
715
+ function resolveToJson(value: object): { selfDescribed: boolean; value?: SpanAttributeValue } {
716
+ try {
717
+ const toJSON = (value as { toJSON?: unknown }).toJSON
718
+ if (typeof toJSON === 'function') {
719
+ return { selfDescribed: true, value: toJSON.call(value) as SpanAttributeValue }
720
+ }
721
+ } catch {
722
+ // Falls through to the plain walk.
723
+ }
724
+ return { selfDescribed: false }
725
+ }
726
+
727
+ /**
728
+ * A copy of a caller-supplied attribute bag holding at most `max` entries, each
729
+ * value bounded to `maxLength`, plus how many entries the cap refused.
730
+ *
731
+ * Once the cap is spent the remaining keys are counted without being read, so a
732
+ * wide object does not pay for the getters on values it is about to drop.
733
+ */
734
+ function boundAttributes(
735
+ source: SpanAttributes,
736
+ max: number,
737
+ maxLength: number
738
+ ): { attributes: SpanAttributes; dropped: number } {
739
+ let keys: string[]
740
+ try {
741
+ keys = Object.keys(source)
742
+ } catch {
743
+ // A hostile own-keys trap costs the bag, not the event carrying it.
744
+ return { attributes: {}, dropped: 0 }
745
+ }
746
+ const attributes: SpanAttributes = {}
747
+ let kept = 0
748
+ let dropped = 0
749
+ for (const key of keys) {
750
+ if (kept >= max) {
751
+ dropped++
752
+ continue
753
+ }
754
+ let value: SpanAttributeValue
755
+ try {
756
+ value = truncateAttributeValue(source[key], maxLength)
757
+ } catch {
758
+ // A throwing getter costs its own key, as it does in `assignUserAttributes`.
759
+ value = UNSERIALIZABLE_VALUE
760
+ }
761
+ // Nullish spends no slot, matching `_writeAttribute` and the span half of
762
+ // `applySpanLimits`: the encoder drops these, so a caller who blanked a value
763
+ // rather than omitting the key must not lose a real attribute to it.
764
+ if (isNullish(value)) {
765
+ continue
766
+ }
767
+ kept++
768
+ // defineProperty, not assignment: `attributes['__proto__'] = v` hits the
769
+ // prototype setter and the attribute vanishes.
770
+ Object.defineProperty(attributes, key, { value, enumerable: true, writable: true, configurable: true })
771
+ }
772
+ return { attributes, dropped }
773
+ }
774
+
775
+ /** `truncateAttributeValue` across an attribute bag, in place. */
776
+ export function truncateAttributes(attributes: SpanAttributes, maxLength: number): SpanAttributes {
777
+ for (const key of Object.keys(attributes)) {
778
+ attributes[key] = truncateAttributeValue(attributes[key], maxLength)
779
+ }
780
+ return attributes
781
+ }
782
+
783
+ /**
784
+ * Runs `fn` with `span` active, which every scoped helper does the same way.
785
+ *
786
+ * The shared no-op is never activated, so `getActiveSpan()` inside the callback
787
+ * reads null — callbacks should use the handle they're given. A pass-through
788
+ * handle is activated, so `getActiveSpan()?.traceparent()` still propagates an
789
+ * inbound trace through a service with tracing off.
790
+ *
791
+ * @internal Exposed for cross-package use within this SDK; not part of the stable public API.
792
+ */
793
+ export function runWithActiveSpan<T>(contextManager: SpanContextManager, span: Span, fn: (span: Span) => T): T {
794
+ return span === NOOP_SPAN ? fn(span) : contextManager.with(span, () => fn(span))
795
+ }
796
+
797
+ /**
798
+ * Extracts the OTel `exception.type` / `exception.message` pair from whatever was
799
+ * thrown. Anything can be thrown in JS, so non-Errors are described by type.
800
+ */
801
+ export function describeError(error: unknown): { type: string; message: string; stack?: string } {
802
+ try {
803
+ const stack = readStack(error)
804
+ if (isError(error)) {
805
+ return { type: error.name || 'Error', message: error.message || '', ...stack }
806
+ }
807
+ if (typeof error === 'string') {
808
+ return { type: 'string', message: error }
809
+ }
810
+ if (error && typeof error === 'object') {
811
+ const maybe = error as { name?: unknown; message?: unknown }
812
+ if (typeof maybe.message === 'string') {
813
+ return { type: typeof maybe.name === 'string' ? maybe.name : 'Object', message: maybe.message, ...stack }
814
+ }
815
+ }
816
+ return { type: typeof error, message: String(error) }
817
+ } catch {
818
+ // A hostile `toString` or accessor must not throw a second error: in `withSpan`
819
+ // that would replace the application's error and skip the span's `end()`.
820
+ return { type: typeof error, message: '' }
821
+ }
822
+ }