diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts index fbaf239ae6d1..945327064df2 100644 --- a/packages/node/src/integrations/tracing/graphql.ts +++ b/packages/node/src/integrations/tracing/graphql.ts @@ -55,9 +55,6 @@ export const instrumentGraphql = generateInstrumentOnce( if (options.useOperationNameForRootSpan && operationType) { const rootSpan = getRootSpan(span); - - // We guard to only do this on http.server spans - const rootSpanAttributes = spanToJSON(rootSpan).data; const existingOperations = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION] || []; diff --git a/packages/opentelemetry/test/helpers/createSpan.ts b/packages/opentelemetry/test/helpers/createSpan.ts deleted file mode 100644 index 0e3672f5abe3..000000000000 --- a/packages/opentelemetry/test/helpers/createSpan.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Context, SpanContext, TimeInput } from '@opentelemetry/api'; -import { SpanKind } from '@opentelemetry/api'; -import type { Tracer } from '@opentelemetry/sdk-trace-base'; -import { Span } from '@opentelemetry/sdk-trace-base'; -import { uuid4 } from '@sentry/core'; - -export function createSpan( - name?: string, - { - spanId, - parentSpanId, - traceId, - startTime, - }: { - spanId?: string; - parentSpanId?: string; - traceId?: string; - startTime?: TimeInput; - } = {}, -): Span { - const spanProcessor = { - onStart: () => {}, - onEnd: () => {}, - }; - const tracer = { - resource: 'test-resource', - instrumentationLibrary: 'test-instrumentation-library', - getSpanLimits: () => ({}), - getActiveSpanProcessor: () => spanProcessor, - } as unknown as Tracer; - - const spanContext: SpanContext = { - spanId: spanId || uuid4(), - traceId: traceId || uuid4(), - traceFlags: 0, - }; - - // eslint-disable-next-line deprecation/deprecation - return new Span(tracer, {} as Context, name || 'test', spanContext, SpanKind.INTERNAL, parentSpanId, [], startTime); -} diff --git a/packages/opentelemetry/test/utils/getRequestSpanData.test.ts b/packages/opentelemetry/test/utils/getRequestSpanData.test.ts index 72b64a307c99..b2fba5b2f2f7 100644 --- a/packages/opentelemetry/test/utils/getRequestSpanData.test.ts +++ b/packages/opentelemetry/test/utils/getRequestSpanData.test.ts @@ -1,20 +1,39 @@ /* eslint-disable deprecation/deprecation */ +import type { Span } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; -import { describe, expect, it } from 'vitest'; - +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getRequestSpanData } from '../../src/utils/getRequestSpanData'; -import { createSpan } from '../helpers/createSpan'; +import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; +import { setupOtel } from '../helpers/initOtel'; +import { cleanupOtel } from '../helpers/mockSdkInit'; describe('getRequestSpanData', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + provider = setupOtel(client); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + + function createSpan(name: string): Span { + return trace.getTracer('test').startSpan(name); + } + it('works with basic span', () => { - const span = createSpan(); + const span = createSpan('test-span'); const data = getRequestSpanData(span); expect(data).toEqual({}); }); it('works with http span', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setAttributes({ [SEMATTRS_HTTP_URL]: 'http://example.com?foo=bar#baz', [SEMATTRS_HTTP_METHOD]: 'GET', @@ -31,7 +50,7 @@ describe('getRequestSpanData', () => { }); it('works without method', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setAttributes({ [SEMATTRS_HTTP_URL]: 'http://example.com', }); @@ -45,7 +64,7 @@ describe('getRequestSpanData', () => { }); it('works with incorrect URL', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setAttributes({ [SEMATTRS_HTTP_URL]: 'malformed-url-here', [SEMATTRS_HTTP_METHOD]: 'GET', diff --git a/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts b/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts index f1bea09bd2f5..c137748353bf 100644 --- a/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts +++ b/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts @@ -1,19 +1,45 @@ -import { describe, expect, it } from 'vitest'; - +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider, ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import type { Span } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { withActiveSpan } from '../../src/trace'; import { groupSpansWithParents } from '../../src/utils/groupSpansWithParents'; -import { createSpan } from '../helpers/createSpan'; +import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; +import { setupOtel } from '../helpers/initOtel'; +import { cleanupOtel } from '../helpers/mockSdkInit'; describe('groupSpansWithParents', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + provider = setupOtel(client); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + it('works with no spans', () => { const actual = groupSpansWithParents([]); expect(actual).toEqual([]); }); it('works with a single root span & in-order spans', () => { - const rootSpan = createSpan('root', { spanId: 'rootId' }); - const parentSpan1 = createSpan('parent1', { spanId: 'parent1Id', parentSpanId: 'rootId' }); - const parentSpan2 = createSpan('parent2', { spanId: 'parent2Id', parentSpanId: 'rootId' }); - const child1 = createSpan('child1', { spanId: 'child1', parentSpanId: 'parent1Id' }); + const tracer = trace.getTracer('test'); + const rootSpan = tracer.startSpan('root') as unknown as ReadableSpan; + const parentSpan1 = withActiveSpan( + rootSpan as unknown as Span, + () => tracer.startSpan('parent1') as unknown as ReadableSpan, + ); + const parentSpan2 = withActiveSpan( + rootSpan as unknown as Span, + () => tracer.startSpan('parent2') as unknown as ReadableSpan, + ); + const child1 = withActiveSpan( + parentSpan1 as unknown as Span, + () => tracer.startSpan('child1') as unknown as ReadableSpan, + ); const actual = groupSpansWithParents([rootSpan, parentSpan1, parentSpan2, child1]); expect(actual).toHaveLength(4); @@ -46,15 +72,28 @@ describe('groupSpansWithParents', () => { }); it('works with a spans with missing root span', () => { - const parentSpan1 = createSpan('parent1', { spanId: 'parent1Id', parentSpanId: 'rootId' }); - const parentSpan2 = createSpan('parent2', { spanId: 'parent2Id', parentSpanId: 'rootId' }); - const child1 = createSpan('child1', { spanId: 'child1', parentSpanId: 'parent1Id' }); + const tracer = trace.getTracer('test'); + + // We create this root span here, but we do not pass it to `groupSpansWithParents` below + const rootSpan = tracer.startSpan('root') as unknown as ReadableSpan; + const parentSpan1 = withActiveSpan( + rootSpan as unknown as Span, + () => tracer.startSpan('parent1') as unknown as ReadableSpan, + ); + const parentSpan2 = withActiveSpan( + rootSpan as unknown as Span, + () => tracer.startSpan('parent2') as unknown as ReadableSpan, + ); + const child1 = withActiveSpan( + parentSpan1 as unknown as Span, + () => tracer.startSpan('child1') as unknown as ReadableSpan, + ); const actual = groupSpansWithParents([parentSpan1, parentSpan2, child1]); expect(actual).toHaveLength(4); // Ensure parent & span is correctly set - const rootRef = actual.find(ref => ref.id === 'rootId'); + const rootRef = actual.find(ref => ref.id === rootSpan.spanContext().spanId); const parent1Ref = actual.find(ref => ref.span === parentSpan1); const parent2Ref = actual.find(ref => ref.span === parentSpan2); const child1Ref = actual.find(ref => ref.span === child1); @@ -82,11 +121,21 @@ describe('groupSpansWithParents', () => { }); it('works with multiple root spans & out-of-order spans', () => { - const rootSpan1 = createSpan('root1', { spanId: 'root1Id' }); - const rootSpan2 = createSpan('root2', { spanId: 'root2Id' }); - const parentSpan1 = createSpan('parent1', { spanId: 'parent1Id', parentSpanId: 'root1Id' }); - const parentSpan2 = createSpan('parent2', { spanId: 'parent2Id', parentSpanId: 'root2Id' }); - const childSpan1 = createSpan('child1', { spanId: 'child1Id', parentSpanId: 'parent1Id' }); + const tracer = trace.getTracer('test'); + const rootSpan1 = tracer.startSpan('root1') as unknown as ReadableSpan; + const rootSpan2 = tracer.startSpan('root2') as unknown as ReadableSpan; + const parentSpan1 = withActiveSpan( + rootSpan1 as unknown as Span, + () => tracer.startSpan('parent1') as unknown as ReadableSpan, + ); + const parentSpan2 = withActiveSpan( + rootSpan2 as unknown as Span, + () => tracer.startSpan('parent2') as unknown as ReadableSpan, + ); + const childSpan1 = withActiveSpan( + parentSpan1 as unknown as Span, + () => tracer.startSpan('child1') as unknown as ReadableSpan, + ); const actual = groupSpansWithParents([childSpan1, parentSpan1, parentSpan2, rootSpan2, rootSpan1]); expect(actual).toHaveLength(5); diff --git a/packages/opentelemetry/test/utils/mapStatus.test.ts b/packages/opentelemetry/test/utils/mapStatus.test.ts index 83d7548aa3ad..aa72cfb95b53 100644 --- a/packages/opentelemetry/test/utils/mapStatus.test.ts +++ b/packages/opentelemetry/test/utils/mapStatus.test.ts @@ -1,13 +1,32 @@ /* eslint-disable deprecation/deprecation */ +import type { Span } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SEMATTRS_HTTP_STATUS_CODE, SEMATTRS_RPC_GRPC_STATUS_CODE } from '@opentelemetry/semantic-conventions'; -import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '@sentry/core'; import type { SpanStatus } from '@sentry/core'; -import { describe, expect, it } from 'vitest'; - +import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { mapStatus } from '../../src/utils/mapStatus'; -import { createSpan } from '../helpers/createSpan'; +import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; +import { setupOtel } from '../helpers/initOtel'; +import { cleanupOtel } from '../helpers/mockSdkInit'; describe('mapStatus', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + provider = setupOtel(client); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + + function createSpan(name: string): Span { + return trace.getTracer('test').startSpan(name); + } + const statusTestTable: [undefined | number | string, undefined | string, SpanStatus][] = [ // http codes [400, undefined, { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], @@ -23,19 +42,6 @@ describe('mapStatus', () => { [504, undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], [999, undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - ['400', undefined, { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], - ['401', undefined, { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], - ['403', undefined, { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], - ['404', undefined, { code: SPAN_STATUS_ERROR, message: 'not_found' }], - ['409', undefined, { code: SPAN_STATUS_ERROR, message: 'already_exists' }], - ['429', undefined, { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], - ['499', undefined, { code: SPAN_STATUS_ERROR, message: 'cancelled' }], - ['500', undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - ['501', undefined, { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], - ['503', undefined, { code: SPAN_STATUS_ERROR, message: 'unavailable' }], - ['504', undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], - ['999', undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - // grpc codes [undefined, '1', { code: SPAN_STATUS_ERROR, message: 'cancelled' }], [undefined, '2', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], @@ -56,11 +62,11 @@ describe('mapStatus', () => { [undefined, '999', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], // http takes precedence over grpc - ['400', '2', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], + [400, '2', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], ]; it.each(statusTestTable)('works with httpCode=%s, grpcCode=%s', (httpCode, grpcCode, expected) => { - const span = createSpan(); + const span = createSpan('test-span'); span.setStatus({ code: 0 }); // UNSET if (httpCode) { @@ -75,39 +81,49 @@ describe('mapStatus', () => { expect(actual).toEqual(expected); }); + it('works with string SEMATTRS_HTTP_STATUS_CODE xxx', () => { + const span = createSpan('test-span'); + + span.setStatus({ code: 0 }); // UNSET + span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, '400'); + + const actual = mapStatus(span); + expect(actual).toEqual({ code: SPAN_STATUS_ERROR, message: 'invalid_argument' }); + }); + it('returns ok span status when is UNSET present on span', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setStatus({ code: 0 }); // UNSET expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_OK }); }); it('returns ok span status when already present on span', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setStatus({ code: 1 }); // OK expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_OK }); }); it('returns error status when span already has error status', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setStatus({ code: 2, message: 'invalid_argument' }); // ERROR expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'invalid_argument' }); }); it('returns error status when span already has error status without message', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setStatus({ code: 2 }); // ERROR expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'unknown_error' }); }); it('infers error status form attributes when span already has error status without message', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, 500); span.setStatus({ code: 2 }); // ERROR expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); }); it('returns unknown error status when code is unknown', () => { - const span = createSpan(); + const span = createSpan('test-span'); span.setStatus({ code: -1 as 0 }); expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'unknown_error' }); }); diff --git a/packages/opentelemetry/test/utils/spanToJSON.test.ts b/packages/opentelemetry/test/utils/spanToJSON.test.ts index a56d1ec6d240..88da6550d1e0 100644 --- a/packages/opentelemetry/test/utils/spanToJSON.test.ts +++ b/packages/opentelemetry/test/utils/spanToJSON.test.ts @@ -1,32 +1,50 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; -import { describe, expect, it } from 'vitest'; - -import { createSpan } from '../helpers/createSpan'; +import type { Span, SpanOptions } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + spanToJSON, +} from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; +import { setupOtel } from '../helpers/initOtel'; +import { cleanupOtel } from '../helpers/mockSdkInit'; describe('spanToJSON', () => { describe('OpenTelemetry Span', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + provider = setupOtel(client); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + + function createSpan(name: string, params?: SpanOptions): Span { + return trace.getTracer('test').startSpan(name, params); + } + it('works with a simple span', () => { - const span = createSpan('test span', { - spanId: 'SPAN-1', - traceId: 'TRACE-1', - startTime: [123, 0], - }); + const span = createSpan('test span', { startTime: [123, 0] }); expect(spanToJSON(span)).toEqual({ - span_id: 'SPAN-1', - trace_id: 'TRACE-1', + span_id: span.spanContext().spanId, + trace_id: span.spanContext().traceId, start_timestamp: 123, description: 'test span', - data: {}, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, }); }); it('works with a full span', () => { - const span = createSpan('test span', { - spanId: 'SPAN-1', - traceId: 'TRACE-1', - startTime: [123, 0], - }); + const span = createSpan('test span', { startTime: [123, 0] }); span.setAttributes({ attr1: 'value1', @@ -39,8 +57,8 @@ describe('spanToJSON', () => { span.end([456, 0]); expect(spanToJSON(span)).toEqual({ - span_id: 'SPAN-1', - trace_id: 'TRACE-1', + span_id: span.spanContext().spanId, + trace_id: span.spanContext().traceId, start_timestamp: 123, timestamp: 456, description: 'test span', @@ -51,6 +69,7 @@ describe('spanToJSON', () => { attr2: 2, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }, status: 'unknown_error', });