From ce4bf7197f65582fb497ab9f31ca9f92570947c6 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 20 Feb 2025 13:51:15 +0100 Subject: [PATCH 1/3] feat(core): Add `addLink(s)` to Sentry span --- .../suites/tracing/linking-addLink/init.js | 9 +++ .../suites/tracing/linking-addLink/subject.js | 28 +++++++ .../suites/tracing/linking-addLink/test.ts | 66 +++++++++++++++ .../suites/tracing/linking-addLinks/init.js | 9 +++ .../tracing/linking-addLinks/subject.js | 35 ++++++++ .../suites/tracing/linking-addLinks/test.ts | 81 +++++++++++++++++++ packages/core/src/tracing/sentrySpan.ts | 18 ++++- packages/core/src/utils/spanUtils.ts | 3 +- packages/core/test/lib/tracing/trace.test.ts | 68 ++++++++++++++++ 9 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js new file mode 100644 index 000000000000..3ec6adbbdb75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js new file mode 100644 index 000000000000..510fb07540ad --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js @@ -0,0 +1,28 @@ +// REGULAR --- +const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' }); +rootSpan1.end(); + +Sentry.startSpan({ name: 'rootSpan2' }, rootSpan2 => { + rootSpan2.addLink({ + context: rootSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); +}); + +// NESTED --- +Sentry.startSpan({ name: 'rootSpan3' }, async rootSpan3 => { + Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => { + childSpan1.addLink({ + context: rootSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); + + childSpan1.end(); + }); + + Sentry.startSpan({ name: 'childSpan3.2' }, async childSpan2 => { + childSpan2.addLink({ context: rootSpan3.spanContext() }); + + childSpan2.end(); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts new file mode 100644 index 000000000000..14759294346b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import type { Event, SpanJSON } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../utils/helpers'; + +sentryTest('should link spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const events = await getMultipleSentryEnvelopeRequests(page, 3, { url }); + const [rootSpan1, rootSpan2] = events; + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + expect(rootSpan2.transaction).toBe('rootSpan2'); + expect(rootSpan2.spans).toEqual([]); + + expect(rootSpan2.contexts?.trace?.links?.length).toBe(1); + expect(rootSpan2.contexts?.trace?.links?.[0]).toMatchObject({ + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }); +}); + +sentryTest('should link spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const events = await getMultipleSentryEnvelopeRequests(page, 3, { url }); + const [rootSpan1, /* rootSpan2 */ , rootSpan3] = events; + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + const [childSpan_3_1, childSpan_3_2] = rootSpan3.spans as [SpanJSON, SpanJSON]; + const rootSpan3_traceId = rootSpan3.contexts?.trace?.trace_id as string; + const rootSpan3_spanId = rootSpan3.contexts?.trace?.span_id as string; + + expect(rootSpan3.transaction).toBe('rootSpan3'); + + expect(childSpan_3_1.description).toBe('childSpan3.1'); + expect(childSpan_3_1.links?.length).toBe(1); + expect(childSpan_3_1.links?.[0]).toMatchObject({ + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }); + + expect(childSpan_3_2.description).toBe('childSpan3.2'); + expect(childSpan_3_2.links?.[0]).toMatchObject({ + sampled: true, + span_id: rootSpan3_spanId, + trace_id: rootSpan3_traceId, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js new file mode 100644 index 000000000000..3ec6adbbdb75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js new file mode 100644 index 000000000000..af6c89848fd3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js @@ -0,0 +1,35 @@ +// REGULAR --- +const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' }); +rootSpan1.end(); + +const rootSpan2 = Sentry.startInactiveSpan({ name: 'rootSpan2' }); +rootSpan2.end(); + +Sentry.startSpan({ name: 'rootSpan3' }, rootSpan3 => { + rootSpan3.addLinks([ + { context: rootSpan1.spanContext() }, + { + context: rootSpan2.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]); +}); + +// NESTED --- +Sentry.startSpan({ name: 'rootSpan4' }, async rootSpan4 => { + Sentry.startSpan({ name: 'childSpan4.1' }, async childSpan1 => { + Sentry.startSpan({ name: 'childSpan4.2' }, async childSpan2 => { + childSpan2.addLinks([ + { context: rootSpan4.spanContext() }, + { + context: rootSpan2.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]); + + childSpan2.end(); + }); + + childSpan1.end(); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts new file mode 100644 index 000000000000..6e083cb90cf0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts @@ -0,0 +1,81 @@ +import { expect } from '@playwright/test'; +import type { Event, SpanJSON } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../utils/helpers'; + +sentryTest('should link spans with addLinks() in trace context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const events = await getMultipleSentryEnvelopeRequests(page, 3, { url }); + const [rootSpan1, rootSpan2, rootSpan3] = events; + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; + const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + + expect(rootSpan2.transaction).toBe('rootSpan2'); + expect(rootSpan2.spans).toEqual([]); + + expect(rootSpan3.transaction).toBe('rootSpan3'); + expect(rootSpan3.spans).toEqual([]); + expect(rootSpan3.contexts?.trace?.links?.length).toBe(2); + expect(rootSpan3.contexts?.trace?.links).toEqual([ + { + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }, + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan2_spanId, + trace_id: rootSpan2_traceId, + }, + ]); +}); + +sentryTest('should link spans with addLinks() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const events = await getMultipleSentryEnvelopeRequests(page, 4, { url }); + const [/* rootSpan1 */ , rootSpan2, /* rootSpan3 */ , rootSpan4] = events; + + const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; + const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + + const [childSpan_4_1, childSpan_4_2] = rootSpan4.spans as [SpanJSON, SpanJSON]; + const rootSpan4_traceId = rootSpan4.contexts?.trace?.trace_id as string; + const rootSpan4_spanId = rootSpan4.contexts?.trace?.span_id as string; + + expect(rootSpan4.transaction).toBe('rootSpan4'); + + expect(childSpan_4_1.description).toBe('childSpan4.1'); + expect(childSpan_4_1.links).toBe(undefined); + + expect(childSpan_4_2.description).toBe('childSpan4.2'); + expect(childSpan_4_2.links?.length).toBe(2); + expect(childSpan_4_2.links).toEqual([ + { + sampled: true, + span_id: rootSpan4_spanId, + trace_id: rootSpan4_traceId, + }, + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan2_spanId, + trace_id: rootSpan2_traceId, + }, + ]); +}); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index ddf036f88cdb..3194a45f707f 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -24,6 +24,7 @@ import type { TransactionEvent, TransactionSource, } from '../types-hoist'; +import type { SpanLink } from '../types-hoist/link'; import { logger } from '../utils-hoist/logger'; import { dropUndefinedKeys } from '../utils-hoist/object'; import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; @@ -31,6 +32,7 @@ import { timestampInSeconds } from '../utils-hoist/time'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, + convertSpanLinksForEnvelope, getRootSpan, getSpanDescendants, getStatusMessage, @@ -55,6 +57,7 @@ export class SentrySpan implements Span { protected _sampled: boolean | undefined; protected _name?: string | undefined; protected _attributes: SpanAttributes; + protected _links?: SpanLink[]; /** Epoch timestamp in seconds when the span started. */ protected _startTime: number; /** Epoch timestamp in seconds when the span ended. */ @@ -110,12 +113,22 @@ export class SentrySpan implements Span { } /** @inheritDoc */ - public addLink(_link: unknown): this { + public addLink(link: SpanLink): this { + if (this._links) { + this._links.push(link); + } else { + this._links = [link]; + } return this; } /** @inheritDoc */ - public addLinks(_links: unknown[]): this { + public addLinks(links: SpanLink[]): this { + if (this._links) { + this._links.push(...links); + } else { + this._links = links; + } return this; } @@ -225,6 +238,7 @@ export class SentrySpan implements Span { measurements: timedEventsToMeasurements(this._events), is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined, segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined, + links: convertSpanLinksForEnvelope(this._links), }); } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index d23a08a96808..7cb19fbacf3c 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -40,7 +40,7 @@ let hasShownSpanDropWarning = false; */ export function spanToTransactionTraceContext(span: Span): TraceContext { const { spanId: span_id, traceId: trace_id } = span.spanContext(); - const { data, op, parent_span_id, status, origin } = spanToJSON(span); + const { data, op, parent_span_id, status, origin, links } = spanToJSON(span); return dropUndefinedKeys({ parent_span_id, @@ -50,6 +50,7 @@ export function spanToTransactionTraceContext(span: Span): TraceContext { op, status, origin, + links, }); } diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index c33b50c01a85..de18169df257 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -399,6 +399,40 @@ describe('startSpan', () => { }); }); + it('allows to pass span links', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual(undefined); + + const span1JSON = spanToJSON(rawSpan1); + + startSpan({ name: '/users/:id' }, rawSpan2 => { + rawSpan2.addLink({ + context: rawSpan1.spanContext(), + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }); + + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); @@ -900,6 +934,40 @@ describe('startSpanManual', () => { }); }); + it('allows to pass span links', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual(undefined); + + const span1JSON = spanToJSON(rawSpan1); + + startSpanManual({ name: '/users/:id' }, rawSpan2 => { + rawSpan2.addLink({ + context: rawSpan1.spanContext(), + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }); + + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); From 82c5a349eb2ec9e20e4f9bdf8c32edd0bced9bc5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 20 Feb 2025 14:29:06 +0100 Subject: [PATCH 2/3] feat(core): Add links to span options --- .../tracing/linking-spanOptions/init.js | 9 ++ .../tracing/linking-spanOptions/subject.js | 20 +++ .../tracing/linking-spanOptions/test.ts | 40 ++++++ packages/core/src/tracing/sentrySpan.ts | 1 + packages/core/test/lib/tracing/trace.test.ts | 122 +++++++++++++++++- 5 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js new file mode 100644 index 000000000000..3ec6adbbdb75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js new file mode 100644 index 000000000000..797ce3d98fa7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js @@ -0,0 +1,20 @@ +const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' }); +rootSpan1.end(); + +const rootSpan2 = Sentry.startInactiveSpan({ name: 'rootSpan2' }); +rootSpan2.end(); + +Sentry.startSpan( + { + name: 'rootSpan3', + links: [ + { context: rootSpan1.spanContext() }, + { context: rootSpan2.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }, + ], + }, + async () => { + Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => { + childSpan1.end(); + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts new file mode 100644 index 000000000000..cc9f731f3123 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../utils/helpers'; + +sentryTest('should link spans by adding "links" to span options', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const [rootSpan1, rootSpan2, rootSpan3] = await getMultipleSentryEnvelopeRequests(page, 3, { url }); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; + const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + expect(rootSpan3.transaction).toBe('rootSpan3'); + expect(rootSpan3.spans?.length).toBe(1); + expect(rootSpan3.spans?.[0].description).toBe('childSpan3.1'); + + expect(rootSpan3.contexts?.trace?.links?.length).toBe(2); + expect(rootSpan3.contexts?.trace?.links).toEqual([ + { + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }, + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan2_spanId, + trace_id: rootSpan2_traceId, + }, + ]); +}); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 3194a45f707f..53f103c5ed52 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -81,6 +81,7 @@ export class SentrySpan implements Span { this._traceId = spanContext.traceId || generateTraceId(); this._spanId = spanContext.spanId || generateSpanId(); this._startTime = spanContext.startTimestamp || timestampInSeconds(); + this._links = spanContext.links; this._attributes = {}; this.setAttributes({ diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index de18169df257..08cf069c45ec 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -402,8 +402,8 @@ describe('startSpan', () => { it('allows to pass span links', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual(undefined); + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); const span1JSON = spanToJSON(rawSpan1); @@ -433,6 +433,44 @@ describe('startSpan', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); + + const span1JSON = spanToJSON(rawSpan1); + + startSpan( + { + name: '/users/:id', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }, + rawSpan2 => { + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }, + ); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); @@ -937,8 +975,8 @@ describe('startSpanManual', () => { it('allows to pass span links', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual(undefined); + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); const span1JSON = spanToJSON(rawSpan1); @@ -968,6 +1006,44 @@ describe('startSpanManual', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); + + const span1JSON = spanToJSON(rawSpan1); + + startSpanManual( + { + name: '/users/:id', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }, + rawSpan2 => { + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }, + ); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); @@ -1305,6 +1381,44 @@ describe('startInactiveSpan', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); + + const rawSpan2 = startInactiveSpan({ + name: 'GET users/[id]', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }); + + const span1JSON = spanToJSON(rawSpan1); + const span2JSON = spanToJSON(rawSpan2); + const span2LinkJSON = span2JSON.links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + + // sampling decision is inherited + expect(span2LinkJSON?.sampled).toBe(Boolean(spanToJSON(rawSpan1).data['sentry.sample_rate'])); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); From 2f7a252055fb3a382982b4fc81b9b663dd6acc8e Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 24 Feb 2025 14:37:37 +0100 Subject: [PATCH 3/3] use `waitForTransactionRequest` --- .../suites/tracing/linking-spanOptions/test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts index cc9f731f3123..c2a2ed02f0e6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts @@ -1,15 +1,23 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import type { TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; sentryTest('should link spans by adding "links" to span options', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + const url = await getLocalTestUrl({ testDir: __dirname }); - const [rootSpan1, rootSpan2, rootSpan3] = await getMultipleSentryEnvelopeRequests(page, 3, { url }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); + const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;