Skip to content

Commit 97e1d50

Browse files
committed
feat: Add convinience helper functions for apm
1 parent ea41928 commit 97e1d50

File tree

6 files changed

+143
-1
lines changed

6 files changed

+143
-1
lines changed

packages/apm/src/helper.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* This files exports some global helper functions to make it easier to work with tracing/apm
3+
*/
4+
import { getCurrentHub } from '@sentry/browser';
5+
import { SpanContext } from '@sentry/types';
6+
7+
import { Span } from './span';
8+
9+
/**
10+
* You need to wrap spans into a transaction in order for them to show up.
11+
* After this function returns the transaction will be sent to Sentry.
12+
*/
13+
export async function withTransaction(
14+
name: string,
15+
spanContext: SpanContext = {},
16+
callback: (transaction: Span) => Promise<void>,
17+
): Promise<void> {
18+
return withSpan(
19+
{
20+
...spanContext,
21+
transaction: name,
22+
},
23+
callback,
24+
);
25+
}
26+
27+
/**
28+
* Create a span from a callback. Make sure you wrap you `withSpan` calls into a transaction.
29+
*/
30+
export async function withSpan(spanContext: SpanContext = {}, callback?: (span: Span) => Promise<void>): Promise<void> {
31+
const span = getCurrentHub().startSpan({
32+
...spanContext,
33+
}) as Span;
34+
if (callback) {
35+
await callback(span);
36+
}
37+
span.finish();
38+
}

packages/apm/src/index.bundle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { addExtensionMethods } from './hubextensions';
5656
import * as ApmIntegrations from './integrations';
5757

5858
export { Span, TRACEPARENT_REGEXP } from './span';
59+
export { withSpan, withTransaction } from './helper';
5960

6061
let windowIntegrations = {};
6162

packages/apm/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as ApmIntegrations from './integrations';
33

44
export { ApmIntegrations as Integrations };
55
export { Span, TRACEPARENT_REGEXP } from './span';
6+
export { withSpan, withTransaction } from './helper';
67

78
// We are patching the global object with our hub extension methods
89
addExtensionMethods();

packages/apm/src/span.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ export class Span implements SpanInterface, SpanContext {
193193
return span;
194194
}
195195

196+
/**
197+
* Create a child with a async callback
198+
*/
199+
public async withChild(
200+
spanContext: Pick<SpanContext, Exclude<keyof SpanContext, 'spanId' | 'sampled' | 'traceId' | 'parentSpanId'>> = {},
201+
callback?: (span: Span) => Promise<void>,
202+
): Promise<void> {
203+
const child = this.child(spanContext);
204+
if (callback) {
205+
await callback(child);
206+
}
207+
child.finish();
208+
}
209+
196210
/**
197211
* @inheritDoc
198212
*/

packages/apm/test/helper.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { BrowserClient } from '@sentry/browser';
2+
import { Hub, makeMain, Scope } from '@sentry/hub';
3+
4+
import { Span, withSpan, withTransaction } from '../src';
5+
6+
describe('APM Helpers', () => {
7+
let hub: Hub;
8+
9+
beforeEach(() => {
10+
jest.resetAllMocks();
11+
const myScope = new Scope();
12+
hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }), myScope);
13+
makeMain(hub);
14+
});
15+
16+
describe('helpers', () => {
17+
test('withTransaction', async () => {
18+
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
19+
let capturedTransaction: Span;
20+
await withTransaction('a', { op: 'op' }, async (transaction: Span) => {
21+
expect(transaction.op).toEqual('op');
22+
capturedTransaction = transaction;
23+
});
24+
expect(spy).toHaveBeenCalled();
25+
expect(spy.mock.calls[0][0].spans).toHaveLength(0);
26+
expect(spy.mock.calls[0][0].contexts.trace).toEqual(capturedTransaction!.getTraceContext());
27+
});
28+
29+
test('withTransaction + withSpan', async () => {
30+
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
31+
await withTransaction('a', { op: 'op' }, async (transaction: Span) => {
32+
await transaction.withChild({
33+
op: 'sub',
34+
});
35+
});
36+
expect(spy).toHaveBeenCalled();
37+
expect(spy.mock.calls[0][0].spans).toHaveLength(1);
38+
expect(spy.mock.calls[0][0].spans[0].op).toEqual('sub');
39+
});
40+
41+
test('withSpan', async () => {
42+
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
43+
44+
// Setting transaction on the scope
45+
const transaction = hub.startSpan({
46+
transaction: 'transaction',
47+
});
48+
hub.configureScope((scope: Scope) => {
49+
scope.setSpan(transaction);
50+
});
51+
52+
let capturedSpan: Span;
53+
await withSpan({ op: 'op' }, async (span: Span) => {
54+
expect(span.op).toEqual('op');
55+
capturedSpan = span;
56+
});
57+
expect(spy).not.toHaveBeenCalled();
58+
expect(capturedSpan!.op).toEqual('op');
59+
});
60+
61+
test('withTransaction + withSpan + timing', async () => {
62+
jest.useRealTimers();
63+
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
64+
await withTransaction('a', { op: 'op' }, async (transaction: Span) => {
65+
await transaction.withChild(
66+
{
67+
op: 'sub',
68+
},
69+
async () => {
70+
const ret = new Promise<void>((resolve: any) => {
71+
setTimeout(() => {
72+
resolve();
73+
}, 1100);
74+
});
75+
return ret;
76+
},
77+
);
78+
});
79+
expect(spy).toHaveBeenCalled();
80+
expect(spy.mock.calls[0][0].spans).toHaveLength(1);
81+
expect(spy.mock.calls[0][0].spans[0].op).toEqual('sub');
82+
const duration = spy.mock.calls[0][0].spans[0].timestamp - spy.mock.calls[0][0].spans[0].startTimestamp;
83+
expect(duration).toBeGreaterThanOrEqual(1);
84+
});
85+
});
86+
});

packages/apm/test/tslint.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"extends": ["../tslint.json"],
33
"rules": {
4-
"no-unsafe-any": false
4+
"no-unsafe-any": false,
5+
"no-non-null-assertion": false,
6+
"no-unnecessary-type-assertion": false
57
}
68
}

0 commit comments

Comments
 (0)