From 46a5abbfa310ee795ea5ebd0ec206d434eaeeec2 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Fri, 24 Apr 2020 13:37:47 +0200 Subject: [PATCH] feat: Add convenience helper functions for apm --- packages/apm/src/helper.ts | 38 ++++++++++++++ packages/apm/src/index.bundle.ts | 1 + packages/apm/src/index.ts | 1 + packages/apm/src/span.ts | 14 ++++++ packages/apm/test/helper.test.ts | 86 ++++++++++++++++++++++++++++++++ packages/apm/test/tslint.json | 4 +- 6 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 packages/apm/src/helper.ts create mode 100644 packages/apm/test/helper.test.ts diff --git a/packages/apm/src/helper.ts b/packages/apm/src/helper.ts new file mode 100644 index 000000000000..bb8046153e2e --- /dev/null +++ b/packages/apm/src/helper.ts @@ -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, +): Promise { + return withSpan( + { + ...spanContext, + transaction: name, + }, + callback, + ); +} + +/** + * Create a span from a callback. Make sure you wrap you `withSpan` calls into a transaction. + */ +export async function withSpan(spanContext: SpanContext = {}, callback?: (span: Span) => Promise): Promise { + const span = getCurrentHub().startSpan({ + ...spanContext, + }) as Span; + if (callback) { + await callback(span); + } + span.finish(); +} diff --git a/packages/apm/src/index.bundle.ts b/packages/apm/src/index.bundle.ts index 0d39253e7980..ddc9abc8f498 100644 --- a/packages/apm/src/index.bundle.ts +++ b/packages/apm/src/index.bundle.ts @@ -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 = {}; diff --git a/packages/apm/src/index.ts b/packages/apm/src/index.ts index 301dbc7fd38a..0ef0c9919f99 100644 --- a/packages/apm/src/index.ts +++ b/packages/apm/src/index.ts @@ -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(); diff --git a/packages/apm/src/span.ts b/packages/apm/src/span.ts index 6fafa6d8e34b..b332b13f737a 100644 --- a/packages/apm/src/span.ts +++ b/packages/apm/src/span.ts @@ -193,6 +193,20 @@ export class Span implements SpanInterface, SpanContext { return span; } + /** + * Create a child with a async callback + */ + public async withChild( + spanContext: Pick> = {}, + callback?: (span: Span) => Promise, + ): Promise { + const child = this.child(spanContext); + if (callback) { + await callback(child); + } + child.finish(); + } + /** * @inheritDoc */ diff --git a/packages/apm/test/helper.test.ts b/packages/apm/test/helper.test.ts new file mode 100644 index 000000000000..0e19d9f971c5 --- /dev/null +++ b/packages/apm/test/helper.test.ts @@ -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((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); + }); + }); +}); diff --git a/packages/apm/test/tslint.json b/packages/apm/test/tslint.json index 0827b5c40259..d5e5bf986d1a 100644 --- a/packages/apm/test/tslint.json +++ b/packages/apm/test/tslint.json @@ -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 } }