@zuplo/cli 7.7.7 → 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/index.d.ts +1 -1
- package/node_modules/@posthog/core/dist/error-tracking/index.d.ts.map +1 -1
- 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/index.ts +24 -1
- 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-FEB2VFS7.js → chunk-KNAYKBEE.js} +96 -96
- 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-FEB2VFS7.js.map +0 -1
- /package/node_modules/@zuplo/runtime/out/esm/{chunk-FEB2VFS7.js.LEGAL.txt → chunk-KNAYKBEE.js.LEGAL.txt} +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { createMockLogger } from '@/testing'
|
|
2
|
+
import { resolveTracesConfig } from './config'
|
|
3
|
+
|
|
4
|
+
describe('resolveTracesConfig', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['zero', 0],
|
|
7
|
+
['negative', -1],
|
|
8
|
+
['not a number', NaN],
|
|
9
|
+
// Floored, these read as 1, 2 and 3 — caps an order of magnitude below what
|
|
10
|
+
// the caller wrote, applied silently.
|
|
11
|
+
['a fraction', 1.5],
|
|
12
|
+
['a large fraction', 200.5],
|
|
13
|
+
['infinity', Infinity],
|
|
14
|
+
])('falls back to the default per-span caps when given %s', (_label, value) => {
|
|
15
|
+
const resolved = resolveTracesConfig({
|
|
16
|
+
maxAttributesPerSpan: value,
|
|
17
|
+
maxEventsPerSpan: value,
|
|
18
|
+
maxAttributeValueLength: value,
|
|
19
|
+
})
|
|
20
|
+
expect(resolved.maxAttributesPerSpan).toBe(128)
|
|
21
|
+
expect(resolved.maxEventsPerSpan).toBe(128)
|
|
22
|
+
expect(resolved.maxAttributeValueLength).toBe(8192)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('honours explicit per-span caps', () => {
|
|
26
|
+
// Without this the resolver can ignore maxEventsPerSpan entirely and every
|
|
27
|
+
// other test still passes, because they all assert the default.
|
|
28
|
+
expect(resolveTracesConfig({ maxAttributesPerSpan: 5, maxEventsPerSpan: 7 })).toMatchObject({
|
|
29
|
+
maxAttributesPerSpan: 5,
|
|
30
|
+
maxEventsPerSpan: 7,
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('keeps the per-event attribute cap fixed', () => {
|
|
35
|
+
// The cap is internal, so an untyped caller naming it gets the default.
|
|
36
|
+
expect(resolveTracesConfig({ maxAttributesPerEvent: 9 } as any).maxAttributesPerEvent).toBe(128)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('applies the documented defaults', () => {
|
|
40
|
+
expect(resolveTracesConfig(undefined)).toMatchObject({
|
|
41
|
+
flushIntervalMs: 5000,
|
|
42
|
+
maxExportBatchSize: 512,
|
|
43
|
+
maxQueueSize: 2048,
|
|
44
|
+
maxAttributesPerSpan: 128,
|
|
45
|
+
maxEventsPerSpan: 128,
|
|
46
|
+
maxAttributesPerEvent: 128,
|
|
47
|
+
maxAttributeValueLength: 8192,
|
|
48
|
+
maxLiveSpans: 10_000,
|
|
49
|
+
maxSpanAgeMs: 3_600_000,
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('honours an explicit attribute value bound', () => {
|
|
54
|
+
expect(resolveTracesConfig({ maxAttributeValueLength: 256 }).maxAttributeValueLength).toBe(256)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('honours explicit live-span bounds', () => {
|
|
58
|
+
expect(resolveTracesConfig({ maxLiveSpans: 50, maxSpanAgeMs: 30_000 })).toMatchObject({
|
|
59
|
+
maxLiveSpans: 50,
|
|
60
|
+
maxSpanAgeMs: 30_000,
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it.each([0, -1, Number.NaN])('falls back to the defaults for unusable live-span bounds (%p)', (value) => {
|
|
65
|
+
expect(resolveTracesConfig({ maxLiveSpans: value, maxSpanAgeMs: value })).toMatchObject({
|
|
66
|
+
maxLiveSpans: 10_000,
|
|
67
|
+
maxSpanAgeMs: 3_600_000,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('leaves serviceName unset so core supplies unknown_service', () => {
|
|
72
|
+
expect(resolveTracesConfig({}).serviceName).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('honours explicit values', () => {
|
|
76
|
+
expect(
|
|
77
|
+
resolveTracesConfig({ serviceName: 'checkout', flushIntervalMs: 1000, maxExportBatchSize: 50 })
|
|
78
|
+
).toMatchObject({
|
|
79
|
+
serviceName: 'checkout',
|
|
80
|
+
flushIntervalMs: 1000,
|
|
81
|
+
maxExportBatchSize: 50,
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('lets OTLP resource attributes override the named fields', () => {
|
|
86
|
+
const resolved = resolveTracesConfig({
|
|
87
|
+
serviceName: 'named',
|
|
88
|
+
serviceVersion: '1.0.0',
|
|
89
|
+
environment: 'staging',
|
|
90
|
+
resourceAttributes: {
|
|
91
|
+
'service.name': 'from-attributes',
|
|
92
|
+
'service.version': '2.0.0',
|
|
93
|
+
'deployment.environment': 'production',
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(resolved.serviceName).toBe('from-attributes')
|
|
98
|
+
expect(resolved.serviceVersion).toBe('2.0.0')
|
|
99
|
+
expect(resolved.environment).toBe('production')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it.each([0, -1, 0.5, Number.NaN, Number.POSITIVE_INFINITY])(
|
|
103
|
+
'falls back to the default for an unusable maxExportBatchSize (%p)',
|
|
104
|
+
(value) => {
|
|
105
|
+
// A non-positive batch size reaches an export loop that cannot make
|
|
106
|
+
// progress with it, so it spins forever posting empty batches.
|
|
107
|
+
expect(resolveTracesConfig({ maxExportBatchSize: value }).maxExportBatchSize).toBe(512)
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
it('takes the default for a fractional batch size rather than flooring it', () => {
|
|
112
|
+
// Every numeric knob resolves the same way, so a fraction is a value the
|
|
113
|
+
// caller did not mean rather than one to round down behind their back.
|
|
114
|
+
expect(resolveTracesConfig({ maxExportBatchSize: 10.9 }).maxExportBatchSize).toBe(512)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it.each([0, -1, 1.5, Number.NaN])('falls back to the default for an unusable flushIntervalMs (%p)', (value) => {
|
|
118
|
+
expect(resolveTracesConfig({ flushIntervalMs: value }).flushIntervalMs).toBe(5000)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it.each([0.5, 10_000.5])('falls back to the defaults for fractional live-span bounds (%p)', (value) => {
|
|
122
|
+
expect(resolveTracesConfig({ maxLiveSpans: value, maxSpanAgeMs: value })).toMatchObject({
|
|
123
|
+
maxLiveSpans: 10_000,
|
|
124
|
+
maxSpanAgeMs: 3_600_000,
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('keeps the queue at least as large as the export batch', () => {
|
|
129
|
+
// A queue smaller than the flush trigger would stop the depth-based flush
|
|
130
|
+
// from ever firing.
|
|
131
|
+
expect(resolveTracesConfig({ maxExportBatchSize: 5000 }).maxQueueSize).toBe(5000)
|
|
132
|
+
expect(resolveTracesConfig({ maxExportBatchSize: 10 }).maxQueueSize).toBe(2048)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('honours an explicit maxQueueSize', () => {
|
|
136
|
+
expect(resolveTracesConfig({ maxQueueSize: 100_000 }).maxQueueSize).toBe(100_000)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('floors an explicit maxQueueSize at the export batch size', () => {
|
|
140
|
+
expect(resolveTracesConfig({ maxExportBatchSize: 512, maxQueueSize: 10 }).maxQueueSize).toBe(512)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('attaches the host resource attributes the entrypoint supplies', () => {
|
|
144
|
+
expect(
|
|
145
|
+
resolveTracesConfig(undefined, { 'os.name': 'linux', 'os.version': '6.1.0-27-amd64' }).resourceAttributes
|
|
146
|
+
).toEqual({ 'os.name': 'linux', 'os.version': '6.1.0-27-amd64' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('lets user resource attributes override the host ones', () => {
|
|
150
|
+
expect(
|
|
151
|
+
resolveTracesConfig(
|
|
152
|
+
{ resourceAttributes: { 'os.name': 'my-os', 'os.version': '1.2.3' } },
|
|
153
|
+
{ 'os.name': 'linux', 'os.version': '6.1.0-27-amd64' }
|
|
154
|
+
).resourceAttributes
|
|
155
|
+
).toEqual({ 'os.name': 'my-os', 'os.version': '1.2.3' })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('resolves when the entrypoint supplies no host attributes', () => {
|
|
159
|
+
expect(resolveTracesConfig({ resourceAttributes: { 'host.name': 'worker-01' } }).resourceAttributes).toEqual({
|
|
160
|
+
'host.name': 'worker-01',
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('resourceAttributes guarding', () => {
|
|
166
|
+
it('ignores a non-object value', () => {
|
|
167
|
+
const { resourceAttributes } = resolveTracesConfig({ resourceAttributes: 'oops' as never })
|
|
168
|
+
|
|
169
|
+
expect(Object.keys(resourceAttributes ?? {})).toEqual([])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('ignores an array, which would otherwise spread as numeric keys', () => {
|
|
173
|
+
const { resourceAttributes } = resolveTracesConfig({
|
|
174
|
+
resourceAttributes: [{ key: 'service.name' }] as never,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(Object.keys(resourceAttributes ?? {})).not.toContain('0')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('drops an identity key that is not a string', () => {
|
|
181
|
+
const resolved = resolveTracesConfig({
|
|
182
|
+
serviceName: 'checkout-api',
|
|
183
|
+
resourceAttributes: { 'service.name': 12345 as never, region: 'us' },
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(resolved.serviceName).toBe('checkout-api')
|
|
187
|
+
expect(resolved.resourceAttributes).toEqual({ region: 'us' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('ignores a beforeSpanSend entry that is not a function', () => {
|
|
191
|
+
// A plain-JS caller passing the wrong shape would otherwise have every span
|
|
192
|
+
// dropped by a hook that throws on call, with tracing silently off.
|
|
193
|
+
const scrub = (span: any): any => span
|
|
194
|
+
const resolved = resolveTracesConfig({ beforeSpanSend: ['not a function', scrub] as never })
|
|
195
|
+
|
|
196
|
+
expect(resolved.beforeSpanSend).toEqual([scrub])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('warns about a dropped hook, since the redaction it was configured for is gone', () => {
|
|
200
|
+
const logger = createMockLogger()
|
|
201
|
+
const scrub = (span: any): any => span
|
|
202
|
+
|
|
203
|
+
resolveTracesConfig({ beforeSpanSend: ['not a function', scrub] as never }, undefined, logger)
|
|
204
|
+
|
|
205
|
+
// `critical`, not `warn`: every other level is gated behind `debug: true`.
|
|
206
|
+
expect(logger.critical).toHaveBeenCalledWith(expect.stringContaining('ignoring 1 of 2 entries'))
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('stays quiet for a conditionally disabled hook', () => {
|
|
210
|
+
// `[featureEnabled && scrub]` is ordinary JS; nothing was configured to
|
|
211
|
+
// redact, so claiming redaction is broken would be false alarm.
|
|
212
|
+
const logger = createMockLogger()
|
|
213
|
+
|
|
214
|
+
const resolved = resolveTracesConfig({ beforeSpanSend: [false, null, undefined] as never }, undefined, logger)
|
|
215
|
+
|
|
216
|
+
expect(resolved.beforeSpanSend).toEqual([])
|
|
217
|
+
expect(logger.critical).not.toHaveBeenCalled()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('stays quiet when every hook is callable', () => {
|
|
221
|
+
const logger = createMockLogger()
|
|
222
|
+
|
|
223
|
+
resolveTracesConfig({ beforeSpanSend: [(span: any): any => span] }, undefined, logger)
|
|
224
|
+
|
|
225
|
+
expect(logger.critical).not.toHaveBeenCalled()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('resolves to no hooks when beforeSpanSend is the wrong type entirely', () => {
|
|
229
|
+
const resolved = resolveTracesConfig({ beforeSpanSend: { scrub: true } as never })
|
|
230
|
+
|
|
231
|
+
expect(resolved.beforeSpanSend).toEqual([])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('does not throw when an identity accessor throws', () => {
|
|
235
|
+
const hostile = {}
|
|
236
|
+
Object.defineProperty(hostile, 'service.name', {
|
|
237
|
+
enumerable: true,
|
|
238
|
+
get() {
|
|
239
|
+
throw new Error('config getter exploded')
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
expect(() => resolveTracesConfig({ resourceAttributes: hostile as never })).not.toThrow()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('does not throw when a non-identity accessor throws', () => {
|
|
247
|
+
const hostile = { 'service.name': 'checkout-api' }
|
|
248
|
+
Object.defineProperty(hostile, 'region', {
|
|
249
|
+
enumerable: true,
|
|
250
|
+
get() {
|
|
251
|
+
throw new Error('config getter exploded')
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(() => resolveTracesConfig({ resourceAttributes: hostile as never }, { 'os.name': 'Linux' })).not.toThrow()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('keeps the readable attributes when one accessor throws', () => {
|
|
259
|
+
const hostile = { region: 'us' }
|
|
260
|
+
Object.defineProperty(hostile, 'tenant', {
|
|
261
|
+
enumerable: true,
|
|
262
|
+
get() {
|
|
263
|
+
throw new Error('config getter exploded')
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const resolved = resolveTracesConfig({ resourceAttributes: hostile as never }, { 'os.name': 'Linux' })
|
|
268
|
+
|
|
269
|
+
expect(resolved.resourceAttributes).toMatchObject({ 'os.name': 'Linux', region: 'us' })
|
|
270
|
+
})
|
|
271
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { assignUserAttributes } from '../utils/json-utils'
|
|
2
|
+
import type { ResolvedTracesConfig } from './types'
|
|
3
|
+
import type { BeforeSpanSendFn, TracesConfig } from '@posthog/types'
|
|
4
|
+
import type { Logger } from '../types'
|
|
5
|
+
|
|
6
|
+
// OpenTelemetry's BatchSpanProcessor defaults, which sit comfortably under the
|
|
7
|
+
// server's request body cap.
|
|
8
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 5000
|
|
9
|
+
const DEFAULT_MAX_EXPORT_BATCH_SIZE = 512
|
|
10
|
+
const DEFAULT_MAX_QUEUE_SIZE = 2048
|
|
11
|
+
// OpenTelemetry's per-span defaults.
|
|
12
|
+
const DEFAULT_MAX_ATTRIBUTES_PER_SPAN = 128
|
|
13
|
+
const DEFAULT_MAX_EVENTS_PER_SPAN = 128
|
|
14
|
+
// OpenTelemetry's per-event limit, which is the same number. Fixed rather than
|
|
15
|
+
// configurable: `maxAttributesPerSpan` and `maxEventsPerSpan` already give a
|
|
16
|
+
// caller room to shape a span, and this one only has to stop an event holding
|
|
17
|
+
// an unbounded bag.
|
|
18
|
+
const DEFAULT_MAX_ATTRIBUTES_PER_EVENT = 128
|
|
19
|
+
// OpenTelemetry leaves the value length unlimited, which is what lets one
|
|
20
|
+
// multi-MB attribute make a span too large for the endpoint to accept — and an
|
|
21
|
+
// oversized span is dropped whole. 8 KB bounds a single string: it holds a deep
|
|
22
|
+
// stack trace and any realistic header, query string or payload excerpt, and
|
|
23
|
+
// keeps a span's own attributes under 1 MB at the attribute cap. A span's total
|
|
24
|
+
// size is the product of these caps; the body limit itself is enforced at the
|
|
25
|
+
// batch boundary, which is where the whole payload can be weighed at once.
|
|
26
|
+
const DEFAULT_MAX_ATTRIBUTE_VALUE_LENGTH = 8192
|
|
27
|
+
|
|
28
|
+
// Live-span bounds. A server can legitimately hold thousands of spans open at
|
|
29
|
+
// once, and refusing a legitimate span is worse than tolerating a leak, so the
|
|
30
|
+
// count bound sits well above realistic concurrency — affordable because live
|
|
31
|
+
// accounting is an id and a timestamp per span, not the span. The age bound is
|
|
32
|
+
// an hour: production traces routinely run past ten minutes, and a span still
|
|
33
|
+
// open after an hour is a leak rather than slow work.
|
|
34
|
+
const DEFAULT_MAX_LIVE_SPANS = 10_000
|
|
35
|
+
const DEFAULT_MAX_SPAN_AGE_MS = 3_600_000
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Coerces a caller-supplied positive-integer option. `0`, a negative, or `NaN`
|
|
39
|
+
* reaching the export loop would stall it.
|
|
40
|
+
*
|
|
41
|
+
* A fraction takes the default rather than being floored: these are documented
|
|
42
|
+
* as positive integers, and silently reading `maxAttributesPerSpan: 1.5` as `1`
|
|
43
|
+
* caps a span an order of magnitude below what the caller wrote.
|
|
44
|
+
*/
|
|
45
|
+
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
46
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 ? value : fallback
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const IDENTITY_KEYS = ['service.name', 'service.version', 'deployment.environment'] as const
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Drops a non-string identity key rather than letting it through: the resolver
|
|
53
|
+
* would ignore it, and it would still reach the wire as an int and leave the
|
|
54
|
+
* spans unattributable.
|
|
55
|
+
*/
|
|
56
|
+
function withUsableIdentityKeys(attributes: TracesConfig['resourceAttributes']): TracesConfig['resourceAttributes'] {
|
|
57
|
+
// A primitive or array would otherwise be spread into attributes keyed "0", "1", "2".
|
|
58
|
+
if (!attributes || typeof attributes !== 'object' || Array.isArray(attributes)) {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (IDENTITY_KEYS.every((key) => !(key in attributes) || typeof attributes[key] === 'string')) {
|
|
63
|
+
return attributes
|
|
64
|
+
}
|
|
65
|
+
const usable = { ...attributes }
|
|
66
|
+
for (const key of IDENTITY_KEYS) {
|
|
67
|
+
if (key in usable && typeof usable[key] !== 'string') {
|
|
68
|
+
delete usable[key]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return usable
|
|
72
|
+
} catch {
|
|
73
|
+
// A throwing accessor on the config object must not escape `startSpan`.
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Keeps only the callable hooks. Anything else is dropped rather than called: an
|
|
80
|
+
* untyped caller passing the wrong shape would otherwise have every span dropped
|
|
81
|
+
* by a hook that throws, leaving tracing silently off.
|
|
82
|
+
*
|
|
83
|
+
* Reported at `critical` rather than thrown on: a constructor that throws takes
|
|
84
|
+
* the application down, but every other log level is gated behind `debug: true`,
|
|
85
|
+
* and an inert redaction hook ships the values it was meant to remove.
|
|
86
|
+
*/
|
|
87
|
+
function resolveBeforeSpanSend(beforeSpanSend: TracesConfig['beforeSpanSend'], logger?: Logger): BeforeSpanSendFn[] {
|
|
88
|
+
if (!beforeSpanSend) {
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
// `[featureEnabled && scrub]` is ordinary JS, and a caller who wrote it did not
|
|
92
|
+
// configure a hook at all — only a value that was meant to be one is worth
|
|
93
|
+
// shouting about.
|
|
94
|
+
const supplied = [beforeSpanSend].flat().filter((hook) => Boolean(hook))
|
|
95
|
+
const hooks = supplied.filter((hook): hook is BeforeSpanSendFn => typeof hook === 'function')
|
|
96
|
+
if (hooks.length !== supplied.length) {
|
|
97
|
+
logger?.critical(
|
|
98
|
+
`beforeSpanSend: ignoring ${supplied.length - hooks.length} of ${supplied.length} entries that are not functions. ` +
|
|
99
|
+
'Spans export without them, so whatever they were redacting is not redacted.'
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
return hooks
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolves the public `traces` config into the shape core `PostHogTraces` consumes.
|
|
107
|
+
* OTLP resource attributes take precedence over the named fields, matching the
|
|
108
|
+
* logs config. `hostResourceAttributes` are runtime-detected by the entrypoint and
|
|
109
|
+
* merge first, so a user-supplied value of the same key wins.
|
|
110
|
+
*
|
|
111
|
+
* @internal Exposed for cross-package use within this SDK; not part of the stable public API.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveTracesConfig(
|
|
114
|
+
config: TracesConfig | undefined,
|
|
115
|
+
hostResourceAttributes?: Record<string, string>,
|
|
116
|
+
logger?: Logger
|
|
117
|
+
): ResolvedTracesConfig {
|
|
118
|
+
// Copied key by key rather than spread: a throwing accessor on a user-supplied
|
|
119
|
+
// attribute would otherwise escape the first `startSpan`.
|
|
120
|
+
const resourceAttributes = assignUserAttributes(
|
|
121
|
+
{ ...hostResourceAttributes },
|
|
122
|
+
withUsableIdentityKeys(config?.resourceAttributes)
|
|
123
|
+
)
|
|
124
|
+
const maxExportBatchSize = positiveInteger(config?.maxExportBatchSize, DEFAULT_MAX_EXPORT_BATCH_SIZE)
|
|
125
|
+
return {
|
|
126
|
+
serviceName: (resourceAttributes?.['service.name'] as string | undefined) ?? config?.serviceName,
|
|
127
|
+
serviceVersion: (resourceAttributes?.['service.version'] as string | undefined) ?? config?.serviceVersion,
|
|
128
|
+
environment: (resourceAttributes?.['deployment.environment'] as string | undefined) ?? config?.environment,
|
|
129
|
+
resourceAttributes,
|
|
130
|
+
beforeSpanSend: resolveBeforeSpanSend(config?.beforeSpanSend, logger),
|
|
131
|
+
maxAttributesPerSpan: positiveInteger(config?.maxAttributesPerSpan, DEFAULT_MAX_ATTRIBUTES_PER_SPAN),
|
|
132
|
+
maxEventsPerSpan: positiveInteger(config?.maxEventsPerSpan, DEFAULT_MAX_EVENTS_PER_SPAN),
|
|
133
|
+
maxAttributesPerEvent: DEFAULT_MAX_ATTRIBUTES_PER_EVENT,
|
|
134
|
+
maxAttributeValueLength: positiveInteger(config?.maxAttributeValueLength, DEFAULT_MAX_ATTRIBUTE_VALUE_LENGTH),
|
|
135
|
+
flushIntervalMs: positiveInteger(config?.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS),
|
|
136
|
+
maxExportBatchSize,
|
|
137
|
+
// Never below the flush trigger, or the depth-based flush could never fire.
|
|
138
|
+
maxQueueSize: Math.max(positiveInteger(config?.maxQueueSize, DEFAULT_MAX_QUEUE_SIZE), maxExportBatchSize),
|
|
139
|
+
maxLiveSpans: positiveInteger(config?.maxLiveSpans, DEFAULT_MAX_LIVE_SPANS),
|
|
140
|
+
maxSpanAgeMs: positiveInteger(config?.maxSpanAgeMs, DEFAULT_MAX_SPAN_AGE_MS),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Span } from '@posthog/types'
|
|
2
|
+
import type { SpanContextManager } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Synchronous active-span tracking: restores the previous active span when the
|
|
6
|
+
* callback returns, which for an async callback means when it returns its
|
|
7
|
+
* promise — so spans started after an `await` won't see it as active.
|
|
8
|
+
*
|
|
9
|
+
* The fallback for runtimes with no ambient async context; Node injects an
|
|
10
|
+
* `AsyncLocalStorage`-backed manager instead. `parent` is the escape hatch.
|
|
11
|
+
*
|
|
12
|
+
* @internal Exposed for cross-package use within this SDK; not part of the stable public API.
|
|
13
|
+
*/
|
|
14
|
+
export class SyncSpanContextManager implements SpanContextManager {
|
|
15
|
+
private _active: Span | undefined
|
|
16
|
+
|
|
17
|
+
active(): Span | undefined {
|
|
18
|
+
return this._active
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
with<T>(span: Span, fn: () => T): T {
|
|
22
|
+
const previous = this._active
|
|
23
|
+
this._active = span
|
|
24
|
+
try {
|
|
25
|
+
return fn()
|
|
26
|
+
} finally {
|
|
27
|
+
this._active = previous
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getRandomBytes, isValidSpanId, isValidTraceId, newSpanId, newTraceId } from './ids'
|
|
2
|
+
|
|
3
|
+
describe('trace and span ids', () => {
|
|
4
|
+
describe('newTraceId', () => {
|
|
5
|
+
it('is 32 lowercase hex characters', () => {
|
|
6
|
+
for (let i = 0; i < 50; i++) {
|
|
7
|
+
expect(newTraceId()).toMatch(/^[0-9a-f]{32}$/)
|
|
8
|
+
}
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('is never all zeros', () => {
|
|
12
|
+
for (let i = 0; i < 50; i++) {
|
|
13
|
+
expect(newTraceId()).not.toBe('0'.repeat(32))
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('does not repeat', () => {
|
|
18
|
+
const ids = new Set(Array.from({ length: 200 }, newTraceId))
|
|
19
|
+
expect(ids.size).toBe(200)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('newSpanId', () => {
|
|
24
|
+
it('is 16 lowercase hex characters', () => {
|
|
25
|
+
for (let i = 0; i < 50; i++) {
|
|
26
|
+
expect(newSpanId()).toMatch(/^[0-9a-f]{16}$/)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('does not repeat', () => {
|
|
31
|
+
const ids = new Set(Array.from({ length: 200 }, newSpanId))
|
|
32
|
+
expect(ids.size).toBe(200)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('getRandomBytes', () => {
|
|
37
|
+
it('returns the requested length', () => {
|
|
38
|
+
expect(getRandomBytes(8)).toHaveLength(8)
|
|
39
|
+
expect(getRandomBytes(16)).toHaveLength(16)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('falls back to Math.random when crypto is unavailable', () => {
|
|
43
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'crypto')
|
|
44
|
+
// React Native has no global crypto without a polyfill — the fallback path
|
|
45
|
+
// is what keeps span ids working there.
|
|
46
|
+
Object.defineProperty(globalThis, 'crypto', { value: undefined, configurable: true })
|
|
47
|
+
try {
|
|
48
|
+
expect(newTraceId()).toMatch(/^[0-9a-f]{32}$/)
|
|
49
|
+
expect(newSpanId()).toMatch(/^[0-9a-f]{16}$/)
|
|
50
|
+
} finally {
|
|
51
|
+
if (original) {
|
|
52
|
+
Object.defineProperty(globalThis, 'crypto', original)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('falls back when getRandomValues throws', () => {
|
|
58
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'crypto')
|
|
59
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
60
|
+
value: {
|
|
61
|
+
getRandomValues: () => {
|
|
62
|
+
throw new Error('not allowed')
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
configurable: true,
|
|
66
|
+
})
|
|
67
|
+
try {
|
|
68
|
+
expect(newTraceId()).toMatch(/^[0-9a-f]{32}$/)
|
|
69
|
+
} finally {
|
|
70
|
+
if (original) {
|
|
71
|
+
Object.defineProperty(globalThis, 'crypto', original)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('never emits an all-zero id even when the random source is broken', () => {
|
|
77
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'crypto')
|
|
78
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
79
|
+
value: { getRandomValues: (array: Uint8Array) => array.fill(0) },
|
|
80
|
+
configurable: true,
|
|
81
|
+
})
|
|
82
|
+
try {
|
|
83
|
+
// The server zeroes ids it can't use, so an all-zero id would be stored
|
|
84
|
+
// and silently orphaned rather than rejected.
|
|
85
|
+
expect(newTraceId()).not.toBe('0'.repeat(32))
|
|
86
|
+
expect(newSpanId()).not.toBe('0'.repeat(16))
|
|
87
|
+
} finally {
|
|
88
|
+
if (original) {
|
|
89
|
+
Object.defineProperty(globalThis, 'crypto', original)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('validation', () => {
|
|
96
|
+
it.each([
|
|
97
|
+
['a valid trace id', '4bf92f3577b34da6a3ce929d0e0e4736', true],
|
|
98
|
+
['an all-zero trace id', '0'.repeat(32), false],
|
|
99
|
+
['a short trace id', 'abc', false],
|
|
100
|
+
['uppercase hex', '4BF92F3577B34DA6A3CE929D0E0E4736', false],
|
|
101
|
+
['a non-hex string', 'zzf92f3577b34da6a3ce929d0e0e4736', false],
|
|
102
|
+
['a non-string', 12345, false],
|
|
103
|
+
])('isValidTraceId rejects/accepts %s', (_name, value, expected) => {
|
|
104
|
+
expect(isValidTraceId(value)).toBe(expected)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it.each([
|
|
108
|
+
['a valid span id', '00f067aa0ba902b7', true],
|
|
109
|
+
['an all-zero span id', '0'.repeat(16), false],
|
|
110
|
+
['a trace-length id', '4bf92f3577b34da6a3ce929d0e0e4736', false],
|
|
111
|
+
])('isValidSpanId rejects/accepts %s', (_name, value, expected) => {
|
|
112
|
+
expect(isValidSpanId(value)).toBe(expected)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// W3C Trace Context identifier generation. Trace ids are 16 bytes, span ids 8,
|
|
2
|
+
// both lowercase hex on the JSON wire. The ingestion service *zeroes* ids that
|
|
3
|
+
// aren't exactly the right length rather than rejecting them, silently orphaning
|
|
4
|
+
// the span — so length is load-bearing and every id is validated before it ships.
|
|
5
|
+
|
|
6
|
+
const TRACE_ID_BYTES = 16
|
|
7
|
+
const SPAN_ID_BYTES = 8
|
|
8
|
+
|
|
9
|
+
const TRACE_ID_HEX = TRACE_ID_BYTES * 2
|
|
10
|
+
const SPAN_ID_HEX = SPAN_ID_BYTES * 2
|
|
11
|
+
|
|
12
|
+
const INVALID_TRACE_ID = '0'.repeat(TRACE_ID_HEX)
|
|
13
|
+
const INVALID_SPAN_ID = '0'.repeat(SPAN_ID_HEX)
|
|
14
|
+
|
|
15
|
+
const HEX_RE = /^[0-9a-f]+$/
|
|
16
|
+
|
|
17
|
+
type CryptoLike = { getRandomValues?: (array: Uint8Array) => Uint8Array }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Random bytes from the platform's CSPRNG, falling back to `Math.random`.
|
|
21
|
+
*
|
|
22
|
+
* The fallback exists for React Native, which has no global `crypto` without a
|
|
23
|
+
* polyfill. Trace ids need collision resistance, not unpredictability.
|
|
24
|
+
*/
|
|
25
|
+
export function getRandomBytes(byteLength: number): Uint8Array {
|
|
26
|
+
const bytes = new Uint8Array(byteLength)
|
|
27
|
+
const cryptoLike = (globalThis as { crypto?: CryptoLike }).crypto
|
|
28
|
+
if (cryptoLike && typeof cryptoLike.getRandomValues === 'function') {
|
|
29
|
+
try {
|
|
30
|
+
cryptoLike.getRandomValues(bytes)
|
|
31
|
+
return bytes
|
|
32
|
+
} catch {
|
|
33
|
+
// A locked-down `crypto` throws; fall through to the `Math.random` path.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < byteLength; i++) {
|
|
37
|
+
bytes[i] = Math.floor(Math.random() * 256)
|
|
38
|
+
}
|
|
39
|
+
return bytes
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
43
|
+
let hex = ''
|
|
44
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
45
|
+
hex += bytes[i].toString(16).padStart(2, '0')
|
|
46
|
+
}
|
|
47
|
+
return hex
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function randomHexId(byteLength: number): string {
|
|
51
|
+
const hex = bytesToHex(getRandomBytes(byteLength))
|
|
52
|
+
// An all-zero id is invalid per W3C and the server treats one as absent, so the
|
|
53
|
+
// span would be stored and silently orphaned. Only reachable from a broken source.
|
|
54
|
+
return /[^0]/.test(hex) ? hex : hex.slice(0, -1) + '1'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function newTraceId(): string {
|
|
58
|
+
return randomHexId(TRACE_ID_BYTES)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function newSpanId(): string {
|
|
62
|
+
return randomHexId(SPAN_ID_BYTES)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidHexId(value: unknown, length: number, invalid: string): value is string {
|
|
66
|
+
return typeof value === 'string' && value.length === length && value !== invalid && HEX_RE.test(value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isValidTraceId(value: unknown): value is string {
|
|
70
|
+
return isValidHexId(value, TRACE_ID_HEX, INVALID_TRACE_ID)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isValidSpanId(value: unknown): value is string {
|
|
74
|
+
return isValidHexId(value, SPAN_ID_HEX, INVALID_SPAN_ID)
|
|
75
|
+
}
|