diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json index 04a6a80a500c..21beacf9fda5 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json @@ -11,7 +11,7 @@ "test:assert": "pnpm test" }, "dependencies": { - "@opentelemetry/api": "1.9.0", + "@opentelemetry/api": "^1.10.0", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/instrumentation-undici": "0.24.0", diff --git a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts index e480bbc1b68f..2003872d395c 100644 --- a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts +++ b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts @@ -1,7 +1,7 @@ import { exec } from 'child_process'; -import { existsSync } from 'fs'; +import { existsSync, readFileSync } from 'fs'; import { cp, mkdir, readFile, rm, writeFile } from 'fs/promises'; -import { basename, join } from 'path'; +import { basename, dirname, join } from 'path'; import { promisify } from 'util'; import { afterAll, beforeAll, test, type TestAPI } from 'vitest'; import { CLEANUP_STEPS, createRunner } from './createRunner'; @@ -292,6 +292,9 @@ experimentalUseDiagnosticsChannelInjection();`, private: true, version: '0.0.0', dependencies: additionalDependencies, + // Packages like `ai` depend on `@opentelemetry/api` themselves. If npm resolves a newer minor than the SDK's + // copy, the newer copy refuses the globals registered by the older one and the package's spans are dropped. + overrides: additionalDependencies['@opentelemetry/api'] ? {} : { '@opentelemetry/api': getSdkOtelApiVersion() }, } as const; await writeFile(join(tmpDirPath, 'package.json'), JSON.stringify(packageJson, null, 2)); @@ -369,6 +372,22 @@ function convertEsmToCjs(content: string): string { return newContent; } +function getSdkOtelApiVersion(): string { + const otelApiEntry = require.resolve('@opentelemetry/api', { paths: [dirname(require.resolve('@sentry/node'))] }); + let dir = dirname(otelApiEntry); + while (dir !== dirname(dir)) { + const packageJsonPath = join(dir, 'package.json'); + if (existsSync(packageJsonPath)) { + const { name, version } = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { name?: string; version: string }; + if (name === '@opentelemetry/api') { + return version; + } + } + dir = dirname(dir); + } + throw new Error(`Could not find the @opentelemetry/api version used by @sentry/node (resolved ${otelApiEntry})`); +} + const NPM_INSTALL_MAX_RETRIES = 3; const NPM_INSTALL_RETRY_DELAY_MS = 2_000; diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 0d7b2acabad1..96d766782f9a 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api'; +import type { Attributes, Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api'; import { isSpanContextValid, SpanKind, trace } from '@opentelemetry/api'; import { TraceState } from './utils/TraceState'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; @@ -277,7 +277,9 @@ export function wrapSamplingDecision({ }: { decision: SamplingDecision | undefined; context: Context; - spanAttributes: SpanAttributes; + // Typed as OTel `Attributes` so custom samplers can pass their `shouldSample` attributes through, + // whichever `@opentelemetry/api` version they use (1.10 widened attribute values to `AnyValue`). + spanAttributes: Attributes; sampleRand?: number; downstreamTraceSampleRate?: number; // flags for ignored streamed spans (only set for span streaming) @@ -319,7 +321,7 @@ export function wrapSamplingDecision({ return { decision, traceState }; } -function getBaseTraceState(context: Context, spanAttributes: SpanAttributes): TraceStateInterface { +function getBaseTraceState(context: Context, spanAttributes: Attributes): TraceStateInterface { const parentSpan = trace.getSpan(context); const parentContext = parentSpan?.spanContext();