Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
limit: '68 KB',
limit: '70 KB',
modifyWebpackConfig: function (config) {
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [],
transport: loggingTransport,
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'parent1' }, async parentSpan1 => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'child1.1' }, async childSpan1 => {
childSpan1.addLink({
context: parentSpan1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
});

childSpan1.end();
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'child1.2' }, async childSpan2 => {
childSpan2.addLink({
context: parentSpan1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
});

childSpan2.end();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [],
transport: loggingTransport,
});

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

Sentry.startSpan({ name: 'rootSpan' }, rootSpan => {
rootSpan.addLink({
context: span1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [],
transport: loggingTransport,
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'parent1' }, async parentSpan1 => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'child1.1' }, async childSpan1 => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'child2.1' }, async childSpan2 => {
childSpan2.addLinks([
{ context: parentSpan1.spanContext() },
{
context: childSpan1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
},
]);

childSpan2.end();
});

childSpan1.end();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [],
transport: loggingTransport,
});

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

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

Sentry.startSpan({ name: 'rootSpan' }, rootSpan => {
rootSpan.addLinks([
{ context: span1.spanContext() },
{
context: span2.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
},
]);
});
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://[email protected]/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();
});
},
);
187 changes: 187 additions & 0 deletions dev-packages/node-integration-tests/suites/tracing/linking/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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;

createRunner(__dirname, 'scenario-addLink.ts')
.expect({
transaction: event => {
expect(event.transaction).toBe('span1');

span1_traceId = event.contexts?.trace?.trace_id as string;
span1_spanId = event.contexts?.trace?.span_id as string;

expect(event.spans).toEqual([]);
},
})
.expect({
transaction: event => {
expect(event.transaction).toBe('rootSpan');

expect(event.contexts?.trace?.links).toEqual([
expect.objectContaining({
trace_id: expect.stringMatching(span1_traceId),
span_id: expect.stringMatching(span1_spanId),
attributes: expect.objectContaining({
'sentry.link.type': 'previous_trace',
}),
}),
]);
},
})
.start(done);
});

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

createRunner(__dirname, 'scenario-addLinks.ts')
.expect({
transaction: event => {
expect(event.transaction).toBe('span1');

span1_traceId = event.contexts?.trace?.trace_id as string;
span1_spanId = event.contexts?.trace?.span_id as string;

expect(event.spans).toEqual([]);
},
})
.expect({
transaction: event => {
expect(event.transaction).toBe('span2');

span2_traceId = event.contexts?.trace?.trace_id as string;
span2_spanId = event.contexts?.trace?.span_id as string;

expect(event.spans).toEqual([]);
},
})
.expect({
transaction: event => {
expect(event.transaction).toBe('rootSpan');

expect(event.contexts?.trace?.links).toEqual([
expect.not.objectContaining({ attributes: expect.anything() }) &&
expect.objectContaining({
trace_id: expect.stringMatching(span1_traceId),
span_id: expect.stringMatching(span1_spanId),
}),
expect.objectContaining({
trace_id: expect.stringMatching(span2_traceId),
span_id: expect.stringMatching(span2_spanId),
attributes: expect.objectContaining({
'sentry.link.type': 'previous_trace',
}),
}),
]);
},
})
.start(done);
});

test('should link spans with addLink() in nested startSpan() calls', done => {
createRunner(__dirname, 'scenario-addLink-nested.ts')
.expect({
transaction: event => {
expect(event.transaction).toBe('parent1');

const parent1_traceId = event.contexts?.trace?.trace_id as string;
const parent1_spanId = event.contexts?.trace?.span_id as string;

const spans = event.spans || [];
const child1_1 = spans.find(span => span.description === 'child1.1');
const child1_2 = spans.find(span => span.description === 'child1.2');

expect(child1_1).toBeDefined();
expect(child1_1?.links).toEqual([
expect.objectContaining({
trace_id: expect.stringMatching(parent1_traceId),
span_id: expect.stringMatching(parent1_spanId),
attributes: expect.objectContaining({
'sentry.link.type': 'previous_trace',
}),
}),
]);

expect(child1_2).toBeDefined();
expect(child1_2?.links).toEqual([
expect.objectContaining({
trace_id: expect.stringMatching(parent1_traceId),
span_id: expect.stringMatching(parent1_spanId),
attributes: expect.objectContaining({
'sentry.link.type': 'previous_trace',
}),
}),
]);
},
})
.start(done);
});

test('should link spans with addLinks() in nested startSpan() calls', done => {
createRunner(__dirname, 'scenario-addLinks-nested.ts')
.expect({
transaction: event => {
expect(event.transaction).toBe('parent1');

const parent1_traceId = event.contexts?.trace?.trace_id as string;
const parent1_spanId = event.contexts?.trace?.span_id as string;

const spans = event.spans || [];
const child1_1 = spans.find(span => span.description === 'child1.1');
const child2_1 = spans.find(span => span.description === 'child2.1');

expect(child1_1).toBeDefined();

expect(child2_1).toBeDefined();

expect(child2_1?.links).toEqual([
expect.not.objectContaining({ attributes: expect.anything() }) &&
expect.objectContaining({
trace_id: expect.stringMatching(parent1_traceId),
span_id: expect.stringMatching(parent1_spanId),
}),
expect.objectContaining({
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',
}),
}),
]);
},
})
.start(done);
});
});
2 changes: 2 additions & 0 deletions packages/core/src/types-hoist/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FeatureFlag } from '../featureFlags';
import type { SpanLinkJSON } from './link';
import type { Primitive } from './misc';
import type { SpanOrigin } from './span';

Expand Down Expand Up @@ -106,6 +107,7 @@ export interface TraceContext extends Record<string, unknown> {
tags?: { [key: string]: Primitive };
trace_id: string;
origin?: SpanOrigin;
links?: SpanLinkJSON[];
}

export interface CloudResourceContext extends Record<string, unknown> {
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
Loading
Loading