From ed78bdee149ef4b4ba5406a22b8af9965fcc472b Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 27 May 2026 14:13:45 +0200 Subject: [PATCH 1/2] ref(node): Refactor `hrTime` and related usage in vendored code Getting rid of this usage from `@opentelemetry/core`. --- .agents/skills/vendor-otel/SKILL.md | 2 ++ .../src/integration/aws/vendored/aws-sdk.ts | 6 +++--- .../aws/vendored/services/ServiceExtension.ts | 4 ++-- .../aws/vendored/services/ServicesExtensions.ts | 4 ++-- .../aws/vendored/services/bedrock-runtime.ts | 16 ++++++++-------- .../integrations/node-fetch/vendored/undici.ts | 13 ++++++------- .../tracing/amqplib/vendored/amqplib.ts | 10 +++++----- .../tracing/amqplib/vendored/utils.ts | 4 ++-- .../tracing/postgres/vendored/instrumentation.ts | 10 ++++------ 9 files changed, 34 insertions(+), 35 deletions(-) diff --git a/.agents/skills/vendor-otel/SKILL.md b/.agents/skills/vendor-otel/SKILL.md index 3f9555b32349..309b7701d757 100644 --- a/.agents/skills/vendor-otel/SKILL.md +++ b/.agents/skills/vendor-otel/SKILL.md @@ -9,6 +9,8 @@ description: Vendor an OpenTelemetry instrumentation package into the Sentry Jav Copy upstream OTel instrumentation TypeScript source into a `vendored/` directory, remove the npm dependency, and ensure builds and tests pass. No logic changes — the vendored code must behave identically to the original. +**Scope of this rule:** "No logic changes" applies **only to the initial vendoring PR**. After a package has been vendored, the `vendored/` directory is Sentry-owned source and follow-up PRs may refactor, simplify, replace upstream utilities with Sentry equivalents (e.g. `@opentelemetry/core` → `@sentry/core`), or otherwise diverge from upstream. Such cleanup is desired, not discouraged. + ## 1. Research Find upstream source files: diff --git a/packages/aws-serverless/src/integration/aws/vendored/aws-sdk.ts b/packages/aws-serverless/src/integration/aws/vendored/aws-sdk.ts index 0fb87e79186d..b20c1387d72a 100644 --- a/packages/aws-serverless/src/integration/aws/vendored/aws-sdk.ts +++ b/packages/aws-serverless/src/integration/aws/vendored/aws-sdk.ts @@ -20,7 +20,7 @@ /* eslint-disable */ import { Span, SpanKind, context, trace, diag, SpanStatusCode } from '@opentelemetry/api'; -import { hrTime, suppressTracing } from '@opentelemetry/core'; +import { suppressTracing } from '@opentelemetry/core'; import { AttributeNames } from './enums'; import { ServicesExtensions } from './services'; import { @@ -57,7 +57,7 @@ import { propwrap } from './propwrap'; import { RequestMetadata } from './services/ServiceExtension'; import { ATTR_HTTP_STATUS_CODE } from './semconv'; import { ATTR_HTTP_RESPONSE_STATUS_CODE } from '@opentelemetry/semantic-conventions'; -import { SDK_VERSION } from '@sentry/core'; +import { SDK_VERSION, timestampInSeconds } from '@sentry/core'; const PACKAGE_NAME = '@sentry/instrumentation-aws-sdk'; @@ -298,7 +298,7 @@ export class AwsInstrumentation extends InstrumentationBase any | undefined; updateMetricInstruments?: (meter: Meter) => void; diff --git a/packages/aws-serverless/src/integration/aws/vendored/services/ServicesExtensions.ts b/packages/aws-serverless/src/integration/aws/vendored/services/ServicesExtensions.ts index a7b7e6f2d05e..c055154a8a4f 100644 --- a/packages/aws-serverless/src/integration/aws/vendored/services/ServicesExtensions.ts +++ b/packages/aws-serverless/src/integration/aws/vendored/services/ServicesExtensions.ts @@ -19,7 +19,7 @@ */ /* eslint-disable */ -import { Tracer, Span, DiagLogger, Meter, HrTime } from '@opentelemetry/api'; +import { Tracer, Span, DiagLogger, Meter } from '@opentelemetry/api'; import { SemconvStability } from '@opentelemetry/instrumentation'; import { ServiceExtension, RequestMetadata } from './ServiceExtension'; import { SqsServiceExtension } from './sqs'; @@ -77,7 +77,7 @@ export class ServicesExtensions implements ServiceExtension { span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig, - startTime: HrTime, + startTime: number, ) { const serviceExtension = this.services.get(response.request.serviceName); diff --git a/packages/aws-serverless/src/integration/aws/vendored/services/bedrock-runtime.ts b/packages/aws-serverless/src/integration/aws/vendored/services/bedrock-runtime.ts index 77e9ada8960f..3c4d58e63302 100644 --- a/packages/aws-serverless/src/integration/aws/vendored/services/bedrock-runtime.ts +++ b/packages/aws-serverless/src/integration/aws/vendored/services/bedrock-runtime.ts @@ -19,7 +19,7 @@ */ /* eslint-disable */ -import { Attributes, DiagLogger, diag, Histogram, HrTime, Meter, Span, Tracer, ValueType } from '@opentelemetry/api'; +import { Attributes, DiagLogger, diag, Histogram, Meter, Span, Tracer, ValueType } from '@opentelemetry/api'; import { RequestMetadata, ServiceExtension } from './ServiceExtension'; import { ATTR_GEN_AI_SYSTEM, @@ -41,7 +41,7 @@ import { METRIC_GEN_AI_CLIENT_TOKEN_USAGE, } from '../semconv'; import { AwsSdkInstrumentationConfig, NormalizedRequest, NormalizedResponse } from '../types'; -import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; +import { timestampInSeconds } from '@sentry/core'; // Simplified types inlined from @aws-sdk/client-bedrock-runtime // Only the fields accessed by this instrumentation are included @@ -295,7 +295,7 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig, - startTime: HrTime, + startTime: number, ) { if (!span.isRecording()) { return; @@ -318,7 +318,7 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig, - startTime: HrTime, + startTime: number, ) { const { stopReason, usage } = response.data; @@ -331,7 +331,7 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { span: Span, tracer: Tracer, config: AwsSdkInstrumentationConfig, - startTime: HrTime, + startTime: number, ) { return { ...response.data, @@ -345,7 +345,7 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { response: NormalizedResponse, stream: AsyncIterable, span: Span, - startTime: HrTime, + startTime: number, ) { try { let usage: TokenUsage | undefined; @@ -366,14 +366,14 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { } } - private setUsage(response: NormalizedResponse, span: Span, usage: TokenUsage | undefined, startTime: HrTime) { + private setUsage(response: NormalizedResponse, span: Span, usage: TokenUsage | undefined, startTime: number) { const sharedMetricAttrs: Attributes = { [ATTR_GEN_AI_SYSTEM]: GEN_AI_SYSTEM_VALUE_AWS_BEDROCK, [ATTR_GEN_AI_OPERATION_NAME]: GEN_AI_OPERATION_NAME_VALUE_CHAT, [ATTR_GEN_AI_REQUEST_MODEL]: response.request.commandInput.modelId, }; - const durationSecs = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1000; + const durationSecs = timestampInSeconds() - startTime; this.operationDuration.record(durationSecs, sharedMetricAttrs); if (usage) { diff --git a/packages/node/src/integrations/node-fetch/vendored/undici.ts b/packages/node/src/integrations/node-fetch/vendored/undici.ts index 55e09d7c4d53..79aef5ee8964 100644 --- a/packages/node/src/integrations/node-fetch/vendored/undici.ts +++ b/packages/node/src/integrations/node-fetch/vendored/undici.ts @@ -25,7 +25,7 @@ import * as diagch from 'diagnostics_channel'; import { URL } from 'url'; import { InstrumentationBase, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; -import type { Attributes, Histogram, HrTime, Span } from '@opentelemetry/api'; +import type { Attributes, Histogram, Span } from '@opentelemetry/api'; import { context, INVALID_SPAN_CONTEXT, @@ -35,7 +35,6 @@ import { trace, ValueType, } from '@opentelemetry/api'; -import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, @@ -62,12 +61,12 @@ import type { } from './internal-types'; import type { UndiciInstrumentationConfig, UndiciRequest } from './types'; -import { SDK_VERSION } from '@sentry/core'; +import { SDK_VERSION, timestampInSeconds } from '@sentry/core'; interface InstrumentationRecord { span: Span; attributes: Attributes; - startTime: HrTime; + startTime: number; } const PACKAGE_NAME = '@sentry/instrumentation-undici'; @@ -225,7 +224,7 @@ export class UndiciInstrumentation extends InstrumentationBase Date: Wed, 27 May 2026 14:22:53 +0200 Subject: [PATCH 2/2] ref(opentelemetry): Vendor minimal `TraceState` implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the `TraceState` import from `@opentelemetry/core` with a small in-tree implementation under `packages/opentelemetry/src/utils/`. The SDK only ever calls `new TraceState()` and chains `.set()`/`.get()`/ `.serialize()` on it, so the vendored version drops raw-string parsing, key/value validation, and the W3C length/item caps — none of which apply to keys we control. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/opentelemetry/src/sampler.ts | 2 +- .../opentelemetry/src/utils/TraceState.ts | 71 +++++++++++++++++++ .../opentelemetry/src/utils/makeTraceState.ts | 2 +- .../opentelemetry/test/contextManager.test.ts | 2 +- .../test/integration/transactions.test.ts | 2 +- packages/opentelemetry/test/sampler.test.ts | 2 +- .../test/utils/TraceState.test.ts | 44 ++++++++++++ 7 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 packages/opentelemetry/src/utils/TraceState.ts create mode 100644 packages/opentelemetry/test/utils/TraceState.test.ts diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 05dc0758458b..be873aca3d51 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -1,7 +1,7 @@ /* eslint-disable complexity */ import type { Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api'; import { isSpanContextValid, SpanKind, trace } from '@opentelemetry/api'; -import { TraceState } from '@opentelemetry/core'; +import { TraceState } from './utils/TraceState'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { diff --git a/packages/opentelemetry/src/utils/TraceState.ts b/packages/opentelemetry/src/utils/TraceState.ts new file mode 100644 index 000000000000..326371f03093 --- /dev/null +++ b/packages/opentelemetry/src/utils/TraceState.ts @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Minimal vendored implementation of `TraceState` from `@opentelemetry/core` + * to avoid pulling in that dependency for a single class. + * - Drops raw-string parsing and key/value validation, neither of which are + * used by the SDK — the W3C `tracestate` header is parsed by OTel's own + * propagators (which use their own `TraceState`), and every key we `set` + * is a known constant. + */ +import type { TraceState as TraceStateInterface } from '@opentelemetry/api'; + +/** + * Minimal implementation of the W3C `tracestate` field as a `@opentelemetry/api` + * `TraceState`. New entries are inserted at the front of the list, and updating + * an existing key moves it to the front. + * + * See https://www.w3.org/TR/trace-context/#tracestate-field for the field spec. + */ +export class TraceState implements TraceStateInterface { + private _internalState: Map = new Map(); + + /** @inheritDoc */ + public set(key: string, value: string): TraceState { + const next = this._clone(); + if (next._internalState.has(key)) { + next._internalState.delete(key); + } + next._internalState.set(key, value); + return next; + } + + /** @inheritDoc */ + public unset(key: string): TraceState { + const next = this._clone(); + next._internalState.delete(key); + return next; + } + + /** @inheritDoc */ + public get(key: string): string | undefined { + return this._internalState.get(key); + } + + /** @inheritDoc */ + public serialize(): string { + return Array.from(this._internalState.keys()) + .reverse() + .map(key => `${key}=${this._internalState.get(key)}`) + .join(','); + } + + private _clone(): TraceState { + const next = new TraceState(); + next._internalState = new Map(this._internalState); + return next; + } +} diff --git a/packages/opentelemetry/src/utils/makeTraceState.ts b/packages/opentelemetry/src/utils/makeTraceState.ts index 5d7d6248de56..e88d7e882657 100644 --- a/packages/opentelemetry/src/utils/makeTraceState.ts +++ b/packages/opentelemetry/src/utils/makeTraceState.ts @@ -1,7 +1,7 @@ -import { TraceState } from '@opentelemetry/core'; import type { DynamicSamplingContext } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/core'; import { SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from '../constants'; +import { TraceState } from './TraceState'; /** * Generate a TraceState for the given data. diff --git a/packages/opentelemetry/test/contextManager.test.ts b/packages/opentelemetry/test/contextManager.test.ts index 4b871ac3e7bb..795c017dd456 100644 --- a/packages/opentelemetry/test/contextManager.test.ts +++ b/packages/opentelemetry/test/contextManager.test.ts @@ -1,5 +1,5 @@ import { context, trace, TraceFlags } from '@opentelemetry/api'; -import { TraceState } from '@opentelemetry/core'; +import { TraceState } from '../src/utils/TraceState'; import { afterEach, describe, expect, it } from 'vitest'; import { SENTRY_TRACE_STATE_CHILD_IGNORED } from '../src/constants'; import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 570df4a86aa8..aae975d8d8a9 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -1,6 +1,6 @@ import type { SpanContext } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; -import { TraceState } from '@opentelemetry/core'; +import { TraceState } from '../../src/utils/TraceState'; import type { Event, TransactionEvent } from '@sentry/core'; import { addBreadcrumb, diff --git a/packages/opentelemetry/test/sampler.test.ts b/packages/opentelemetry/test/sampler.test.ts index 55c3cff8ac32..37c444a7b8d4 100644 --- a/packages/opentelemetry/test/sampler.test.ts +++ b/packages/opentelemetry/test/sampler.test.ts @@ -1,5 +1,5 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; -import { TraceState } from '@opentelemetry/core'; +import { TraceState } from '../src/utils/TraceState'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { ATTR_HTTP_REQUEST_METHOD } from '@opentelemetry/semantic-conventions'; import { generateSpanId, generateTraceId } from '@sentry/core'; diff --git a/packages/opentelemetry/test/utils/TraceState.test.ts b/packages/opentelemetry/test/utils/TraceState.test.ts new file mode 100644 index 000000000000..779b4604e8c3 --- /dev/null +++ b/packages/opentelemetry/test/utils/TraceState.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { TraceState } from '../../src/utils/TraceState'; + +describe('TraceState', () => { + it('returns undefined for unknown keys', () => { + expect(new TraceState().get('missing')).toBeUndefined(); + }); + + it('set returns a new instance and leaves the original unchanged', () => { + const original = new TraceState(); + const next = original.set('a', '1'); + + expect(next).not.toBe(original); + expect(original.get('a')).toBeUndefined(); + expect(next.get('a')).toBe('1'); + }); + + it('moves an updated key to the front of the serialized list', () => { + const state = new TraceState().set('a', '1').set('b', '2').set('a', '3'); + + expect(state.get('a')).toBe('3'); + expect(state.serialize()).toBe('a=3,b=2'); + }); + + it('serializes newest entries first', () => { + const state = new TraceState().set('a', '1').set('b', '2').set('c', '3'); + + expect(state.serialize()).toBe('c=3,b=2,a=1'); + }); + + it('unset removes the key and returns a new instance', () => { + const state = new TraceState().set('a', '1').set('b', '2'); + const next = state.unset('a'); + + expect(next).not.toBe(state); + expect(state.get('a')).toBe('1'); + expect(next.get('a')).toBeUndefined(); + expect(next.serialize()).toBe('b=2'); + }); + + it('serializes an empty state to an empty string', () => { + expect(new TraceState().serialize()).toBe(''); + }); +});