Skip to content

feat: Add convenience helper functions for apm #2556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/apm/src/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* This files exports some global helper functions to make it easier to work with tracing/apm
*/
import { getCurrentHub } from '@sentry/browser';
import { SpanContext } from '@sentry/types';

import { Span } from './span';

/**
* You need to wrap spans into a transaction in order for them to show up.
* After this function returns the transaction will be sent to Sentry.
*/
export async function withTransaction(
name: string,
spanContext: SpanContext = {},
callback: (transaction: Span) => Promise<void>,
): Promise<void> {
return withSpan(
{
...spanContext,
transaction: name,
},
callback,
);
}

/**
* Create a span from a callback. Make sure you wrap you `withSpan` calls into a transaction.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a draft of this idea but without the caveat of "make sure you wrap with a transaction".

Generally this is one of the reasons I don't like the transaction x span distinction, because it makes it harder to arbitrarily wrap code.

*/
export async function withSpan(spanContext: SpanContext = {}, callback?: (span: Span) => Promise<void>): Promise<void> {
const span = getCurrentHub().startSpan({
...spanContext,
}) as Span;
if (callback) {
await callback(span);
}
span.finish();
}
1 change: 1 addition & 0 deletions packages/apm/src/index.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { addExtensionMethods } from './hubextensions';
import * as ApmIntegrations from './integrations';

export { Span, TRACEPARENT_REGEXP } from './span';
export { withSpan, withTransaction } from './helper';

let windowIntegrations = {};

Expand Down
1 change: 1 addition & 0 deletions packages/apm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as ApmIntegrations from './integrations';

export { ApmIntegrations as Integrations };
export { Span, TRACEPARENT_REGEXP } from './span';
export { withSpan, withTransaction } from './helper';

// We are patching the global object with our hub extension methods
addExtensionMethods();
14 changes: 14 additions & 0 deletions packages/apm/src/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ export class Span implements SpanInterface, SpanContext {
return span;
}

/**
* Create a child with a async callback
*/
public async withChild(
spanContext: Pick<SpanContext, Exclude<keyof SpanContext, 'spanId' | 'sampled' | 'traceId' | 'parentSpanId'>> = {},
callback?: (span: Span) => Promise<void>,
): Promise<void> {
const child = this.child(spanContext);
if (callback) {
await callback(child);
}
child.finish();
}

/**
* @inheritDoc
*/
Expand Down
86 changes: 86 additions & 0 deletions packages/apm/test/helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { BrowserClient } from '@sentry/browser';
import { Hub, makeMain, Scope } from '@sentry/hub';

import { Span, withSpan, withTransaction } from '../src';

describe('APM Helpers', () => {
let hub: Hub;

beforeEach(() => {
jest.resetAllMocks();
const myScope = new Scope();
hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }), myScope);
makeMain(hub);
});

describe('helpers', () => {
test('withTransaction', async () => {
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
let capturedTransaction: Span;
await withTransaction('a', { op: 'op' }, async (transaction: Span) => {
expect(transaction.op).toEqual('op');
capturedTransaction = transaction;
});
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0].spans).toHaveLength(0);
expect(spy.mock.calls[0][0].contexts.trace).toEqual(capturedTransaction!.getTraceContext());
});

test('withTransaction + withSpan', async () => {
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
await withTransaction('a', { op: 'op' }, async (transaction: Span) => {
await transaction.withChild({
op: 'sub',
});
});
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0].spans).toHaveLength(1);
expect(spy.mock.calls[0][0].spans[0].op).toEqual('sub');
});

test('withSpan', async () => {
const spy = jest.spyOn(hub as any, 'captureEvent') as any;

// Setting transaction on the scope
const transaction = hub.startSpan({
transaction: 'transaction',
});
hub.configureScope((scope: Scope) => {
scope.setSpan(transaction);
});

let capturedSpan: Span;
await withSpan({ op: 'op' }, async (span: Span) => {
expect(span.op).toEqual('op');
capturedSpan = span;
});
expect(spy).not.toHaveBeenCalled();
expect(capturedSpan!.op).toEqual('op');
});

test('withTransaction + withSpan + timing', async () => {
jest.useRealTimers();
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
await withTransaction('a', { op: 'op' }, async (transaction: Span) => {
await transaction.withChild(
{
op: 'sub',
},
async () => {
const ret = new Promise<void>((resolve: any) => {
setTimeout(() => {
resolve();
}, 1100);
});
return ret;
},
);
});
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0].spans).toHaveLength(1);
expect(spy.mock.calls[0][0].spans[0].op).toEqual('sub');
const duration = spy.mock.calls[0][0].spans[0].timestamp - spy.mock.calls[0][0].spans[0].startTimestamp;
expect(duration).toBeGreaterThanOrEqual(1);
});
});
});
4 changes: 3 additions & 1 deletion packages/apm/test/tslint.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"extends": ["../tslint.json"],
"rules": {
"no-unsafe-any": false
"no-unsafe-any": false,
"no-non-null-assertion": false,
"no-unnecessary-type-assertion": false
}
}