Skip to content

Commit 7e2443e

Browse files
mydeaclaude
andauthored
ref(opentelemetry): Vendor minimal TraceState implementation (#21192)
## Summary - Adds a small in-tree `TraceState` class under `packages/opentelemetry/src/utils/TraceState.ts` implementing `api.TraceState` from `@opentelemetry/api`. - Swaps the four `import { TraceState } from '@opentelemetry/core'` sites (`sampler.ts`, `makeTraceState.ts`, and three test files) to use the vendored version. ## Why The SDK only ever calls `new TraceState()` and chains `.set()`, `.get()`, `.unset()`, and `.serialize()` on the result — none of the upstream class's heavier behavior (raw `tracestate` header parsing, key/value validation, W3C length/item caps) ever runs against our inputs. Inlining the ~40 lines we actually use lets us drop one consumer of `@opentelemetry/core` from a heavily-imported file without behavior change. ## Deliberately dropped from upstream - Raw-string parsing in the constructor — the W3C `tracestate` header is parsed by OTel's own `W3CTraceContextPropagator`, which uses its own `TraceState`. Our class is never constructed from a raw header. - Key/value validation — we only `set` known `SENTRY_TRACE_STATE_*` constants. - `MAX_TRACE_STATE_ITEMS` / `MAX_TRACE_STATE_LEN` truncation — only relevant when parsing input. `@opentelemetry/core` remains a dependency because `propagator.ts`, `resource.ts`, `trace.ts`, and `utils/suppressTracing.ts` still use `isTracingSuppressed`, `W3CBaggagePropagator`, `SDK_INFO`, and `suppressTracing`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5df0161 commit 7e2443e

7 files changed

Lines changed: 120 additions & 5 deletions

File tree

packages/opentelemetry/src/sampler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable complexity */
22
import type { Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api';
33
import { isSpanContextValid, SpanKind, trace } from '@opentelemetry/api';
4-
import { TraceState } from '@opentelemetry/core';
4+
import { TraceState } from './utils/TraceState';
55
import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base';
66
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';
77
import {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* NOTICE from the Sentry authors:
17+
* - Minimal vendored implementation of `TraceState` from `@opentelemetry/core`
18+
* to avoid pulling in that dependency for a single class.
19+
* - Drops raw-string parsing and key/value validation, neither of which are
20+
* used by the SDK — the W3C `tracestate` header is parsed by OTel's own
21+
* propagators (which use their own `TraceState`), and every key we `set`
22+
* is a known constant.
23+
*/
24+
import type { TraceState as TraceStateInterface } from '@opentelemetry/api';
25+
26+
/**
27+
* Minimal implementation of the W3C `tracestate` field as a `@opentelemetry/api`
28+
* `TraceState`. New entries are inserted at the front of the list, and updating
29+
* an existing key moves it to the front.
30+
*
31+
* See https://www.w3.org/TR/trace-context/#tracestate-field for the field spec.
32+
*/
33+
export class TraceState implements TraceStateInterface {
34+
private _internalState: Map<string, string> = new Map();
35+
36+
/** @inheritDoc */
37+
public set(key: string, value: string): TraceState {
38+
const next = this._clone();
39+
if (next._internalState.has(key)) {
40+
next._internalState.delete(key);
41+
}
42+
next._internalState.set(key, value);
43+
return next;
44+
}
45+
46+
/** @inheritDoc */
47+
public unset(key: string): TraceState {
48+
const next = this._clone();
49+
next._internalState.delete(key);
50+
return next;
51+
}
52+
53+
/** @inheritDoc */
54+
public get(key: string): string | undefined {
55+
return this._internalState.get(key);
56+
}
57+
58+
/** @inheritDoc */
59+
public serialize(): string {
60+
return Array.from(this._internalState.keys())
61+
.reverse()
62+
.map(key => `${key}=${this._internalState.get(key)}`)
63+
.join(',');
64+
}
65+
66+
private _clone(): TraceState {
67+
const next = new TraceState();
68+
next._internalState = new Map(this._internalState);
69+
return next;
70+
}
71+
}

packages/opentelemetry/src/utils/makeTraceState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { TraceState } from '@opentelemetry/core';
21
import type { DynamicSamplingContext } from '@sentry/core';
32
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/core';
43
import { SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from '../constants';
4+
import { TraceState } from './TraceState';
55

66
/**
77
* Generate a TraceState for the given data.

packages/opentelemetry/test/contextManager.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { context, trace, TraceFlags } from '@opentelemetry/api';
2-
import { TraceState } from '@opentelemetry/core';
2+
import { TraceState } from '../src/utils/TraceState';
33
import { afterEach, describe, expect, it } from 'vitest';
44
import { SENTRY_TRACE_STATE_CHILD_IGNORED } from '../src/constants';
55
import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit';

packages/opentelemetry/test/integration/transactions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SpanContext } from '@opentelemetry/api';
22
import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api';
3-
import { TraceState } from '@opentelemetry/core';
3+
import { TraceState } from '../../src/utils/TraceState';
44
import type { Event, TransactionEvent } from '@sentry/core';
55
import {
66
addBreadcrumb,

packages/opentelemetry/test/sampler.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api';
2-
import { TraceState } from '@opentelemetry/core';
2+
import { TraceState } from '../src/utils/TraceState';
33
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';
44
import { ATTR_HTTP_REQUEST_METHOD } from '@opentelemetry/semantic-conventions';
55
import { generateSpanId, generateTraceId } from '@sentry/core';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { TraceState } from '../../src/utils/TraceState';
3+
4+
describe('TraceState', () => {
5+
it('returns undefined for unknown keys', () => {
6+
expect(new TraceState().get('missing')).toBeUndefined();
7+
});
8+
9+
it('set returns a new instance and leaves the original unchanged', () => {
10+
const original = new TraceState();
11+
const next = original.set('a', '1');
12+
13+
expect(next).not.toBe(original);
14+
expect(original.get('a')).toBeUndefined();
15+
expect(next.get('a')).toBe('1');
16+
});
17+
18+
it('moves an updated key to the front of the serialized list', () => {
19+
const state = new TraceState().set('a', '1').set('b', '2').set('a', '3');
20+
21+
expect(state.get('a')).toBe('3');
22+
expect(state.serialize()).toBe('a=3,b=2');
23+
});
24+
25+
it('serializes newest entries first', () => {
26+
const state = new TraceState().set('a', '1').set('b', '2').set('c', '3');
27+
28+
expect(state.serialize()).toBe('c=3,b=2,a=1');
29+
});
30+
31+
it('unset removes the key and returns a new instance', () => {
32+
const state = new TraceState().set('a', '1').set('b', '2');
33+
const next = state.unset('a');
34+
35+
expect(next).not.toBe(state);
36+
expect(state.get('a')).toBe('1');
37+
expect(next.get('a')).toBeUndefined();
38+
expect(next.serialize()).toBe('b=2');
39+
});
40+
41+
it('serializes an empty state to an empty string', () => {
42+
expect(new TraceState().serialize()).toBe('');
43+
});
44+
});

0 commit comments

Comments
 (0)