diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js index 2c123cbb12cc..d4a232fdf46b 100644 --- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js @@ -3,6 +3,7 @@ require('./tracing'); const Sentry = require('@sentry/node-experimental'); const { fastify } = require('fastify'); const fastifyPlugin = require('fastify-plugin'); +const http = require('http'); const FastifySentry = fastifyPlugin(async (fastify, options) => { fastify.decorateRequest('_sentryContext', null); @@ -25,14 +26,24 @@ app.get('/test-param/:param', function (req, res) { res.send({ paramWas: req.params.param }); }); +app.get('/test-inbound-headers', function (req, res) { + const headers = req.headers; + + res.send({ headers }); +}); + +app.get('/test-outgoing-http', async function (req, res) { + const data = await makeHttpRequest('http://localhost:3030/test-inbound-headers'); + + res.send(data); +}); + app.get('/test-transaction', async function (req, res) { Sentry.startSpan({ name: 'test-span' }, () => { Sentry.startSpan({ name: 'child-span' }, () => {}); }); - res.send({ - transactionIds: global.transactionIds || [], - }); + res.send({}); }); app.get('/test-error', async function (req, res) { @@ -45,16 +56,20 @@ app.get('/test-error', async function (req, res) { app.listen({ port: port }); -Sentry.addGlobalEventProcessor(event => { - global.transactionIds = global.transactionIds || []; - - if (event.type === 'transaction') { - const eventId = event.event_id; - - if (eventId) { - global.transactionIds.push(eventId); - } - } - - return event; -}); +function makeHttpRequest(url) { + return new Promise(resolve => { + const data = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('end', () => { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + }); + }) + .end(); + }); +} diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts new file mode 100644 index 000000000000..6b5ffa56fdba --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts @@ -0,0 +1,100 @@ +import { test, expect } from '@playwright/test'; +import { Span } from '@sentry/types'; +import axios from 'axios'; +import { waitForTransaction } from '../event-proxy-server'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 30_000; + +test('Propagates trace for outgoing http requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-inbound-headers' + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-outgoing-http' + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-http`); + + const inboundTransaction = await inboundTransactionPromise; + const outboundTransaction = await outboundTransactionPromise; + + const traceId = outboundTransaction?.contexts?.trace?.trace_id; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as + | ReturnType + | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-outgoing-http', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: traceId, + }, + }), + }), + ); + + expect(inboundTransaction).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-inbound-headers', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: traceId, + }, + }), + }), + ); +}); diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts index c41660be0fa2..8d06aa411c1c 100644 --- a/packages/node-experimental/src/constants.ts +++ b/packages/node-experimental/src/constants.ts @@ -14,3 +14,8 @@ export const OTEL_ATTR_BREADCRUMB_EVENT_ID = 'sentry.breadcrumb.event_id'; export const OTEL_ATTR_BREADCRUMB_CATEGORY = 'sentry.breadcrumb.category'; export const OTEL_ATTR_BREADCRUMB_DATA = 'sentry.breadcrumb.data'; export const OTEL_ATTR_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; + +export const SENTRY_TRACE_HEADER = 'sentry-trace'; +export const SENTRY_BAGGAGE_HEADER = 'baggage'; + +export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); diff --git a/packages/node-experimental/src/opentelemetry/propagator.ts b/packages/node-experimental/src/opentelemetry/propagator.ts new file mode 100644 index 000000000000..7aa43271b72c --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/propagator.ts @@ -0,0 +1,131 @@ +import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; +import { propagation, trace, TraceFlags } from '@opentelemetry/api'; +import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; +import { getDynamicSamplingContextFromClient } from '@sentry/core'; +import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; +import { generateSentryTraceHeader, SENTRY_BAGGAGE_KEY_PREFIX, tracingContextFromHeaders } from '@sentry/utils'; + +import { getCurrentHub } from '../sdk/hub'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, SENTRY_TRACE_HEADER } from './../constants'; +import { getSpanScope } from './spanData'; + +/** + * Injects and extracts `sentry-trace` and `baggage` headers from carriers. + */ +export class SentryPropagator extends W3CBaggagePropagator { + /** + * @inheritDoc + */ + public inject(context: Context, carrier: unknown, setter: TextMapSetter): void { + if (isTracingSuppressed(context)) { + return; + } + + let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); + + const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as + | PropagationContext + | undefined; + + const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext); + + const dynamicSamplingContext = propagationContext ? getDsc(context, propagationContext, traceId) : undefined; + + if (dynamicSamplingContext) { + baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { + if (dscValue) { + return b.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`, { value: dscValue }); + } + return b; + }, baggage); + } + + setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled)); + + super.inject(propagation.setBaggage(context, baggage), carrier, setter); + } + + /** + * @inheritDoc + */ + public extract(context: Context, carrier: unknown, getter: TextMapGetter): Context { + const maybeSentryTraceHeader: string | string[] | undefined = getter.get(carrier, SENTRY_TRACE_HEADER); + const maybeBaggageHeader = getter.get(carrier, SENTRY_BAGGAGE_HEADER); + + const sentryTraceHeader = maybeSentryTraceHeader + ? Array.isArray(maybeSentryTraceHeader) + ? maybeSentryTraceHeader[0] + : maybeSentryTraceHeader + : undefined; + + const { propagationContext } = tracingContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); + + // Add propagation context to context + const contextWithPropagationContext = context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); + + const spanContext: SpanContext = { + traceId: propagationContext.traceId, + spanId: propagationContext.parentSpanId || '', + isRemote: true, + traceFlags: propagationContext.sampled === true ? TraceFlags.SAMPLED : TraceFlags.NONE, + }; + + // Add remote parent span context + return trace.setSpanContext(contextWithPropagationContext, spanContext); + } + + /** + * @inheritDoc + */ + public fields(): string[] { + return [SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]; + } +} + +function getDsc( + context: Context, + propagationContext: PropagationContext, + traceId: string | undefined, +): DynamicSamplingContext | undefined { + // If we have a DSC on the propagation context, we just use it + if (propagationContext.dsc) { + return propagationContext.dsc; + } + + // Else, we try to generate a new one + const client = getCurrentHub().getClient(); + const activeSpan = trace.getSpan(context); + const scope = activeSpan ? getSpanScope(activeSpan) : undefined; + + if (client) { + return getDynamicSamplingContextFromClient(traceId || propagationContext.traceId, client, scope); + } + + return undefined; +} + +function getSentryTraceData( + context: Context, + propagationContext: PropagationContext | undefined, +): { + spanId: string | undefined; + traceId: string | undefined; + sampled: boolean | undefined; +} { + const span = trace.getSpan(context); + const spanContext = span && span.spanContext(); + + const traceId = spanContext ? spanContext.traceId : propagationContext?.traceId; + + // We have a few scenarios here: + // If we have an active span, and it is _not_ remote, we just use the span's ID + // If we have an active span that is remote, we do not want to use the spanId, as we don't want to attach it to the parent span + // If `isRemote === true`, the span is bascially virtual + // If we don't have a local active span, we use the generated spanId from the propagationContext + const spanId = spanContext && !spanContext.isRemote ? spanContext.spanId : propagationContext?.spanId; + + // eslint-disable-next-line no-bitwise + const sampled = spanContext ? Boolean(spanContext.traceFlags & TraceFlags.SAMPLED) : propagationContext?.sampled; + + return { traceId, spanId, sampled }; +} diff --git a/packages/node-experimental/src/opentelemetry/sampler.ts b/packages/node-experimental/src/opentelemetry/sampler.ts index 327294fbf272..373c3b314b70 100644 --- a/packages/node-experimental/src/opentelemetry/sampler.ts +++ b/packages/node-experimental/src/opentelemetry/sampler.ts @@ -4,11 +4,14 @@ import { isSpanContextValid, trace, TraceFlags } from '@opentelemetry/api'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { hasTracingEnabled } from '@sentry/core'; -import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; -import type { Client, ClientOptions, SamplingContext, TraceparentData } from '@sentry/types'; +import type { Client, ClientOptions, PropagationContext, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; -import { OTEL_ATTR_PARENT_SAMPLED, OTEL_ATTR_SENTRY_SAMPLE_RATE } from '../constants'; +import { + OTEL_ATTR_PARENT_SAMPLED, + OTEL_ATTR_SENTRY_SAMPLE_RATE, + SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, +} from '../constants'; /** * A custom OTEL sampler that uses Sentry sampling rates to make it's decision @@ -177,14 +180,14 @@ function isValidSampleRate(rate: unknown): boolean { return true; } -function getTraceParentData(parentContext: Context): TraceparentData | undefined { - return parentContext.getValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined; +function getPropagationContext(parentContext: Context): PropagationContext | undefined { + return parentContext.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; } function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined { const traceId = spanContext.traceId; - const traceparentData = getTraceParentData(context); + const traceparentData = getPropagationContext(context); // Only inherit sample rate if `traceId` is the same - return traceparentData && traceId === traceparentData.traceId ? traceparentData.parentSampled : undefined; + return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; } diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 134926f19c35..b60dac87aeda 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -3,9 +3,9 @@ import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; -import { SentryPropagator } from '@sentry/opentelemetry-node'; import { logger } from '@sentry/utils'; +import { SentryPropagator } from '../opentelemetry/propagator'; import { SentrySampler } from '../opentelemetry/sampler'; import { SentrySpanProcessor } from '../opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../types'; @@ -15,7 +15,6 @@ import { getCurrentHub } from './hub'; /** * Initialize OpenTelemetry for Node. - * We use the @sentry/opentelemetry-node package to communicate with OpenTelemetry. */ export function initOtel(): void { const client = getCurrentHub().getClient(); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 7979b771c440..02b84ec8cef2 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,12 +1,12 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; -import type { TransactionEvent } from '@sentry/types'; +import type { PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import * as Sentry from '../../src'; import { startSpan } from '../../src'; +import { SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../../src/constants'; import type { Http } from '../../src/integrations'; import { SentrySpanProcessor } from '../../src/opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../../src/sdk/client'; @@ -348,10 +348,11 @@ describe('Integration | Transactions', () => { traceFlags: TraceFlags.SAMPLED, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, parentSpanId, - parentSampled: true, + spanId: '6e0c63257de34c93', + sampled: true, }; mockSdkInit({ enableTracing: true, beforeSendTransaction }); @@ -362,7 +363,7 @@ describe('Integration | Transactions', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { diff --git a/packages/node-experimental/test/opentelemetry/propagator.test.ts b/packages/node-experimental/test/opentelemetry/propagator.test.ts new file mode 100644 index 000000000000..80b027496428 --- /dev/null +++ b/packages/node-experimental/test/opentelemetry/propagator.test.ts @@ -0,0 +1,375 @@ +import type { Context } from '@opentelemetry/api'; +import { + defaultTextMapGetter, + defaultTextMapSetter, + propagation, + ROOT_CONTEXT, + trace, + TraceFlags, +} from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import { addTracingExtensions, Hub, makeMain } from '@sentry/core'; +import type { PropagationContext } from '@sentry/types'; + +import { + SENTRY_BAGGAGE_HEADER, + SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, + SENTRY_TRACE_HEADER, +} from '../../src/constants'; +import { SentryPropagator } from '../../src/opentelemetry/propagator'; + +beforeAll(() => { + addTracingExtensions(); +}); + +describe('SentryPropagator', () => { + const propagator = new SentryPropagator(); + let carrier: { [key: string]: unknown }; + + beforeEach(() => { + carrier = {}; + }); + + it('returns fields set', () => { + expect(propagator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]); + }); + + describe('inject', () => { + const client = { + getOptions: () => ({ + environment: 'production', + release: '1.0.0', + }), + getDsn: () => ({ + publicKey: 'abc', + }), + }; + // @ts-expect-error Use mock client for unit tests + const hub: Hub = new Hub(client); + makeMain(hub); + + describe('with active span', () => { + it.each([ + [ + 'works with a sampled propagation context', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=true', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', + ], + [ + 'works with an unsampled propagation context', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: false, + dsc: { + transaction: 'not-sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'false', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=not-sampled-transaction', + 'sentry-sampled=false', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', + ], + [ + 'creates a new DSC if none exists yet', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: undefined, + }, + [ + 'sentry-environment=production', + 'sentry-public_key=abc', + 'sentry-release=1.0.0', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', + ], + [ + 'works with a remote parent span', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + isRemote: true, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=true', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c94-1', + ], + ])('%s', (_name, spanContext, propagationContext, baggage, sentryTrace) => { + const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); + }); + + it('should include existing baggage', () => { + const propagationContext: PropagationContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }; + + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'foo=bar', + 'sentry-transaction=sampled-transaction', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-sampled=true', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-release=1.0.0', + ].sort(), + ); + }); + + it('should create baggage without propagation context', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe('foo=bar'); + }); + + it('should NOT set baggage and sentry-trace header if instrumentation is supressed', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const propagationContext: PropagationContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }; + const context = suppressTracing( + trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext), + ); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(undefined); + }); + }); + + it('should take span from propagationContext id if no active span is found', () => { + const propagationContext: PropagationContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + parentSpanId: '6e0c63257de34c93', + spanId: '6e0c63257de34c92', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }; + + const context = setPropagationContext(ROOT_CONTEXT, propagationContext); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-transaction=sampled-transaction', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-sampled=true', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-release=1.0.0', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'); + }); + }); + + describe('extract', () => { + it('sets sentry span context on the context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + }); + }); + + it('sets defined sentry trace header on context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + + const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext; + expect(propagationContext).toEqual({ + sampled: true, + parentSpanId: '6e0c63257de34c92', + spanId: expect.any(String), + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + }); + + // Ensure spanId !== parentSpanId - it should be a new random ID + expect(propagationContext.spanId).not.toBe('6e0c63257de34c92'); + }); + + it('sets undefined sentry trace header on context', () => { + const sentryTraceHeader = undefined; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + }); + + it('sets defined dynamic sampling context on context', () => { + const baggage = + 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned) + dsc: { + environment: 'production', + public_key: 'abc', + release: '1.0.0', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + transaction: 'dsc-transaction', + }, + }); + }); + + it('sets undefined dynamic sampling context on context', () => { + const baggage = ''; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + }); + + it('handles when sentry-trace is an empty array', () => { + carrier[SENTRY_TRACE_HEADER] = []; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + }); + }); +}); + +function setPropagationContext(context: Context, propagationContext: PropagationContext): Context { + return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); +} + +function baggageToArray(baggage: unknown): string[] { + return typeof baggage === 'string' ? baggage.split(',').sort() : []; +} diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/node-experimental/test/sdk/trace.test.ts index e5f5a1aa7ac7..e141372552a6 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -1,9 +1,15 @@ import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { Span } from '@opentelemetry/sdk-trace-base'; -import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; +import type { PropagationContext } from '@sentry/types'; import * as Sentry from '../../src'; -import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SENTRY_SAMPLE_RATE, OTEL_ATTR_SOURCE } from '../../src/constants'; +import { + OTEL_ATTR_OP, + OTEL_ATTR_ORIGIN, + OTEL_ATTR_SENTRY_SAMPLE_RATE, + OTEL_ATTR_SOURCE, + SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, +} from '../../src/constants'; import { getSpanMetadata } from '../../src/opentelemetry/spanData'; import { getActiveSpan } from '../../src/utils/getActiveSpan'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -367,16 +373,17 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.SAMPLED, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, + sampled: true, parentSpanId, - parentSampled: true, + spanId: '6e0c63257de34c93', }; // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { @@ -406,16 +413,17 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.NONE, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, + sampled: false, parentSpanId, - parentSampled: false, + spanId: '6e0c63257de34c93', }; // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { @@ -533,16 +541,17 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.SAMPLED, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, + sampled: true, parentSpanId, - parentSampled: true, + spanId: '6e0c63257de34c93', }; // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { diff --git a/packages/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts index 0d3c905eaf2c..7ed84c517b45 100644 --- a/packages/opentelemetry-node/src/index.ts +++ b/packages/opentelemetry-node/src/index.ts @@ -1,5 +1,3 @@ -import { SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants'; - export { SentrySpanProcessor } from './spanprocessor'; export { SentryPropagator } from './propagator'; export { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; @@ -10,13 +8,3 @@ export { mapOtelStatus } from './utils/mapOtelStatus'; export { addOtelSpanData, getOtelSpanData, clearOtelSpanData } from './utils/spanData'; export type { AdditionalOtelSpanData } from './utils/spanData'; /* eslint-enable deprecation/deprecation */ - -/** - * This is only exported for internal use. - * Semver etc. does not apply here, this is subject to change at any time! - * This is explicitly _NOT_ public because we may have to change the underlying way we store/handle spans, - * which may make this API unusable without further notice. - * - * @private - */ -export { SENTRY_TRACE_PARENT_CONTEXT_KEY as _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY };