Skip to content

Commit 5c085ef

Browse files
authored
feat(core): Introduce Sentry.startActiveSpan and Sentry.startSpan (#8803)
1 parent 448406a commit 5c085ef

File tree

6 files changed

+120
-59
lines changed

6 files changed

+120
-59
lines changed

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export { extractTraceparentData, getActiveTransaction } from './utils';
66
// eslint-disable-next-line deprecation/deprecation
77
export { SpanStatus } from './spanstatus';
88
export type { SpanStatusType } from './span';
9-
export { trace } from './trace';
9+
export { trace, getActiveSpan, startActiveSpan, startSpan } from './trace';
1010
export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
1111
export { setMeasurement } from './measurement';

packages/core/src/tracing/trace.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ export function trace<T>(
3434

3535
const parentSpan = scope.getSpan();
3636

37-
function getActiveSpan(): Span | undefined {
37+
function startActiveSpan(): Span | undefined {
3838
if (!hasTracingEnabled()) {
3939
return undefined;
4040
}
4141
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
4242
}
4343

44-
const activeSpan = getActiveSpan();
44+
const activeSpan = startActiveSpan();
4545
scope.setSpan(activeSpan);
4646

4747
function finishAndSetSpan(): void {
@@ -76,3 +76,100 @@ export function trace<T>(
7676

7777
return maybePromiseResult;
7878
}
79+
80+
/**
81+
* Wraps a function with a transaction/span and finishes the span after the function is done.
82+
* The created span is the active span and will be used as parent by other spans created inside the function
83+
* and can be accessed via `Sentry.getSpan()`, as long as the function is executed while the scope is active.
84+
*
85+
* If you want to create a span that is not set as active, use {@link startSpan}.
86+
*
87+
* Note that if you have not enabled tracing extensions via `addTracingExtensions`
88+
* or you didn't set `tracesSampleRate`, this function will not generate spans
89+
* and the `span` returned from the callback will be undefined.
90+
*/
91+
export function startActiveSpan<T>(context: TransactionContext, callback: (span: Span | undefined) => T): T {
92+
const ctx = { ...context };
93+
// If a name is set and a description is not, set the description to the name.
94+
if (ctx.name !== undefined && ctx.description === undefined) {
95+
ctx.description = ctx.name;
96+
}
97+
98+
const hub = getCurrentHub();
99+
const scope = hub.getScope();
100+
101+
const parentSpan = scope.getSpan();
102+
103+
function startActiveSpan(): Span | undefined {
104+
if (!hasTracingEnabled()) {
105+
return undefined;
106+
}
107+
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
108+
}
109+
110+
const activeSpan = startActiveSpan();
111+
scope.setSpan(activeSpan);
112+
113+
function finishAndSetSpan(): void {
114+
activeSpan && activeSpan.finish();
115+
hub.getScope().setSpan(parentSpan);
116+
}
117+
118+
let maybePromiseResult: T;
119+
try {
120+
maybePromiseResult = callback(activeSpan);
121+
} catch (e) {
122+
activeSpan && activeSpan.setStatus('internal_error');
123+
finishAndSetSpan();
124+
throw e;
125+
}
126+
127+
if (isThenable(maybePromiseResult)) {
128+
Promise.resolve(maybePromiseResult).then(
129+
() => {
130+
finishAndSetSpan();
131+
},
132+
() => {
133+
activeSpan && activeSpan.setStatus('internal_error');
134+
finishAndSetSpan();
135+
},
136+
);
137+
} else {
138+
finishAndSetSpan();
139+
}
140+
141+
return maybePromiseResult;
142+
}
143+
144+
/**
145+
* Creates a span. This span is not set as active, so will not get automatic instrumentation spans
146+
* as children or be able to be accessed via `Sentry.getSpan()`.
147+
*
148+
* If you want to create a span that is set as active, use {@link startActiveSpan}.
149+
*
150+
* Note that if you have not enabled tracing extensions via `addTracingExtensions`
151+
* or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans
152+
* and the `span` returned from the callback will be undefined.
153+
*/
154+
export function startSpan(context: TransactionContext): Span | undefined {
155+
if (!hasTracingEnabled()) {
156+
return undefined;
157+
}
158+
159+
const ctx = { ...context };
160+
// If a name is set and a description is not, set the description to the name.
161+
if (ctx.name !== undefined && ctx.description === undefined) {
162+
ctx.description = ctx.name;
163+
}
164+
165+
const hub = getCurrentHub();
166+
const parentSpan = getActiveSpan();
167+
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
168+
}
169+
170+
/**
171+
* Returns the currently active span.
172+
*/
173+
export function getActiveSpan(): Span | undefined {
174+
return getCurrentHub().getScope().getSpan();
175+
}

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { addTracingExtensions, Hub, makeMain } from '../../../src';
2-
import { trace } from '../../../src/tracing';
2+
import { startActiveSpan } from '../../../src/tracing';
33
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
44

55
beforeAll(() => {
@@ -14,7 +14,7 @@ const enum Type {
1414
let hub: Hub;
1515
let client: TestClient;
1616

17-
describe('trace', () => {
17+
describe('startActiveSpan', () => {
1818
beforeEach(() => {
1919
const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 });
2020
client = new TestClient(options);
@@ -38,7 +38,7 @@ describe('trace', () => {
3838
])('with %s callback and error %s', (_type, isError, callback, expected) => {
3939
it('should return the same value as the callback', async () => {
4040
try {
41-
const result = await trace({ name: 'GET users/[id]' }, () => {
41+
const result = await startActiveSpan({ name: 'GET users/[id]' }, () => {
4242
return callback();
4343
});
4444
expect(result).toEqual(expected);
@@ -53,7 +53,7 @@ describe('trace', () => {
5353
// if tracingExtensions are not enabled
5454
jest.spyOn(hub, 'startTransaction').mockReturnValue(undefined);
5555
try {
56-
const result = await trace({ name: 'GET users/[id]' }, () => {
56+
const result = await startActiveSpan({ name: 'GET users/[id]' }, () => {
5757
return callback();
5858
});
5959
expect(result).toEqual(expected);
@@ -68,7 +68,7 @@ describe('trace', () => {
6868
ref = transaction;
6969
});
7070
try {
71-
await trace({ name: 'GET users/[id]' }, () => {
71+
await startActiveSpan({ name: 'GET users/[id]' }, () => {
7272
return callback();
7373
});
7474
} catch (e) {
@@ -86,7 +86,7 @@ describe('trace', () => {
8686
ref = transaction;
8787
});
8888
try {
89-
await trace(
89+
await startActiveSpan(
9090
{
9191
name: 'GET users/[id]',
9292
parentSampled: true,
@@ -113,7 +113,7 @@ describe('trace', () => {
113113
ref = transaction;
114114
});
115115
try {
116-
await trace({ name: 'GET users/[id]' }, span => {
116+
await startActiveSpan({ name: 'GET users/[id]' }, span => {
117117
if (span) {
118118
span.op = 'http.server';
119119
}
@@ -132,8 +132,8 @@ describe('trace', () => {
132132
ref = transaction;
133133
});
134134
try {
135-
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
136-
return trace({ name: 'SELECT * from users' }, () => {
135+
await startActiveSpan({ name: 'GET users/[id]', parentSampled: true }, () => {
136+
return startActiveSpan({ name: 'SELECT * from users' }, () => {
137137
return callback();
138138
});
139139
});
@@ -153,8 +153,8 @@ describe('trace', () => {
153153
ref = transaction;
154154
});
155155
try {
156-
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
157-
return trace({ name: 'SELECT * from users' }, childSpan => {
156+
await startActiveSpan({ name: 'GET users/[id]', parentSampled: true }, () => {
157+
return startActiveSpan({ name: 'SELECT * from users' }, childSpan => {
158158
if (childSpan) {
159159
childSpan.op = 'db.query';
160160
}
@@ -168,50 +168,5 @@ describe('trace', () => {
168168
expect(ref.spanRecorder.spans).toHaveLength(2);
169169
expect(ref.spanRecorder.spans[1].op).toEqual('db.query');
170170
});
171-
172-
it('calls `onError` hook', async () => {
173-
const onError = jest.fn();
174-
try {
175-
await trace(
176-
{ name: 'GET users/[id]' },
177-
() => {
178-
return callback();
179-
},
180-
onError,
181-
);
182-
} catch (e) {
183-
expect(onError).toHaveBeenCalledTimes(1);
184-
expect(onError).toHaveBeenCalledWith(e);
185-
}
186-
expect(onError).toHaveBeenCalledTimes(isError ? 1 : 0);
187-
});
188-
189-
it("doesn't create spans but calls onError if tracing is disabled", async () => {
190-
const options = getDefaultTestClientOptions({
191-
/* we don't set tracesSampleRate or tracesSampler */
192-
});
193-
client = new TestClient(options);
194-
hub = new Hub(client);
195-
makeMain(hub);
196-
197-
const startTxnSpy = jest.spyOn(hub, 'startTransaction');
198-
199-
const onError = jest.fn();
200-
try {
201-
await trace(
202-
{ name: 'GET users/[id]' },
203-
() => {
204-
return callback();
205-
},
206-
onError,
207-
);
208-
} catch (e) {
209-
expect(onError).toHaveBeenCalledTimes(1);
210-
expect(onError).toHaveBeenCalledWith(e);
211-
}
212-
expect(onError).toHaveBeenCalledTimes(isError ? 1 : 0);
213-
214-
expect(startTxnSpy).not.toHaveBeenCalled();
215-
});
216171
});
217172
});

packages/node/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export {
5555
withScope,
5656
captureCheckIn,
5757
setMeasurement,
58+
getActiveSpan,
59+
startActiveSpan,
60+
startSpan,
5861
} from '@sentry/core';
5962
export type { SpanStatusType } from '@sentry/core';
6063
export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing';

packages/serverless/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@ export {
5050
Handlers,
5151
Integrations,
5252
setMeasurement,
53+
getActiveSpan,
54+
startActiveSpan,
55+
startSpan,
5356
} from '@sentry/node';

packages/sveltekit/src/server/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export {
4545
Integrations,
4646
Handlers,
4747
setMeasurement,
48+
getActiveSpan,
49+
startActiveSpan,
50+
startSpan,
4851
} from '@sentry/node';
4952

5053
// We can still leave this for the carrier init and type exports

0 commit comments

Comments
 (0)