From 10337a5e2ac05716c90ef3de582efe34a210794d Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 11 Feb 2025 17:05:15 +0100 Subject: [PATCH 1/3] feat(core): Create types and utilities for span links --- packages/core/src/index.ts | 1 + .../src/tracing/sentryNonRecordingSpan.ts | 16 ++-------- packages/core/src/tracing/sentrySpan.ts | 16 ++-------- packages/core/src/types-hoist/index.ts | 1 + packages/core/src/types-hoist/link.ts | 30 +++++++++++++++++++ packages/core/src/types-hoist/span.ts | 15 +++++++--- packages/core/src/utils/spanUtils.ts | 17 ++++++++++- 7 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 packages/core/src/types-hoist/link.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f03f6b9779e9..e0e9097bbc53 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -76,6 +76,7 @@ export { handleCallbackErrors } from './utils/handleCallbackErrors'; export { parameterize } from './utils/parameterize'; export { addAutoIpAddressToSession, addAutoIpAddressToUser } from './utils/ipAddress'; export { + convertSpanLinksForEnvelope, spanToTraceHeader, spanToJSON, spanIsSampled, diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index e7c9a8e9ac41..b4a5a7c8cd61 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -69,24 +69,12 @@ export class SentryNonRecordingSpan implements Span { return this; } - /** - * This should generally not be used, - * but we need it for being compliant with the OTEL Span interface. - * - * @hidden - * @internal - */ + /** @inheritDoc */ public addLink(_link: unknown): this { return this; } - /** - * This should generally not be used, - * but we need it for being compliant with the OTEL Span interface. - * - * @hidden - * @internal - */ + /** @inheritDoc */ public addLinks(_links: unknown[]): this { return this; } diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 74478f79903f..ddf036f88cdb 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -109,24 +109,12 @@ export class SentrySpan implements Span { } } - /** - * This should generally not be used, - * but it is needed for being compliant with the OTEL Span interface. - * - * @hidden - * @internal - */ + /** @inheritDoc */ public addLink(_link: unknown): this { return this; } - /** - * This should generally not be used, - * but it is needed for being compliant with the OTEL Span interface. - * - * @hidden - * @internal - */ + /** @inheritDoc */ public addLinks(_links: unknown[]): this { return this; } diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index c1cbe5284808..48b38926e85f 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -113,6 +113,7 @@ export type { SpanContextData, TraceFlag, } from './span'; +export type { SpanLink, SpanLinkJSON } from './link'; export type { SpanStatus } from './spanStatus'; export type { TimedEvent } from './timedEvent'; export type { StackFrame } from './stackframe'; diff --git a/packages/core/src/types-hoist/link.ts b/packages/core/src/types-hoist/link.ts new file mode 100644 index 000000000000..a330dc108b00 --- /dev/null +++ b/packages/core/src/types-hoist/link.ts @@ -0,0 +1,30 @@ +import type { SpanAttributeValue, SpanContextData } from './span'; + +type SpanLinkAttributes = { + /** + * Setting the link type to 'previous_trace' helps the Sentry product linking to the previous trace + */ + 'sentry.link.type'?: string | 'previous_trace'; +} & Record; + +export interface SpanLink { + /** + * Contains the SpanContext of the span to link to + */ + context: SpanContextData; + /** + * A key-value pair with primitive values or an array of primitive values + */ + attributes?: SpanLinkAttributes; +} + +/** + * Link interface for the event envelope item. It's a flattened representation of `SpanLink`. + * Can include additional fields defined by OTel. + */ +export interface SpanLinkJSON extends Record { + span_id: string; + trace_id: string; + sampled?: boolean; + attributes?: SpanLinkAttributes; +} diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index c74d00e54f97..00ee1ef7faa0 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -1,3 +1,4 @@ +import type { SpanLink, SpanLinkJSON } from './link'; import type { Measurements } from './measurement'; import type { HrTime } from './opentelemetry'; import type { SpanStatus } from './spanStatus'; @@ -50,6 +51,7 @@ export interface SpanJSON { measurements?: Measurements; is_segment?: boolean; segment_id?: string; + links?: SpanLinkJSON[]; } // These are aligned with OpenTelemetry trace flags @@ -249,14 +251,19 @@ export interface Span { addEvent(name: string, attributesOrStartTime?: SpanAttributes | SpanTimeInput, startTime?: SpanTimeInput): this; /** - * NOT USED IN SENTRY, only added for compliance with OTEL Span interface + * Associates this span with a related span. Links can reference spans from the same or different trace + * and are typically used for batch operations, cross-trace scenarios, or scatter/gather patterns. + * + * Prefer setting links during span creation when possible to support sampling decisions. + * @param link - The link containing the context of the span to link to and optional attributes */ - addLink(link: unknown): this; + addLink(link: SpanLink): this; /** - * NOT USED IN SENTRY, only added for compliance with OTEL Span interface + * Associates this span with multiple related spans. See {@link addLink} for more details. + * @param links - Array of links to associate with this span */ - addLinks(links: unknown): this; + addLinks(links: SpanLink[]): this; /** * NOT USED IN SENTRY, only added for compliance with OTEL Span interface diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 41435e8be373..97c694c76ca4 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -14,6 +14,8 @@ import type { Span, SpanAttributes, SpanJSON, + SpanLink, + SpanLinkJSON, SpanOrigin, SpanStatus, SpanTimeInput, @@ -81,6 +83,19 @@ export function spanToTraceHeader(span: Span): string { return generateSentryTraceHeader(traceId, spanId, sampled); } +/** + * Converts the span links array to a flattened version to be sent within an envelope + */ +export function convertSpanLinksForEnvelope(links: SpanLink[]): SpanLinkJSON[] { + return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ + span_id: spanId, + trace_id: traceId, + sampled: traceFlags === TRACE_FLAG_SAMPLED, + attributes, + ...restContext, + })); +} + /** * Convert a span time input into a timestamp in seconds. */ @@ -180,7 +195,7 @@ function spanIsSentrySpan(span: Span): span is SentrySpan { * However, this has a slightly different semantic, as it also returns false if the span is finished. * So in the case where this distinction is important, use this method. */ -export function spanIsSampled(span: Span): boolean { +export function spanIsSampled(span: { spanContext: Span['spanContext'] }): boolean { // We align our trace flags with the ones OpenTelemetry use // So we also check for sampled the same way they do. const { traceFlags } = span.spanContext(); From 3f28bf939152b214ab142662834e02e11dd251d5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 13 Feb 2025 10:46:27 +0100 Subject: [PATCH 2/3] review suggestions --- packages/core/src/types-hoist/index.ts | 1 - packages/core/src/types-hoist/span.ts | 8 +- packages/core/src/utils/spanUtils.ts | 29 +++-- .../core/test/lib/utils/spanUtils.test.ts | 112 ++++++++++++++++++ 4 files changed, 134 insertions(+), 16 deletions(-) diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 48b38926e85f..c1cbe5284808 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -113,7 +113,6 @@ export type { SpanContextData, TraceFlag, } from './span'; -export type { SpanLink, SpanLinkJSON } from './link'; export type { SpanStatus } from './spanStatus'; export type { TimedEvent } from './timedEvent'; export type { StackFrame } from './stackframe'; diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index 00ee1ef7faa0..0945da7b04fe 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -254,16 +254,18 @@ export interface Span { * Associates this span with a related span. Links can reference spans from the same or different trace * and are typically used for batch operations, cross-trace scenarios, or scatter/gather patterns. * - * Prefer setting links during span creation when possible to support sampling decisions. + * Prefer setting links directly when starting a span (e.g. `Sentry.startSpan()`) as some context information is only available during span creation. * @param link - The link containing the context of the span to link to and optional attributes */ - addLink(link: SpanLink): this; + addLink(link: SpanLink | unknown): this; /** * Associates this span with multiple related spans. See {@link addLink} for more details. + * + * Prefer setting links directly when starting a span (e.g. `Sentry.startSpan()`) as some context information is only available during span creation. * @param links - Array of links to associate with this span */ - addLinks(links: SpanLink[]): this; + addLinks(links: SpanLink[] | unknown): this; /** * NOT USED IN SENTRY, only added for compliance with OTEL Span interface diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 97c694c76ca4..fcf4aa1857e3 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -14,13 +14,12 @@ import type { Span, SpanAttributes, SpanJSON, - SpanLink, - SpanLinkJSON, SpanOrigin, SpanStatus, SpanTimeInput, TraceContext, } from '../types-hoist'; +import type { SpanLink, SpanLinkJSON } from '../types-hoist/link'; import { consoleSandbox } from '../utils-hoist/logger'; import { addNonEnumerableProperty, dropUndefinedKeys } from '../utils-hoist/object'; import { generateSpanId } from '../utils-hoist/propagationContext'; @@ -84,16 +83,22 @@ export function spanToTraceHeader(span: Span): string { } /** - * Converts the span links array to a flattened version to be sent within an envelope + * Converts the span links array to a flattened version to be sent within an envelope. + * + * If the links array is empty, it returns `undefined` so the empty value can be dropped before it's sent. */ -export function convertSpanLinksForEnvelope(links: SpanLink[]): SpanLinkJSON[] { - return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ - span_id: spanId, - trace_id: traceId, - sampled: traceFlags === TRACE_FLAG_SAMPLED, - attributes, - ...restContext, - })); +export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[] | undefined { + if (links && links.length > 0) { + return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ + span_id: spanId, + trace_id: traceId, + sampled: traceFlags === TRACE_FLAG_SAMPLED, + attributes, + ...restContext, + })); + } else { + return undefined; + } } /** @@ -195,7 +200,7 @@ function spanIsSentrySpan(span: Span): span is SentrySpan { * However, this has a slightly different semantic, as it also returns false if the span is finished. * So in the case where this distinction is important, use this method. */ -export function spanIsSampled(span: { spanContext: Span['spanContext'] }): boolean { +export function spanIsSampled(span: Span): boolean { // We align our trace flags with the ones OpenTelemetry use // So we also check for sampled the same way they do. const { traceFlags } = span.spanContext(); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index 8139460e8304..acbac2b35dbd 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -7,6 +7,7 @@ import { SPAN_STATUS_UNSET, SentrySpan, TRACEPARENT_REGEXP, + convertSpanLinksForEnvelope, setCurrentClient, spanToTraceHeader, startInactiveSpan, @@ -14,7 +15,9 @@ import { timestampInSeconds, } from '../../../src'; import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist'; +import type { SpanLink } from '../../../src/types-hoist/link'; import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; import { getRootSpan, spanIsSampled, @@ -156,6 +159,115 @@ describe('spanToTraceContext', () => { }); }); +describe('convertSpanLinksForEnvelope', () => { + it('returns undefined for undefined input', () => { + expect(convertSpanLinksForEnvelope(undefined)).toBeUndefined(); + }); + + it('returns undefined for empty array input', () => { + expect(convertSpanLinksForEnvelope([])).toBeUndefined(); + }); + + it('converts a single span link to a flattened envelope item', () => { + const links: SpanLink[] = [ + { + context: { + spanId: 'span1', + traceId: 'trace1', + traceFlags: TRACE_FLAG_SAMPLED, + }, + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }, + ]; + + const result = convertSpanLinksForEnvelope(links); + + result?.forEach(item => expect(item).not.toHaveProperty('context')); + expect(result).toEqual([ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }, + ]); + }); + + it('converts multiple span links to a flattened envelope item', () => { + const links: SpanLink[] = [ + { + context: { + spanId: 'span1', + traceId: 'trace1', + traceFlags: TRACE_FLAG_SAMPLED, + }, + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }, + { + context: { + spanId: 'span2', + traceId: 'trace2', + traceFlags: TRACE_FLAG_NONE, + }, + attributes: { + 'sentry.link.type': 'another_trace', + }, + }, + ]; + + const result = convertSpanLinksForEnvelope(links); + + result?.forEach(item => expect(item).not.toHaveProperty('context')); + expect(result).toEqual([ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }, + { + span_id: 'span2', + trace_id: 'trace2', + sampled: false, + attributes: { + 'sentry.link.type': 'another_trace', + }, + }, + ]); + }); + + it('handles span links without attributes', () => { + const links: SpanLink[] = [ + { + context: { + spanId: 'span1', + traceId: 'trace1', + traceFlags: TRACE_FLAG_SAMPLED, + }, + }, + ]; + + const result = convertSpanLinksForEnvelope(links); + + result?.forEach(item => expect(item).not.toHaveProperty('context')); + expect(result).toEqual([ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + }, + ]); + }); +}); + describe('spanTimeInputToSeconds', () => { it('works with undefined', () => { const now = timestampInSeconds(); From 1e5f6d1788e07f2a530c774cccc55eae737ad401 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 13 Feb 2025 11:00:35 +0100 Subject: [PATCH 3/3] remove unknown typing --- packages/core/src/types-hoist/span.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index 0945da7b04fe..2b82aab74934 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -257,7 +257,7 @@ export interface Span { * Prefer setting links directly when starting a span (e.g. `Sentry.startSpan()`) as some context information is only available during span creation. * @param link - The link containing the context of the span to link to and optional attributes */ - addLink(link: SpanLink | unknown): this; + addLink(link: SpanLink): this; /** * Associates this span with multiple related spans. See {@link addLink} for more details. @@ -265,7 +265,7 @@ export interface Span { * Prefer setting links directly when starting a span (e.g. `Sentry.startSpan()`) as some context information is only available during span creation. * @param links - Array of links to associate with this span */ - addLinks(links: SpanLink[] | unknown): this; + addLinks(links: SpanLink[]): this; /** * NOT USED IN SENTRY, only added for compliance with OTEL Span interface