Skip to content

Commit 4f34b5a

Browse files
authored
feat(core): Add trace function (#7556)
```js const fetchResult = Sentry.trace({ name: 'GET /users'}, () => fetch('/users'), handleError); ```
1 parent a73f58b commit 4f34b5a

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { extractTraceparentData, getActiveTransaction, stripUrlQueryAndFragment,
66
// eslint-disable-next-line deprecation/deprecation
77
export { SpanStatus } from './spanstatus';
88
export type { SpanStatusType } from './span';
9+
export { trace } from './trace';

packages/core/src/tracing/trace.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { TransactionContext } from '@sentry/types';
2+
import { isThenable } from '@sentry/utils';
3+
4+
import { getCurrentHub } from '../hub';
5+
import type { Span } from './span';
6+
7+
/**
8+
* Wraps a function with a transaction/span and finishes the span after the function is done.
9+
*
10+
* This function is meant to be used internally and may break at any time. Use at your own risk.
11+
*
12+
* @internal
13+
* @private
14+
*/
15+
export function trace<T>(
16+
context: TransactionContext,
17+
callback: (span: Span) => T,
18+
// eslint-disable-next-line @typescript-eslint/no-empty-function
19+
onError: (error: unknown) => void = () => {},
20+
): T {
21+
const ctx = { ...context };
22+
// If a name is set and a description is not, set the description to the name.
23+
if (ctx.name !== undefined && ctx.description === undefined) {
24+
ctx.description = ctx.name;
25+
}
26+
27+
const hub = getCurrentHub();
28+
const scope = hub.getScope();
29+
30+
const parentSpan = scope.getSpan();
31+
const activeSpan = parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
32+
scope.setSpan(activeSpan);
33+
34+
function finishAndSetSpan(): void {
35+
activeSpan.finish();
36+
hub.getScope().setSpan(parentSpan);
37+
}
38+
39+
let maybePromiseResult: T;
40+
try {
41+
maybePromiseResult = callback(activeSpan);
42+
} catch (e) {
43+
activeSpan.setStatus('internal_error');
44+
onError(e);
45+
finishAndSetSpan();
46+
throw e;
47+
}
48+
49+
if (isThenable(maybePromiseResult)) {
50+
Promise.resolve(maybePromiseResult).then(
51+
() => {
52+
finishAndSetSpan();
53+
},
54+
e => {
55+
activeSpan.setStatus('internal_error');
56+
onError(e);
57+
finishAndSetSpan();
58+
},
59+
);
60+
} else {
61+
finishAndSetSpan();
62+
}
63+
64+
return maybePromiseResult;
65+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { addTracingExtensions, Hub, makeMain } from '../../../src';
2+
import { trace } from '../../../src/tracing';
3+
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
4+
5+
beforeAll(() => {
6+
addTracingExtensions();
7+
});
8+
9+
const enum Type {
10+
Sync = 'sync',
11+
Async = 'async',
12+
}
13+
14+
let hub: Hub;
15+
let client: TestClient;
16+
17+
describe('trace', () => {
18+
beforeEach(() => {
19+
const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 });
20+
client = new TestClient(options);
21+
hub = new Hub(client);
22+
makeMain(hub);
23+
});
24+
25+
describe.each([
26+
// isSync, isError, callback, expectedReturnValue
27+
[Type.Async, false, () => Promise.resolve('async good'), 'async good'],
28+
[Type.Sync, false, () => 'sync good', 'sync good'],
29+
[Type.Async, true, () => Promise.reject('async bad'), 'async bad'],
30+
[
31+
Type.Sync,
32+
true,
33+
() => {
34+
throw 'sync bad';
35+
},
36+
'sync bad',
37+
],
38+
])('with %s callback and error %s', (_type, isError, callback, expected) => {
39+
it('should return the same value as the callback', async () => {
40+
try {
41+
const result = await trace({ name: 'GET users/[id]' }, () => {
42+
return callback();
43+
});
44+
expect(result).toEqual(expected);
45+
} catch (e) {
46+
expect(e).toEqual(expected);
47+
}
48+
});
49+
50+
it('creates a transaction', async () => {
51+
let ref: any = undefined;
52+
client.on('finishTransaction', transaction => {
53+
ref = transaction;
54+
});
55+
try {
56+
await trace({ name: 'GET users/[id]' }, () => {
57+
return callback();
58+
});
59+
} catch (e) {
60+
//
61+
}
62+
expect(ref).toBeDefined();
63+
64+
expect(ref.name).toEqual('GET users/[id]');
65+
expect(ref.status).toEqual(isError ? 'internal_error' : undefined);
66+
});
67+
68+
it('allows traceparent information to be overriden', async () => {
69+
let ref: any = undefined;
70+
client.on('finishTransaction', transaction => {
71+
ref = transaction;
72+
});
73+
try {
74+
await trace(
75+
{
76+
name: 'GET users/[id]',
77+
parentSampled: true,
78+
traceId: '12345678901234567890123456789012',
79+
parentSpanId: '1234567890123456',
80+
},
81+
() => {
82+
return callback();
83+
},
84+
);
85+
} catch (e) {
86+
//
87+
}
88+
expect(ref).toBeDefined();
89+
90+
expect(ref.sampled).toEqual(true);
91+
expect(ref.traceId).toEqual('12345678901234567890123456789012');
92+
expect(ref.parentSpanId).toEqual('1234567890123456');
93+
});
94+
95+
it('allows for transaction to be mutated', async () => {
96+
let ref: any = undefined;
97+
client.on('finishTransaction', transaction => {
98+
ref = transaction;
99+
});
100+
try {
101+
await trace({ name: 'GET users/[id]' }, span => {
102+
span.op = 'http.server';
103+
return callback();
104+
});
105+
} catch (e) {
106+
//
107+
}
108+
109+
expect(ref.op).toEqual('http.server');
110+
});
111+
112+
it('creates a span with correct description', async () => {
113+
let ref: any = undefined;
114+
client.on('finishTransaction', transaction => {
115+
ref = transaction;
116+
});
117+
try {
118+
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
119+
return trace({ name: 'SELECT * from users' }, () => {
120+
return callback();
121+
});
122+
});
123+
} catch (e) {
124+
//
125+
}
126+
127+
expect(ref.spanRecorder.spans).toHaveLength(2);
128+
expect(ref.spanRecorder.spans[1].description).toEqual('SELECT * from users');
129+
expect(ref.spanRecorder.spans[1].parentSpanId).toEqual(ref.spanId);
130+
expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined);
131+
});
132+
133+
it('allows for span to be mutated', async () => {
134+
let ref: any = undefined;
135+
client.on('finishTransaction', transaction => {
136+
ref = transaction;
137+
});
138+
try {
139+
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
140+
return trace({ name: 'SELECT * from users' }, childSpan => {
141+
childSpan.op = 'db.query';
142+
return callback();
143+
});
144+
});
145+
} catch (e) {
146+
//
147+
}
148+
149+
expect(ref.spanRecorder.spans).toHaveLength(2);
150+
expect(ref.spanRecorder.spans[1].op).toEqual('db.query');
151+
});
152+
153+
it('calls `onError` hook', async () => {
154+
const onError = jest.fn();
155+
try {
156+
await trace(
157+
{ name: 'GET users/[id]' },
158+
() => {
159+
return callback();
160+
},
161+
onError,
162+
);
163+
} catch (e) {
164+
expect(onError).toHaveBeenCalledTimes(1);
165+
expect(onError).toHaveBeenCalledWith(e);
166+
}
167+
expect(onError).toHaveBeenCalledTimes(isError ? 1 : 0);
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)