@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,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
|
+
}
|