Skip to content

Commit

Permalink
feat(node): Add links to span options
Browse files Browse the repository at this point in the history
  • Loading branch information
s1gr1d committed Feb 13, 2025
1 parent 91fde6d commit 4372b90
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [],
transport: loggingTransport,
});

const parentSpan1 = Sentry.startInactiveSpan({ name: 'parent1' });
parentSpan1.end();

// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan(
{
name: 'parent2',
links: [{ context: parentSpan1.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }],
},
async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'child2.1' }, async childSpan1 => {
childSpan1.end();
});
},
);
38 changes: 32 additions & 6 deletions dev-packages/node-integration-tests/suites/tracing/linking/test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import { createRunner } from '../../../utils/runner';

describe('span links', () => {
test('should link spans by adding "links" to span options', done => {
let span1_traceId: string, span1_spanId: string;

createRunner(__dirname, 'scenario-span-options.ts')
.expect({
transaction: event => {
expect(event.transaction).toBe('parent1');

const traceContext = event.contexts?.trace;
span1_traceId = traceContext?.trace_id as string;
span1_spanId = traceContext?.span_id as string;
},
})
.expect({
transaction: event => {
expect(event.transaction).toBe('parent2');

const traceContext = event.contexts?.trace;
expect(traceContext).toBeDefined();
expect(traceContext?.links).toEqual([
expect.objectContaining({
trace_id: expect.stringMatching(span1_traceId),
span_id: expect.stringMatching(span1_spanId),
}),
]);
},
})
.start(done);
});

test('should link spans with addLink() in trace context', done => {
let span1_traceId: string, span1_spanId: string;

Expand Down Expand Up @@ -143,12 +173,8 @@ describe('span links', () => {
span_id: expect.stringMatching(parent1_spanId),
}),
expect.objectContaining({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error traceID is defined
trace_id: expect.stringMatching(child1_1?.trace_id),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error spanID is defined
span_id: expect.stringMatching(child1_1?.span_id),
trace_id: expect.stringMatching(child1_1?.trace_id as string),
span_id: expect.stringMatching(child1_1?.span_id as string),
attributes: expect.objectContaining({
'sentry.link.type': 'previous_trace',
}),
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/types-hoist/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export interface SentrySpanArguments {
*/
endTimestamp?: number | undefined;

/**
* Links to associate with the new span. Setting links here is preferred over addLink()
* as certain context information is only available during span creation.
*/
links?: SpanLink[];

/**
* Set to `true` if this span should be sent as a standalone segment span
* as opposed to a transaction.
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/types-hoist/startSpanOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Scope } from '../scope';
import type { SpanLink } from './link';
import type { Span, SpanAttributes, SpanTimeInput } from './span';

export interface StartSpanOptions {
Expand Down Expand Up @@ -44,6 +45,12 @@ export interface StartSpanOptions {
/** Attributes for the span. */
attributes?: SpanAttributes;

/**
* Links to associate with the new span. Setting links here is preferred over addLink()
* as it allows sampling decisions to consider the link information.
*/
links?: SpanLink[];

/**
* Experimental options without any stability guarantees. Use with caution!
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/opentelemetry/src/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function getTracer(): Tracer {
}

function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions {
const { startTime, attributes, kind, op } = options;
const { startTime, attributes, kind, op, links } = options;

// OTEL expects timestamps in ms, not seconds
const fixedStartTime = typeof startTime === 'number' ? ensureTimestampInMilliseconds(startTime) : startTime;
Expand All @@ -173,6 +173,7 @@ function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions {
}
: attributes,
kind,
links,
startTime: fixedStartTime,
};
}
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/test/spanExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,34 @@ describe('createTransactionForOtelSpan', () => {
);
});
});

it('adds span link to the trace context when linked in span options', () => {
const span = startInactiveSpan({ name: 'parent1' });

const prevTraceId = span.spanContext().traceId;
const prevSpanId = span.spanContext().spanId;

const linkedSpan = startInactiveSpan({
name: 'parent2',
links: [{ context: span.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }],
});

span.end();
linkedSpan.end();

const event = createTransactionForOtelSpan(linkedSpan as any);

expect(event.contexts?.trace).toEqual(
expect.objectContaining({
links: [
expect.objectContaining({
attributes: { 'sentry.link.type': 'previous_trace' },
sampled: true,
trace_id: expect.stringMatching(prevTraceId),
span_id: expect.stringMatching(prevSpanId),
}),
],
}),
);
});
});
114 changes: 114 additions & 0 deletions packages/opentelemetry/test/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,44 @@ describe('trace', () => {
});
});

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([]);

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 _spanContext exist on span
expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId);
// @ts-expect-error links and _spanContext exist on span
expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id);
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);

// @ts-expect-error links and _spanContext exist on span
expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId);
// @ts-expect-error links and _spanContext exist on span
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 client = getClient()!;
const transactionEvents: Event[] = [];
Expand Down Expand Up @@ -651,6 +689,44 @@ describe('trace', () => {
});
});

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([]);

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 _spanContext exist on span
expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId);
// @ts-expect-error links and _spanContext exist on span
expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id);
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);

// @ts-expect-error links and _spanContext exist on span
expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId);
// @ts-expect-error links and _spanContext exist on span
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 client = getClient()!;
const transactionEvents: Event[] = [];
Expand Down Expand Up @@ -974,6 +1050,44 @@ describe('trace', () => {
});
});

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([]);

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 _spanContext exist on span
expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId);
// @ts-expect-error links and _spanContext exist on span
expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id);
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);

// @ts-expect-error links and _spanContext exist on span
expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId);
// @ts-expect-error links and _spanContext exist on span
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 client = getClient()!;
const transactionEvents: Event[] = [];
Expand Down

0 comments on commit 4372b90

Please sign in to comment.