diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index eddc533f68b5..9363a5237153 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -91,6 +91,7 @@ export { spanIsSampled, spanToTraceContext, getSpanDescendants, + getStatusMessage, } from './utils/spanUtils'; export { getRootSpan } from './utils/getRootSpan'; export { applySdkMetadata } from './utils/sdkMetadata'; @@ -114,4 +115,5 @@ export { metrics } from './metrics/exports'; export type { MetricData } from './metrics/exports'; export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; +export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch'; diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index a6d58ad589aa..ac17ed2063dd 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -15,9 +15,17 @@ describe('Integration | Transactions', () => { }); it('correctly creates transaction & spans', async () => { - const beforeSendTransaction = jest.fn(() => null); + const transactions: TransactionEvent[] = []; + const beforeSendTransaction = jest.fn(event => { + transactions.push(event); + return null; + }); - mockSdkInit({ enableTracing: true, beforeSendTransaction }); + mockSdkInit({ + enableTracing: true, + beforeSendTransaction, + release: '8.0.0', + }); const client = Sentry.getClient()!; @@ -58,86 +66,75 @@ describe('Integration | Transactions', () => { await client.flush(); - expect(beforeSendTransaction).toHaveBeenCalledTimes(1); - expect(beforeSendTransaction).toHaveBeenLastCalledWith( - expect.objectContaining({ - breadcrumbs: [ - { message: 'test breadcrumb 1', timestamp: 123456 }, - { message: 'test breadcrumb 2', timestamp: 123456 }, - { message: 'test breadcrumb 3', timestamp: 123456 }, - ], - contexts: { - otel: { - attributes: { - 'test.outer': 'test value', - 'sentry.op': 'test op', - 'sentry.origin': 'auto.test', - 'sentry.source': 'task', - }, - resource: { - 'service.name': 'node', - 'service.namespace': 'sentry', - 'service.version': expect.any(String), - 'telemetry.sdk.language': 'nodejs', - 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': expect.any(String), - }, - }, - runtime: { name: 'node', version: expect.any(String) }, - trace: { - data: { - 'otel.kind': 'INTERNAL', - 'sentry.op': 'test op', - 'sentry.origin': 'auto.test', - 'sentry.source': 'task', - 'sentry.sample_rate': 1, - 'test.outer': 'test value', - }, - op: 'test op', - span_id: expect.any(String), - status: 'ok', - trace_id: expect.any(String), - origin: 'auto.test', - }, - }, - environment: 'production', - event_id: expect.any(String), - platform: 'node', - sdkProcessingMetadata: expect.objectContaining({ - dynamicSamplingContext: expect.objectContaining({ - environment: 'production', - public_key: expect.any(String), - sample_rate: '1', - sampled: 'true', - trace_id: expect.any(String), - transaction: 'test name', - }), - sampleRate: 1, - spanMetadata: expect.any(Object), - requestPath: 'test-path', - }), - server_name: expect.any(String), - // spans are circular (they have a reference to the transaction), which leads to jest choking on this - // instead we compare them in detail below - spans: [expect.any(Object), expect.any(Object)], - start_timestamp: expect.any(Number), - tags: { - 'outer.tag': 'test value', - 'test.tag': 'test value', - }, - timestamp: expect.any(Number), - transaction: 'test name', - transaction_info: { source: 'task' }, - type: 'transaction', - }), - { - event_id: expect.any(String), + expect(transactions).toHaveLength(1); + const transaction = transactions[0]; + + expect(transaction.breadcrumbs).toEqual([ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ]); + + expect(transaction.contexts?.otel).toEqual({ + attributes: { + 'test.outer': 'test value', + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', }, - ); + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }); - // Checking the spans here, as they are circular to the transaction... - const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; - const spans = runArgs[0].spans || []; + expect(transaction.contexts?.trace).toEqual({ + data: { + 'otel.kind': 'INTERNAL', + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', + 'sentry.sample_rate': 1, + 'test.outer': 'test value', + }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.test', + }); + + expect(transaction.sdkProcessingMetadata?.sampleRate).toEqual(1); + expect(transaction.sdkProcessingMetadata?.requestPath).toEqual('test-path'); + expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({ + environment: 'production', + public_key: expect.any(String), + sample_rate: '1', + sampled: 'true', + release: '8.0.0', + trace_id: expect.any(String), + transaction: 'test name', + }); + + expect(transaction.environment).toEqual('production'); + expect(transaction.event_id).toEqual(expect.any(String)); + expect(transaction.start_timestamp).toEqual(expect.any(Number)); + expect(transaction.timestamp).toEqual(expect.any(Number)); + expect(transaction.transaction).toEqual('test name'); + + expect(transaction.tags).toEqual({ + 'outer.tag': 'test value', + 'test.tag': 'test value', + }); + expect(transaction.transaction_info).toEqual({ source: 'task' }); + expect(transaction.type).toEqual('transaction'); + + expect(transaction.spans).toHaveLength(2); + const spans = transaction.spans || []; // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs diff --git a/packages/opentelemetry/src/custom/hubextensions.ts b/packages/opentelemetry/src/custom/hubextensions.ts deleted file mode 100644 index a70cb1aaca33..000000000000 --- a/packages/opentelemetry/src/custom/hubextensions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { addTracingExtensions as _addTracingExtensions, getMainCarrier } from '@sentry/core'; -import type { CustomSamplingContext, TransactionContext } from '@sentry/types'; -import { consoleSandbox } from '@sentry/utils'; - -/** - * Add tracing extensions, ensuring a patched `startTransaction` to work with OTEL. - */ -export function addTracingExtensions(): void { - _addTracingExtensions(); - - const carrier = getMainCarrier(); - if (!carrier.__SENTRY__) { - return; - } - - carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {}; - if (carrier.__SENTRY__.extensions.startTransaction !== startTransactionNoop) { - carrier.__SENTRY__.extensions.startTransaction = startTransactionNoop; - } -} - -function startTransactionNoop( - _transactionContext: TransactionContext, - _customSamplingContext?: CustomSamplingContext, -): unknown { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('startTransaction is a noop in @sentry/opentelemetry. Use `startSpan` instead.'); - }); - // We return an object here as hub.ts checks for the result of this - // and renders a different warning if this is empty - return {}; -} diff --git a/packages/opentelemetry/src/custom/transaction.ts b/packages/opentelemetry/src/custom/transaction.ts deleted file mode 100644 index 35028f450a4c..000000000000 --- a/packages/opentelemetry/src/custom/transaction.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Hub } from '@sentry/core'; -import { Transaction } from '@sentry/core'; -import type { Hub as HubInterface, TransactionContext } from '@sentry/types'; - -/** - * This is a fork of core's tracing/hubextensions.ts _startTransaction, - * with some OTEL specifics. - */ -export function startTransaction(hub: HubInterface, transactionContext: TransactionContext): Transaction { - // eslint-disable-next-line deprecation/deprecation - const client = hub.getClient(); - - // eslint-disable-next-line deprecation/deprecation - const transaction = new Transaction(transactionContext, hub as Hub); - - if (client) { - client.emit('startTransaction', transaction); - client.emit('spanStart', transaction); - } - return transaction; -} diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index aa5478e3f877..a2aeb9163e82 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -34,7 +34,6 @@ export { startSpan, startSpanManual, startInactiveSpan, withActiveSpan } from '. export { setupGlobalHub } from './custom/hub'; // eslint-disable-next-line deprecation/deprecation export { getCurrentHub } from './custom/getCurrentHub'; -export { addTracingExtensions } from './custom/hubextensions'; export { setupEventContextTrace } from './setupEventContextTrace'; export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrategy'; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index d818bd0d9763..387e1f654c96 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -1,19 +1,19 @@ +import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import type { SentrySpan, Transaction } from '@sentry/core'; +import { captureEvent, getMetricSummaryJsonForSpan } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - getCurrentHub, + getStatusMessage, } from '@sentry/core'; -import type { Scope, Span, SpanOrigin, TransactionSource } from '@sentry/types'; -import { addNonEnumerableProperty, dropUndefinedKeys, logger } from '@sentry/utils'; -import { startTransaction } from './custom/transaction'; +import type { SpanJSON, SpanOrigin, TraceContext, TransactionEvent, TransactionSource } from '@sentry/types'; +import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; import { InternalSentrySemanticAttributes } from './semanticAttributes'; @@ -24,7 +24,7 @@ import type { SpanNode } from './utils/groupSpansWithParents'; import { groupSpansWithParents } from './utils/groupSpansWithParents'; import { mapStatus } from './utils/mapStatus'; import { parseSpanDescription } from './utils/parseSpanDescription'; -import { getSpanHub, getSpanMetadata, getSpanScopes } from './utils/spanData'; +import { getSpanMetadata, getSpanScopes } from './utils/spanData'; type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; @@ -111,13 +111,20 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { rootNodes.forEach(root => { remaining.delete(root); const span = root.span; - const transaction = createTransactionForOtelSpan(span); + const transactionEvent = createTransactionForOtelSpan(span); + + // We'll recursively add all the child spans to this array + const spans = transactionEvent.spans || []; root.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, transaction, remaining); + createAndFinishSpanForOtelSpan(child, spans, remaining); }); - transaction.end(span.endTime); + transactionEvent.spans = spans; + + // TODO Measurements are not yet implemented in OTEL + + captureEvent(transactionEvent); }); return Array.from(remaining) @@ -144,69 +151,72 @@ function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; sour return { origin, op, source }; } -function createTransactionForOtelSpan(span: ReadableSpan): Transaction { - // eslint-disable-next-line deprecation/deprecation - const hub = getSpanHub(span) || getCurrentHub(); - const spanContext = span.spanContext(); - const spanId = spanContext.spanId; - const traceId = spanContext.traceId; - const parentSpanId = span.parentSpanId; - - const parentSampled = span.attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] as boolean | undefined; - - const { op, description, data, origin, source } = getSpanData(span); +function createTransactionForOtelSpan(span: ReadableSpan): TransactionEvent { + const { op, description, data, origin = 'manual', source } = getSpanData(span); const metadata = getSpanMetadata(span); const capturedSpanScopes = getSpanScopes(span); const sampleRate = span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as number | undefined; - const attributes = { + const attributes = dropUndefinedKeys({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, ...data, ...removeSentryAttributes(span.attributes), - }; + }); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); + const { traceId: trace_id, spanId: span_id } = span.spanContext(); + const parent_span_id = span.parentSpanId; - const transaction = startTransaction(hub, { - spanId, - traceId, - parentSpanId, - parentSampled, - name: description, + const status = mapStatus(span); + + const traceContext: TraceContext = dropUndefinedKeys({ + parent_span_id, + span_id, + trace_id, + data: attributes, + origin, op, - startTimestamp: convertOtelTimeToSeconds(span.startTime), - metadata: { + status: getStatusMessage(status), + }); + + const transactionEvent: TransactionEvent = { + contexts: { + trace: traceContext, + otel: { + // TODO: remove the attributes here? + attributes: removeSentryAttributes(span.attributes), + resource: span.resource.attributes, + }, + }, + spans: [], + start_timestamp: convertOtelTimeToSeconds(span.startTime), + timestamp: convertOtelTimeToSeconds(span.endTime), + transaction: description, + type: 'transaction', + sdkProcessingMetadata: { + sampleRate, + ...metadata, + capturedSpanScope: capturedSpanScopes?.scope, + capturedSpanIsolationScope: capturedSpanScopes?.isolationScope, ...dropUndefinedKeys({ - dynamicSamplingContext, - sampleRate, + dynamicSamplingContext: getDynamicSamplingContextFromSpan(span), }), - ...metadata, }, - attributes, - origin, - sampled: true, - }); - - transaction.setStatus(mapStatus(span)); - - // We currently don't want to write this to the scope because it would mutate it. - // In the future we will likely have some sort of transaction payload factory where we can pass this context in directly - // eslint-disable-next-line deprecation/deprecation - transaction.setContext('otel', { - attributes: removeSentryAttributes(span.attributes), - resource: span.resource.attributes, - }); - - if (capturedSpanScopes) { - setCapturedScopesOnTransaction(transaction, capturedSpanScopes.scope, capturedSpanScopes.isolationScope); - } + ...(source && { + transaction_info: { + source, + }, + }), + _metrics_summary: getMetricSummaryJsonForSpan(span as unknown as Span), + }; - return transaction; + return transactionEvent; } -function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: SentrySpan, remaining: Set): void { +function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remaining: Set): void { remaining.delete(node); const span = node.span; @@ -215,33 +225,46 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry // If this span should be dropped, we still want to create spans for the children of this if (shouldDrop) { node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, sentryParentSpan, remaining); + createAndFinishSpanForOtelSpan(child, spans, remaining); }); return; } - const spanId = span.spanContext().spanId; - const { attributes } = span; + const span_id = span.spanContext().spanId; + const trace_id = span.spanContext().traceId; - const { op, description, data, origin } = getSpanData(span); - const allData = { ...removeSentryAttributes(attributes), ...data }; + const { attributes, startTime, endTime, parentSpanId } = span; - // eslint-disable-next-line deprecation/deprecation - const sentrySpan = sentryParentSpan.startChild({ - name: description, - op, + const { op, description, data, origin = 'manual' } = getSpanData(span); + const allData = dropUndefinedKeys({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + ...removeSentryAttributes(attributes), + ...data, + }); + + const status = mapStatus(span); + + const spanJSON: SpanJSON = dropUndefinedKeys({ + span_id, + trace_id, data: allData, - startTimestamp: convertOtelTimeToSeconds(span.startTime), - spanId, + description, + parent_span_id: parentSpanId, + start_timestamp: convertOtelTimeToSeconds(startTime), + // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time + timestamp: convertOtelTimeToSeconds(endTime) || undefined, + status: getStatusMessage(status), + op, origin, - }) as SentrySpan; - sentrySpan.setStatus(mapStatus(span)); + _metrics_summary: getMetricSummaryJsonForSpan(span as unknown as Span), + }); + + spans.push(spanJSON); node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, sentrySpan, remaining); + createAndFinishSpanForOtelSpan(child, spans, remaining); }); - - sentrySpan.end(convertOtelTimeToSeconds(span.endTime)); } function getSpanData(span: ReadableSpan): { @@ -309,12 +332,3 @@ function getData(span: ReadableSpan): Record { return data; } - -const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; -const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; - -/** Sets the scope and isolation scope to be used for when the transaction is finished. */ -function setCapturedScopesOnTransaction(span: Span, scope: Scope, isolationScope: Scope): void { - addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, isolationScope); - addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, scope); -} diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts index a2f95725ff35..df1f6fcc2068 100644 --- a/packages/opentelemetry/src/spanProcessor.ts +++ b/packages/opentelemetry/src/spanProcessor.ts @@ -2,7 +2,7 @@ import type { Context } from '@opentelemetry/api'; import { ROOT_CONTEXT, trace } from '@opentelemetry/api'; import type { Span, SpanProcessor as SpanProcessorInterface } from '@opentelemetry/sdk-trace-base'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { getCurrentHub } from '@sentry/core'; +import { getClient, getCurrentHub } from '@sentry/core'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -39,6 +39,9 @@ function onSpanStart(span: Span, parentContext: Context): void { setSpanHub(span, actualHub); setSpanScopes(span, { scope, isolationScope }); } + + const client = getClient(); + client?.emit('spanStart', span); } function onSpanEnd(span: Span): void { @@ -48,6 +51,9 @@ function onSpanEnd(span: Span): void { span.events.forEach(event => { maybeCaptureExceptionForTimedEvent(hub, event, span); }); + + const client = getClient(); + client?.emit('spanEnd', span); } /** diff --git a/packages/opentelemetry/test/custom/hubextensions.test.ts b/packages/opentelemetry/test/custom/hubextensions.test.ts deleted file mode 100644 index 9d8f72cafb1f..000000000000 --- a/packages/opentelemetry/test/custom/hubextensions.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getCurrentHub, setCurrentClient } from '@sentry/core'; -import { addTracingExtensions } from '../../src/custom/hubextensions'; -import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; - -describe('hubextensions', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('startTransaction is noop', () => { - const client = new TestClient(getDefaultTestClientOptions()); - setCurrentClient(client); - client.init(); - addTracingExtensions(); - - const mockConsole = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - // eslint-disable-next-line deprecation/deprecation - const transaction = getCurrentHub().startTransaction({ name: 'test' }); - expect(transaction).toEqual({}); - - expect(mockConsole).toHaveBeenCalledTimes(1); - expect(mockConsole).toHaveBeenCalledWith( - 'startTransaction is a noop in @sentry/opentelemetry. Use `startSpan` instead.', - ); - }); -}); diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts deleted file mode 100644 index 8d6a7ef6d8cf..000000000000 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - Transaction, - getCurrentHub, - getSpanDescendants, - setCurrentClient, - spanIsSampled, - spanToJSON, -} from '@sentry/core'; -import { startTransaction } from '../../src/custom/transaction'; -import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; - -describe('startTranscation', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('creates an unsampled transaction', () => { - const client = new TestClient(getDefaultTestClientOptions()); - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - setCurrentClient(client); - client.init(); - - const transaction = startTransaction(hub, { name: 'test' }); - - expect(transaction).toBeInstanceOf(Transaction); - expect(transaction['_sampled']).toBe(undefined); - expect(spanIsSampled(transaction)).toBe(false); - // unsampled span is filtered out here - expect(getSpanDescendants(transaction)).toHaveLength(0); - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata).toEqual({ - spanMetadata: {}, - }); - - expect(spanToJSON(transaction)).toEqual( - expect.objectContaining({ - origin: 'manual', - span_id: expect.any(String), - start_timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - ); - }); - - it('creates a sampled transaction', () => { - const client = new TestClient(getDefaultTestClientOptions()); - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - setCurrentClient(client); - client.init(); - - const transaction = startTransaction(hub, { name: 'test', sampled: true }); - - expect(transaction).toBeInstanceOf(Transaction); - expect(transaction['_sampled']).toBe(true); - expect(spanIsSampled(transaction)).toBe(true); - expect(getSpanDescendants(transaction)).toHaveLength(1); - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata).toEqual({ - spanMetadata: {}, - }); - - expect(spanToJSON(transaction)).toEqual( - expect.objectContaining({ - origin: 'manual', - span_id: expect.any(String), - start_timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - ); - }); - - it('allows to pass data to transaction', () => { - const client = new TestClient(getDefaultTestClientOptions()); - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - setCurrentClient(client); - client.init(); - - const transaction = startTransaction(hub, { - name: 'test', - startTimestamp: 1234, - spanId: 'span1', - traceId: 'trace1', - }); - - expect(transaction).toBeInstanceOf(Transaction); - // eslint-disable-next-line deprecation/deprecation - expect(transaction.metadata).toEqual({ - spanMetadata: {}, - }); - - expect(spanToJSON(transaction)).toEqual( - expect.objectContaining({ - origin: 'manual', - span_id: 'span1', - start_timestamp: 1234, - trace_id: 'trace1', - }), - ); - }); -}); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 38316b15f264..ff405c735764 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -21,9 +21,17 @@ describe('Integration | Transactions', () => { }); it('correctly creates transaction & spans', async () => { - const beforeSendTransaction = jest.fn(() => null); + const transactions: TransactionEvent[] = []; + const beforeSendTransaction = jest.fn(event => { + transactions.push(event); + return null; + }); - mockSdkInit({ enableTracing: true, beforeSendTransaction }); + mockSdkInit({ + enableTracing: true, + beforeSendTransaction, + release: '8.0.0', + }); const client = getClient() as TestClientInterface; @@ -64,83 +72,75 @@ describe('Integration | Transactions', () => { await client.flush(); - expect(beforeSendTransaction).toHaveBeenCalledTimes(1); - expect(beforeSendTransaction).toHaveBeenLastCalledWith( - expect.objectContaining({ - breadcrumbs: [ - { message: 'test breadcrumb 1', timestamp: 123456 }, - { message: 'test breadcrumb 2', timestamp: 123456 }, - { message: 'test breadcrumb 3', timestamp: 123456 }, - ], - contexts: { - otel: { - attributes: { - 'test.outer': 'test value', - 'sentry.op': 'test op', - 'sentry.origin': 'auto.test', - 'sentry.source': 'task', - }, - resource: { - 'service.name': 'opentelemetry-test', - 'service.namespace': 'sentry', - 'service.version': expect.any(String), - 'telemetry.sdk.language': 'nodejs', - 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': expect.any(String), - }, - }, - trace: { - data: { - 'otel.kind': 'INTERNAL', - 'sentry.op': 'test op', - 'sentry.origin': 'auto.test', - 'sentry.source': 'task', - 'sentry.sample_rate': 1, - 'test.outer': 'test value', - }, - op: 'test op', - span_id: expect.any(String), - status: 'ok', - trace_id: expect.any(String), - origin: 'auto.test', - }, - }, - environment: 'production', - event_id: expect.any(String), - sdkProcessingMetadata: expect.objectContaining({ - dynamicSamplingContext: expect.objectContaining({ - environment: 'production', - public_key: expect.any(String), - sample_rate: '1', - sampled: 'true', - trace_id: expect.any(String), - transaction: 'test name', - }), - sampleRate: 1, - spanMetadata: expect.any(Object), - requestPath: 'test-path', - }), - // spans are circular (they have a reference to the transaction), which leads to jest choking on this - // instead we compare them in detail below - spans: [expect.any(Object), expect.any(Object)], - start_timestamp: expect.any(Number), - tags: { - 'outer.tag': 'test value', - 'test.tag': 'test value', - }, - timestamp: expect.any(Number), - transaction: 'test name', - transaction_info: { source: 'task' }, - type: 'transaction', - }), - { - event_id: expect.any(String), + expect(transactions).toHaveLength(1); + const transaction = transactions[0]; + + expect(transaction.breadcrumbs).toEqual([ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ]); + + expect(transaction.contexts?.otel).toEqual({ + attributes: { + 'test.outer': 'test value', + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', }, - ); + resource: { + 'service.name': 'opentelemetry-test', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }); - // Checking the spans here, as they are circular to the transaction... - const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; - const spans = runArgs[0].spans || []; + expect(transaction.contexts?.trace).toEqual({ + data: { + 'otel.kind': 'INTERNAL', + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', + 'sentry.sample_rate': 1, + 'test.outer': 'test value', + }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.test', + }); + + expect(transaction.sdkProcessingMetadata?.sampleRate).toEqual(1); + expect(transaction.sdkProcessingMetadata?.requestPath).toEqual('test-path'); + expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({ + environment: 'production', + public_key: expect.any(String), + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'test name', + release: '8.0.0', + }); + + expect(transaction.environment).toEqual('production'); + expect(transaction.event_id).toEqual(expect.any(String)); + expect(transaction.start_timestamp).toEqual(expect.any(Number)); + expect(transaction.timestamp).toEqual(expect.any(Number)); + expect(transaction.transaction).toEqual('test name'); + + expect(transaction.tags).toEqual({ + 'outer.tag': 'test value', + 'test.tag': 'test value', + }); + expect(transaction.transaction_info).toEqual({ source: 'task' }); + expect(transaction.type).toEqual('transaction'); + + expect(transaction.spans).toHaveLength(2); + const spans = transaction.spans || []; // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs @@ -543,7 +543,11 @@ describe('Integration | Transactions', () => { traceState: new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString), }; - mockSdkInit({ enableTracing: true, beforeSendTransaction }); + mockSdkInit({ + enableTracing: true, + beforeSendTransaction, + release: '7.0.0', + }); const client = getClient() as TestClientInterface;