diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index f7119bf1f66e..dfb8c6f0d2b2 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -1,6 +1,6 @@ -import { Context } from '@opentelemetry/api'; +import { Context, trace } from '@opentelemetry/api'; import { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { getCurrentHub } from '@sentry/core'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Transaction } from '@sentry/tracing'; import { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -20,6 +20,32 @@ export const SENTRY_SPAN_PROCESSOR_MAP: Map = * the Sentry SDK. */ export class SentrySpanProcessor implements OtelSpanProcessor { + public constructor() { + addGlobalEventProcessor(event => { + const otelSpan = trace.getActiveSpan(); + if (!otelSpan) { + return event; + } + + const otelSpanId = otelSpan.spanContext().spanId; + const sentrySpan = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId); + + if (!sentrySpan) { + return event; + } + + // If event has already set `trace` context, use that one. + // This happens in the case of transaction events. + event.contexts = { trace: sentrySpan.getTraceContext(), ...event.contexts }; + const transactionName = sentrySpan.transaction && sentrySpan.transaction.name; + if (transactionName) { + event.tags = { transaction: transactionName, ...event.tags }; + } + + return event; + }); + } + /** * @inheritDoc */ diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 297cc2386545..746f6e447c13 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -13,6 +13,13 @@ import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanproce const SENTRY_DSN = 'https://0@0.ingest.sentry.io/0'; +const DEFAULT_NODE_CLIENT_OPTIONS = { + dsn: SENTRY_DSN, + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], +}; + // Integration Test of SentrySpanProcessor beforeAll(() => { @@ -21,16 +28,12 @@ beforeAll(() => { describe('SentrySpanProcessor', () => { let hub: Hub; + let client: NodeClient; let provider: NodeTracerProvider; let spanProcessor: SentrySpanProcessor; beforeEach(() => { - const client = new NodeClient({ - dsn: SENTRY_DSN, - integrations: [], - transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), - stackParser: () => [], - }); + client = new NodeClient(DEFAULT_NODE_CLIENT_OPTIONS); hub = new Hub(client); makeMain(hub); @@ -711,6 +714,42 @@ describe('SentrySpanProcessor', () => { }); }); }); + + it('associates an error to a transaction', () => { + let sentryEvent: any; + let otelSpan: any; + + client = new NodeClient({ + ...DEFAULT_NODE_CLIENT_OPTIONS, + beforeSend: event => { + sentryEvent = event; + return null; + }, + }); + hub = new Hub(client); + makeMain(hub); + + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('SELECT * FROM users;', child => { + hub.captureException(new Error('oh nooooo!')); + otelSpan = child as OtelSpan; + child.end(); + }); + + parentOtelSpan.end(); + }); + + expect(sentryEvent).toBeDefined(); + expect(sentryEvent.exception).toBeDefined(); + expect(sentryEvent.contexts.trace).toEqual({ + description: otelSpan.name, + parent_span_id: otelSpan.parentSpanId, + span_id: otelSpan.spanContext().spanId, + trace_id: otelSpan.spanContext().traceId, + }); + }); }); // OTEL expects a custom date format