Skip to content

Commit 53e1df9

Browse files
authored
fix: Make sure that mongo method is thenable before calling it (#3173)
* fix: Make sure that mongo method is thenable before calling it * test: Add test suite for MongoDB tracing integration
1 parent a7f5c00 commit 53e1df9

File tree

2 files changed

+125
-4
lines changed

2 files changed

+125
-4
lines changed

packages/tracing/src/integrations/mongo.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Hub } from '@sentry/hub';
22
import { EventProcessor, Integration, SpanContext } from '@sentry/types';
3-
import { dynamicRequire, fill, logger } from '@sentry/utils';
3+
import { dynamicRequire, fill, isThenable, logger } from '@sentry/utils';
44

55
// This allows us to use the same array for both defaults options and the type itself.
66
// (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... )
@@ -152,10 +152,17 @@ export class Mongo implements Integration {
152152
// its (non-callback) arguments can also be functions.)
153153
if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) {
154154
const span = parentSpan?.startChild(getSpanContext(this, operation, args));
155-
return (orig.call(this, ...args) as Promise<unknown>).then((res: unknown) => {
155+
const maybePromise = orig.call(this, ...args) as Promise<unknown>;
156+
157+
if (isThenable(maybePromise)) {
158+
return maybePromise.then((res: unknown) => {
159+
span?.finish();
160+
return res;
161+
});
162+
} else {
156163
span?.finish();
157-
return res;
158-
});
164+
return maybePromise;
165+
}
159166
}
160167

161168
const span = parentSpan?.startChild(getSpanContext(this, operation, args.slice(0, -1)));
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* eslint-disable @typescript-eslint/unbound-method */
2+
import { Hub, Scope } from '@sentry/hub';
3+
4+
import { Mongo } from '../../src/integrations/mongo';
5+
import { Span } from '../../src/span';
6+
7+
class Collection {
8+
public collectionName: string = 'mockedCollectionName';
9+
public dbName: string = 'mockedDbName';
10+
public namespace: string = 'mockedNamespace';
11+
12+
// Method that can have a callback as last argument, or return a promise otherwise.
13+
insertOne(_doc: unknown, _options: unknown, callback?: () => void) {
14+
if (typeof callback === 'function') {
15+
callback();
16+
return;
17+
}
18+
return Promise.resolve();
19+
}
20+
// Method that has no callback as last argument, and doesnt return promise.
21+
initializeOrderedBulkOp() {
22+
return {};
23+
}
24+
}
25+
26+
jest.mock('@sentry/utils', () => {
27+
const actual = jest.requireActual('@sentry/utils');
28+
return {
29+
...actual,
30+
dynamicRequire() {
31+
return {
32+
Collection,
33+
};
34+
},
35+
};
36+
});
37+
38+
describe('patchOperation()', () => {
39+
const doc = {
40+
name: 'PickleRick',
41+
answer: 42,
42+
};
43+
const collection: Collection = new Collection();
44+
let scope = new Scope();
45+
let parentSpan: Span;
46+
let childSpan: Span;
47+
48+
beforeAll(() => {
49+
new Mongo({
50+
operations: ['insertOne', 'initializeOrderedBulkOp'],
51+
}).setupOnce(
52+
() => undefined,
53+
() => new Hub(undefined, scope),
54+
);
55+
});
56+
57+
beforeEach(() => {
58+
scope = new Scope();
59+
parentSpan = new Span();
60+
childSpan = parentSpan.startChild();
61+
jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan);
62+
jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan);
63+
jest.spyOn(childSpan, 'finish');
64+
});
65+
66+
it('should wrap method accepting callback as the last argument', done => {
67+
collection.insertOne(doc, {}, function() {
68+
expect(scope.getSpan).toBeCalled();
69+
expect(parentSpan.startChild).toBeCalledWith({
70+
data: {
71+
collectionName: 'mockedCollectionName',
72+
dbName: 'mockedDbName',
73+
doc: JSON.stringify(doc),
74+
namespace: 'mockedNamespace',
75+
},
76+
op: `db`,
77+
description: 'insertOne',
78+
});
79+
expect(childSpan.finish).toBeCalled();
80+
done();
81+
}) as void;
82+
});
83+
84+
it('should wrap method accepting no callback as the last argument but returning promise', async () => {
85+
await collection.insertOne(doc, {});
86+
expect(scope.getSpan).toBeCalled();
87+
expect(parentSpan.startChild).toBeCalledWith({
88+
data: {
89+
collectionName: 'mockedCollectionName',
90+
dbName: 'mockedDbName',
91+
doc: JSON.stringify(doc),
92+
namespace: 'mockedNamespace',
93+
},
94+
op: `db`,
95+
description: 'insertOne',
96+
});
97+
expect(childSpan.finish).toBeCalled();
98+
});
99+
100+
it('should wrap method accepting no callback as the last argument and not returning promise', () => {
101+
collection.initializeOrderedBulkOp();
102+
expect(scope.getSpan).toBeCalled();
103+
expect(parentSpan.startChild).toBeCalledWith({
104+
data: {
105+
collectionName: 'mockedCollectionName',
106+
dbName: 'mockedDbName',
107+
namespace: 'mockedNamespace',
108+
},
109+
op: `db`,
110+
description: 'initializeOrderedBulkOp',
111+
});
112+
expect(childSpan.finish).toBeCalled();
113+
});
114+
});

0 commit comments

Comments
 (0)