diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-otlp/.gitignore
new file mode 100644
index 000000000000..ae044ec5ad53
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/.gitignore
@@ -0,0 +1,46 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+event-dumps
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Sentry Config File
+.env.sentry-build-plugin
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/api/telemetry/[id]/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/api/telemetry/[id]/route.ts
new file mode 100644
index 000000000000..5c1823c8805b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/api/telemetry/[id]/route.ts
@@ -0,0 +1,20 @@
+import { metrics, trace } from '@opentelemetry/api';
+import * as Sentry from '@sentry/nextjs';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+
+ return trace.getTracer('nextjs-otlp').startActiveSpan('telemetry-handler', span => {
+ const { traceId, spanId } = span.spanContext();
+
+ metrics.getMeter('nextjs-otlp').createCounter('otlp.test.count').add(1, { id });
+
+ Sentry.captureException(new Error(`This is an exception with id ${id}`));
+
+ span.end();
+
+ return Response.json({ traceId, spanId });
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/layout.tsx
@@ -0,0 +1,7 @@
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/page.tsx
new file mode 100644
index 000000000000..753b8859885c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/app/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Next.js app with user-owned OpenTelemetry tracing and metrics
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/eslint.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-otlp/eslint.config.mjs
new file mode 100644
index 000000000000..60f7af38f6c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/eslint.config.mjs
@@ -0,0 +1,19 @@
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { FlatCompat } from '@eslint/eslintrc';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends('next/core-web-vitals', 'next/typescript'),
+ {
+ ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
+ },
+];
+
+export default eslintConfig;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/instrumentation-client.ts
new file mode 100644
index 000000000000..115a6667cb4c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/instrumentation-client.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa',
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: 'http://localhost:3031/', // proxy server
+});
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/instrumentation.ts
new file mode 100644
index 000000000000..c3761ac260bc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/instrumentation.ts
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/nextjs';
+
+export async function register() {
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ // Order matters: the app's OpenTelemetry SDK claims the global tracer provider first, and
+ // Sentry then attaches to it instead of setting up its own.
+ await import('./otel.server.config');
+ await import('./sentry.server.config');
+ }
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts
new file mode 100644
index 000000000000..6699b3dd2c33
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/next.config.ts
@@ -0,0 +1,8 @@
+import { withSentryConfig } from '@sentry/nextjs';
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {};
+
+export default withSentryConfig(nextConfig, {
+ silent: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/otel-receiver.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/otel-receiver.ts
new file mode 100644
index 000000000000..029f85ac7903
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/otel-receiver.ts
@@ -0,0 +1,110 @@
+import { createServer } from 'node:http';
+
+export const OTLP_RECEIVER_PORT = 3033;
+
+export interface CollectedSpan {
+ traceId: string;
+ spanId: string;
+ name: string;
+}
+
+export interface CollectedMetric {
+ name: string;
+ value: number;
+ attributes: Record;
+}
+
+const collectedSpans: CollectedSpan[] = [];
+const collectedMetrics: CollectedMetric[] = [];
+
+interface OtlpAnyValue {
+ stringValue?: string;
+ intValue?: string | number;
+ doubleValue?: number;
+ boolValue?: boolean;
+}
+
+function flattenAttributes(attributes: { key: string; value: OtlpAnyValue }[] = []): Record {
+ const flattened: Record = {};
+
+ for (const { key, value } of attributes) {
+ const rawValue = value.stringValue ?? value.intValue ?? value.doubleValue ?? value.boolValue;
+ if (rawValue !== undefined) {
+ flattened[key] = String(rawValue);
+ }
+ }
+
+ return flattened;
+}
+
+function collectSpans(body: any): void {
+ for (const resourceSpan of body?.resourceSpans ?? []) {
+ for (const scopeSpan of resourceSpan.scopeSpans ?? []) {
+ for (const span of scopeSpan.spans ?? []) {
+ collectedSpans.push({ traceId: span.traceId, spanId: span.spanId, name: span.name });
+ }
+ }
+ }
+}
+
+function collectMetrics(body: any): void {
+ for (const resourceMetric of body?.resourceMetrics ?? []) {
+ for (const scopeMetric of resourceMetric.scopeMetrics ?? []) {
+ for (const metric of scopeMetric.metrics ?? []) {
+ // Only counters are recorded by this app, so `sum` is the only shape that needs handling.
+ for (const dataPoint of metric.sum?.dataPoints ?? []) {
+ collectedMetrics.push({
+ name: metric.name,
+ value: Number(dataPoint.asInt ?? dataPoint.asDouble ?? 0),
+ attributes: flattenAttributes(dataPoint.attributes),
+ });
+ }
+ }
+ }
+ }
+}
+
+async function readJsonBody(stream: AsyncIterable): Promise {
+ const chunks: Buffer[] = [];
+ for await (const chunk of stream) {
+ chunks.push(chunk);
+ }
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
+}
+
+/**
+ * Stands in for the OTLP backend the app would export to in production, so the test can assert what
+ * the user's OpenTelemetry SDK actually put on the wire.
+ *
+ * It deliberately runs as a plain `node:http` server rather than a Next.js route: exporting into the
+ * Next.js server would make every export request produce spans of its own, which would then be
+ * exported again.
+ */
+export function startOtlpReceiver(): void {
+ const server = createServer((req, res) => {
+ void (async () => {
+ if (req.method === 'POST' && req.url === '/v1/traces') {
+ collectSpans(await readJsonBody(req));
+ res.writeHead(200, { 'content-type': 'application/json' }).end('{}');
+ return;
+ }
+
+ if (req.method === 'POST' && req.url === '/v1/metrics') {
+ collectMetrics(await readJsonBody(req));
+ res.writeHead(200, { 'content-type': 'application/json' }).end('{}');
+ return;
+ }
+
+ if (req.method === 'GET' && req.url === '/collected') {
+ res
+ .writeHead(200, { 'content-type': 'application/json' })
+ .end(JSON.stringify({ spans: collectedSpans, metrics: collectedMetrics }));
+ return;
+ }
+
+ res.writeHead(404).end();
+ })();
+ });
+
+ server.listen(OTLP_RECEIVER_PORT);
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/otel.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/otel.server.config.ts
new file mode 100644
index 000000000000..b3207cce10f4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/otel.server.config.ts
@@ -0,0 +1,46 @@
+import { metrics } from '@opentelemetry/api';
+import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import { resourceFromAttributes } from '@opentelemetry/resources';
+import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
+import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
+import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
+import { OTLP_RECEIVER_PORT, startOtlpReceiver } from './otel-receiver';
+
+// Next.js can run `register()` more than once in dev, which would leave a second receiver fighting
+// for the port and a second set of providers losing the race to register globally.
+const globalWithOtelFlag = globalThis as typeof globalThis & { __otelRegistered?: boolean };
+
+if (!globalWithOtelFlag.__otelRegistered) {
+ globalWithOtelFlag.__otelRegistered = true;
+
+ startOtlpReceiver();
+
+ const resource = resourceFromAttributes({ 'service.name': 'nextjs-otlp' });
+ const otlpBaseUrl = `http://localhost:${OTLP_RECEIVER_PORT}`;
+
+ // The user owns tracing: this registers the global tracer provider, context manager and
+ // propagator. Sentry is initialized afterwards with `enableOpenTelemetrySetup: false` so it does
+ // not contend for any of them.
+ new NodeTracerProvider({
+ resource,
+ spanProcessors: [
+ new BatchSpanProcessor(new OTLPTraceExporter({ url: `${otlpBaseUrl}/v1/traces` }), {
+ scheduledDelayMillis: 100,
+ }),
+ ],
+ }).register();
+
+ metrics.setGlobalMeterProvider(
+ new MeterProvider({
+ resource,
+ readers: [
+ new PeriodicExportingMetricReader({
+ exporter: new OTLPMetricExporter({ url: `${otlpBaseUrl}/v1/metrics` }),
+ exportIntervalMillis: 500,
+ exportTimeoutMillis: 500,
+ }),
+ ],
+ }),
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json b/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json
new file mode 100644
index 000000000000..96764928cfa5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "nextjs-otlp",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "dev:webpack": "next dev --webpack",
+ "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
+ "build-webpack": "next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
+ "start": "next start",
+ "lint": "eslint",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:dev": "TEST_ENV=development playwright test",
+ "test:dev-webpack": "TEST_ENV=development-webpack playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-webpack": "pnpm install && pnpm build-webpack",
+ "test:assert": "pnpm test:prod && pnpm test:dev",
+ "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
+ },
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0",
+ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
+ "@opentelemetry/resources": "^2.9.0",
+ "@opentelemetry/sdk-metrics": "^2.9.0",
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
+ "@opentelemetry/sdk-trace-node": "^2.9.0",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "import-in-the-middle": "^2",
+ "next": "16.3.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "require-in-the-middle": "^8"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "^16",
+ "typescript": "^5"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sentryTest": {
+ "variants": [
+ {
+ "build-command": "pnpm test:build-webpack",
+ "label": "nextjs-otlp (webpack)",
+ "assert-command": "pnpm test:assert-webpack"
+ }
+ ]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-otlp/playwright.config.mjs
new file mode 100644
index 000000000000..f727698c0983
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/playwright.config.mjs
@@ -0,0 +1,30 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const getStartCommand = () => {
+ if (testEnv === 'development') {
+ return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs';
+ }
+
+ if (testEnv === 'development-webpack') {
+ return 'pnpm next dev -p 3030 --webpack 2>&1 | tee .tmp_dev_server_logs';
+ }
+
+ if (testEnv === 'production') {
+ return 'pnpm next start -p 3030';
+ }
+
+ throw new Error(`Unknown test env: ${testEnv}`);
+};
+
+const config = getPlaywrightConfig({
+ startCommand: getStartCommand(),
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/sentry.server.config.ts
new file mode 100644
index 000000000000..8aac811122de
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/sentry.server.config.ts
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa',
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: 'http://localhost:3031/', // proxy server
+
+ // Errors only: no `tracesSampleRate`, so Sentry starts no spans and sends no transactions.
+
+ // The app brings its own OpenTelemetry SDK, which already owns the global tracer provider,
+ // context manager and propagator.
+ enableOpenTelemetrySetup: false,
+
+ // Puts the active OpenTelemetry span's trace on everything Sentry sends.
+ integrations: [Sentry.otlpIntegration()],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-otlp/start-event-proxy.mjs
new file mode 100644
index 000000000000..40613495946b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nextjs-otlp',
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/tests/otel-telemetry.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-otlp/tests/otel-telemetry.test.ts
new file mode 100644
index 000000000000..ce87c41ea2ef
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/tests/otel-telemetry.test.ts
@@ -0,0 +1,119 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+const OTLP_RECEIVER_URL = 'http://localhost:3033';
+
+interface CollectedSpan {
+ traceId: string;
+ spanId: string;
+ name: string;
+}
+
+interface CollectedMetric {
+ name: string;
+ value: number;
+ attributes: Record;
+}
+
+async function triggerTelemetry(baseURL: string, id: string): Promise<{ traceId: string; spanId: string }> {
+ const response = await fetch(`${baseURL}/api/telemetry/${id}`);
+ return (await response.json()) as { traceId: string; spanId: string };
+}
+
+interface Collected {
+ spans: CollectedSpan[];
+ metrics: CollectedMetric[];
+}
+
+async function waitForCollected(select: (collected: Collected) => T | undefined, description: string): Promise {
+ const deadline = Date.now() + 15_000;
+
+ while (Date.now() < deadline) {
+ const response = await fetch(`${OTLP_RECEIVER_URL}/collected`);
+ const collected = (await response.json()) as Collected;
+
+ const match = select(collected);
+ if (match !== undefined) {
+ return match;
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 200));
+ }
+
+ throw new Error(`Timed out waiting for ${description} to be exported over OTLP`);
+}
+
+const waitForExportedMetric = (id: string): Promise =>
+ waitForCollected(
+ ({ metrics }) => metrics.find(metric => metric.name === 'otlp.test.count' && metric.attributes.id === id),
+ `the metric for id ${id}`,
+ );
+
+const waitForExportedSpan = (spanId: string): Promise =>
+ waitForCollected(({ spans }) => spans.find(span => span.spanId === spanId), `the span ${spanId}`);
+
+test('stamps errors with the trace of the active OpenTelemetry span', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('nextjs-otlp', event => {
+ return event.exception?.values?.[0]?.value === 'This is an exception with id 123';
+ });
+
+ const { traceId, spanId } = await triggerTelemetry(baseURL as string, '123');
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.contexts?.trace).toEqual({ trace_id: traceId, span_id: spanId });
+});
+
+test('keeps exporting the app-owned metrics over OTLP', async ({ baseURL }) => {
+ await triggerTelemetry(baseURL as string, '234');
+
+ const metric = await waitForExportedMetric('234');
+
+ expect(metric).toEqual({ name: 'otlp.test.count', value: 1, attributes: { id: '234' } });
+});
+
+test('keeps exporting the app-owned spans over OTLP', async ({ baseURL }) => {
+ const { traceId, spanId } = await triggerTelemetry(baseURL as string, '345');
+
+ const span = await waitForExportedSpan(spanId);
+
+ expect(span).toEqual({ traceId, spanId, name: 'telemetry-handler' });
+});
+
+test('sends no transactions to Sentry', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction('nextjs-otlp', () => true);
+ const errorPromise = waitForError('nextjs-otlp', event => {
+ return event.exception?.values?.[0]?.value === 'This is an exception with id 456';
+ });
+
+ await triggerTelemetry(baseURL as string, '456');
+ // Proves the request's telemetry reached the proxy, so the absence check below is not vacuous.
+ await errorPromise;
+
+ // Absence can only be time bounded. This guards against Sentry's tracing defaults changing under
+ // the app, which would emit a transaction for every request, well inside this window.
+ const transaction = await Promise.race([
+ transactionPromise,
+ new Promise(resolve => setTimeout(() => resolve(undefined), 3000)),
+ ]);
+
+ expect(transaction).toBeUndefined();
+});
+
+test('keeps concurrent requests on separate traces', async ({ baseURL }) => {
+ const errorEventPromises = ['567', '678'].map(id =>
+ waitForError('nextjs-otlp', event => {
+ return event.exception?.values?.[0]?.value === `This is an exception with id ${id}`;
+ }),
+ );
+
+ const [first, second] = await Promise.all([
+ triggerTelemetry(baseURL as string, '567'),
+ triggerTelemetry(baseURL as string, '678'),
+ ]);
+
+ const [firstError, secondError] = await Promise.all(errorEventPromises);
+
+ expect(first.traceId).not.toBe(second.traceId);
+ expect(firstError.contexts?.trace?.trace_id).toBe(first.traceId);
+ expect(secondError.contexts?.trace?.trace_id).toBe(second.traceId);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-otlp/tsconfig.json
new file mode 100644
index 000000000000..cc9ed39b5aa2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts
index 94ca87dc1ef1..369f1d95d8fe 100644
--- a/packages/core/src/carrier.ts
+++ b/packages/core/src/carrier.ts
@@ -43,6 +43,9 @@ export interface SentryCarrier {
/** Strategy for assembling segment spans into transactions; set by SDKs that defer capture. */
segmentSpanCaptureStrategy?: SegmentSpanCaptureStrategy;
+ /** Supplies trace context from a non-Sentry source (e.g. OpenTelemetry); set by `otlpIntegration`. */
+ externalPropagationContextProvider?: () => { traceId: string; spanId: string } | undefined;
+
/** Overwrites TextEncoder used in `@sentry/core`, need for `react-native@0.73` and older */
encodePolyfill?: (input: string) => Uint8Array;
/** Overwrites TextDecoder used in `@sentry/core`, need for `react-native@0.73` and older */
diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts
index b66f3fac6e66..dd8e49c9c5d4 100644
--- a/packages/core/src/currentScopes.ts
+++ b/packages/core/src/currentScopes.ts
@@ -1,33 +1,34 @@
import { getAsyncContextStrategy } from './asyncContext';
-import { getGlobalSingleton, getMainCarrier } from './carrier';
+import { getGlobalSingleton, getMainCarrier, getSentryCarrier } from './carrier';
import type { Client } from './client';
import { Scope } from './scope';
import type { TraceContext } from './types/context';
import { generateSpanId } from './utils/propagationContext';
-let _externalPropagationContextProvider: (() => { traceId: string; spanId: string } | undefined) | undefined;
-
/**
* Register an external propagation context provider function.
* When registered, trace context will be read from the external source (e.g. OpenTelemetry)
* instead of from the Sentry scope's propagation context.
*/
export function registerExternalPropagationContext(fn: () => { traceId: string; spanId: string } | undefined): void {
- _externalPropagationContextProvider = fn;
+ // Kept on the carrier rather than in module state: bundlers routinely emit more than one copy of
+ // `@sentry/core` (e.g. one per Next.js server chunk), and the copy the integration registers on is
+ // usually not the copy that reads it back when an event is assembled.
+ getSentryCarrier(getMainCarrier()).externalPropagationContextProvider = fn;
}
/**
* Get the external propagation context, if a provider has been registered.
*/
export function getExternalPropagationContext(): { traceId: string; spanId: string } | undefined {
- return _externalPropagationContextProvider?.();
+ return getSentryCarrier(getMainCarrier()).externalPropagationContextProvider?.();
}
/**
* Check if an external propagation context provider has been registered.
*/
export function hasExternalPropagationContext(): boolean {
- return _externalPropagationContextProvider !== undefined;
+ return getSentryCarrier(getMainCarrier()).externalPropagationContextProvider !== undefined;
}
/**