Skip to content

Commit

Permalink
feat(core): Add trace function (#7556)
Browse files Browse the repository at this point in the history
```js
const fetchResult = Sentry.trace({ name: 'GET /users'}, () => fetch('/users'), handleError);
```
  • Loading branch information
AbhiPrasad committed Mar 22, 2023
1 parent a73f58b commit 4f34b5a
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { extractTraceparentData, getActiveTransaction, stripUrlQueryAndFragment,
// eslint-disable-next-line deprecation/deprecation
export { SpanStatus } from './spanstatus';
export type { SpanStatusType } from './span';
export { trace } from './trace';
65 changes: 65 additions & 0 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import { getCurrentHub } from '../hub';
import type { Span } from './span';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
*
* This function is meant to be used internally and may break at any time. Use at your own risk.
*
* @internal
* @private
*/
export function trace<T>(
context: TransactionContext,
callback: (span: Span) => T,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onError: (error: unknown) => void = () => {},
): T {
const ctx = { ...context };
// If a name is set and a description is not, set the description to the name.
if (ctx.name !== undefined && ctx.description === undefined) {
ctx.description = ctx.name;
}

const hub = getCurrentHub();
const scope = hub.getScope();

const parentSpan = scope.getSpan();
const activeSpan = parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
scope.setSpan(activeSpan);

function finishAndSetSpan(): void {
activeSpan.finish();
hub.getScope().setSpan(parentSpan);
}

let maybePromiseResult: T;
try {
maybePromiseResult = callback(activeSpan);
} catch (e) {
activeSpan.setStatus('internal_error');
onError(e);
finishAndSetSpan();
throw e;
}

if (isThenable(maybePromiseResult)) {
Promise.resolve(maybePromiseResult).then(
() => {
finishAndSetSpan();
},
e => {
activeSpan.setStatus('internal_error');
onError(e);
finishAndSetSpan();
},
);
} else {
finishAndSetSpan();
}

return maybePromiseResult;
}
170 changes: 170 additions & 0 deletions packages/core/test/lib/tracing/trace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { addTracingExtensions, Hub, makeMain } from '../../../src';
import { trace } from '../../../src/tracing';
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';

beforeAll(() => {
addTracingExtensions();
});

const enum Type {
Sync = 'sync',
Async = 'async',
}

let hub: Hub;
let client: TestClient;

describe('trace', () => {
beforeEach(() => {
const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 });
client = new TestClient(options);
hub = new Hub(client);
makeMain(hub);
});

describe.each([
// isSync, isError, callback, expectedReturnValue
[Type.Async, false, () => Promise.resolve('async good'), 'async good'],
[Type.Sync, false, () => 'sync good', 'sync good'],
[Type.Async, true, () => Promise.reject('async bad'), 'async bad'],
[
Type.Sync,
true,
() => {
throw 'sync bad';
},
'sync bad',
],
])('with %s callback and error %s', (_type, isError, callback, expected) => {
it('should return the same value as the callback', async () => {
try {
const result = await trace({ name: 'GET users/[id]' }, () => {
return callback();
});
expect(result).toEqual(expected);
} catch (e) {
expect(e).toEqual(expected);
}
});

it('creates a transaction', async () => {
let ref: any = undefined;
client.on('finishTransaction', transaction => {
ref = transaction;
});
try {
await trace({ name: 'GET users/[id]' }, () => {
return callback();
});
} catch (e) {
//
}
expect(ref).toBeDefined();

expect(ref.name).toEqual('GET users/[id]');
expect(ref.status).toEqual(isError ? 'internal_error' : undefined);
});

it('allows traceparent information to be overriden', async () => {
let ref: any = undefined;
client.on('finishTransaction', transaction => {
ref = transaction;
});
try {
await trace(
{
name: 'GET users/[id]',
parentSampled: true,
traceId: '12345678901234567890123456789012',
parentSpanId: '1234567890123456',
},
() => {
return callback();
},
);
} catch (e) {
//
}
expect(ref).toBeDefined();

expect(ref.sampled).toEqual(true);
expect(ref.traceId).toEqual('12345678901234567890123456789012');
expect(ref.parentSpanId).toEqual('1234567890123456');
});

it('allows for transaction to be mutated', async () => {
let ref: any = undefined;
client.on('finishTransaction', transaction => {
ref = transaction;
});
try {
await trace({ name: 'GET users/[id]' }, span => {
span.op = 'http.server';
return callback();
});
} catch (e) {
//
}

expect(ref.op).toEqual('http.server');
});

it('creates a span with correct description', async () => {
let ref: any = undefined;
client.on('finishTransaction', transaction => {
ref = transaction;
});
try {
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
return trace({ name: 'SELECT * from users' }, () => {
return callback();
});
});
} catch (e) {
//
}

expect(ref.spanRecorder.spans).toHaveLength(2);
expect(ref.spanRecorder.spans[1].description).toEqual('SELECT * from users');
expect(ref.spanRecorder.spans[1].parentSpanId).toEqual(ref.spanId);
expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined);
});

it('allows for span to be mutated', async () => {
let ref: any = undefined;
client.on('finishTransaction', transaction => {
ref = transaction;
});
try {
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
return trace({ name: 'SELECT * from users' }, childSpan => {
childSpan.op = 'db.query';
return callback();
});
});
} catch (e) {
//
}

expect(ref.spanRecorder.spans).toHaveLength(2);
expect(ref.spanRecorder.spans[1].op).toEqual('db.query');
});

it('calls `onError` hook', async () => {
const onError = jest.fn();
try {
await trace(
{ name: 'GET users/[id]' },
() => {
return callback();
},
onError,
);
} catch (e) {
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(e);
}
expect(onError).toHaveBeenCalledTimes(isError ? 1 : 0);
});
});
});

0 comments on commit 4f34b5a

Please sign in to comment.