Skip to content

Commit 117bc99

Browse files
kamilogorekHazATrhcarvalho
authored
feat: Autoload Database Integrations in Node environment (#3483)
* feat: Autoload Database Integrations in Node environment * Remove value before deleting __SENTRY__ key * ref: Attach autoloaded integrations to defaultIntegrations * Remove unnecessary comment and fix attaching integrations to global object * Use packageToIntegrationMapping keys for iteration Co-authored-by: Rodolfo Carvalho <[email protected]> * Update FIXME comment for getMainCarrier and loadModule Co-authored-by: HazA <[email protected]> Co-authored-by: Rodolfo Carvalho <[email protected]>
1 parent 3b19a03 commit 117bc99

File tree

12 files changed

+242
-79
lines changed

12 files changed

+242
-79
lines changed

packages/core/src/integration.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,37 @@ export interface IntegrationIndex {
99
[key: string]: Integration;
1010
}
1111

12+
/**
13+
* @private
14+
*/
15+
function filterDuplicates(integrations: Integration[]): Integration[] {
16+
return integrations.reduce((acc, integrations) => {
17+
if (acc.every(accIntegration => integrations.name !== accIntegration.name)) {
18+
acc.push(integrations);
19+
}
20+
return acc;
21+
}, [] as Integration[]);
22+
}
23+
1224
/** Gets integration to install */
1325
export function getIntegrationsToSetup(options: Options): Integration[] {
1426
const defaultIntegrations = (options.defaultIntegrations && [...options.defaultIntegrations]) || [];
1527
const userIntegrations = options.integrations;
16-
let integrations: Integration[] = [];
17-
if (Array.isArray(userIntegrations)) {
18-
const userIntegrationsNames = userIntegrations.map(i => i.name);
19-
const pickedIntegrationsNames: string[] = [];
2028

21-
// Leave only unique default integrations, that were not overridden with provided user integrations
22-
defaultIntegrations.forEach(defaultIntegration => {
23-
if (
24-
userIntegrationsNames.indexOf(defaultIntegration.name) === -1 &&
25-
pickedIntegrationsNames.indexOf(defaultIntegration.name) === -1
26-
) {
27-
integrations.push(defaultIntegration);
28-
pickedIntegrationsNames.push(defaultIntegration.name);
29-
}
30-
});
29+
let integrations: Integration[] = [...filterDuplicates(defaultIntegrations)];
3130

32-
// Don't add same user integration twice
33-
userIntegrations.forEach(userIntegration => {
34-
if (pickedIntegrationsNames.indexOf(userIntegration.name) === -1) {
35-
integrations.push(userIntegration);
36-
pickedIntegrationsNames.push(userIntegration.name);
37-
}
38-
});
31+
if (Array.isArray(userIntegrations)) {
32+
// Filter out integrations that are also included in user options
33+
integrations = [
34+
...integrations.filter(integrations =>
35+
userIntegrations.every(userIntegration => userIntegration.name !== integrations.name),
36+
),
37+
// And filter out duplicated user options integrations
38+
...filterDuplicates(userIntegrations),
39+
];
3940
} else if (typeof userIntegrations === 'function') {
40-
integrations = userIntegrations(defaultIntegrations);
41+
integrations = userIntegrations(integrations);
4142
integrations = Array.isArray(integrations) ? integrations : [integrations];
42-
} else {
43-
integrations = [...defaultIntegrations];
4443
}
4544

4645
// Make sure that if present, `Debug` integration will always run last

packages/core/test/lib/sdk.test.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,26 @@ declare var global: any;
99

1010
const PUBLIC_DSN = 'https://username@domain/123';
1111

12-
jest.mock('@sentry/hub', () => ({
13-
getCurrentHub(): {
14-
bindClient(client: Client): boolean;
15-
getClient(): boolean;
16-
} {
17-
return {
18-
getClient(): boolean {
19-
return false;
20-
},
21-
bindClient(client: Client): boolean {
22-
client.setupIntegrations();
23-
return true;
24-
},
25-
};
26-
},
27-
}));
12+
jest.mock('@sentry/hub', () => {
13+
const original = jest.requireActual('@sentry/hub');
14+
return {
15+
...original,
16+
getCurrentHub(): {
17+
bindClient(client: Client): boolean;
18+
getClient(): boolean;
19+
} {
20+
return {
21+
getClient(): boolean {
22+
return false;
23+
},
24+
bindClient(client: Client): boolean {
25+
client.setupIntegrations();
26+
return true;
27+
},
28+
};
29+
},
30+
};
31+
});
2832

2933
class MockIntegration implements Integration {
3034
public name: string;

packages/hub/src/hub.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { Session } from './session';
3535
*
3636
* @hidden
3737
*/
38-
export const API_VERSION = 3;
38+
export const API_VERSION = 4;
3939

4040
/**
4141
* Default maximum number of breadcrumbs added to an event. Can be overwritten
@@ -457,7 +457,13 @@ export class Hub implements HubInterface {
457457
}
458458
}
459459

460-
/** Returns the global shim registry. */
460+
/**
461+
* Returns the global shim registry.
462+
*
463+
* FIXME: This function is problematic, because despite always returning a valid Carrier,
464+
* it has an optional `__SENTRY__` property, which then in turn requires us to always perform an unnecessary check
465+
* at the call-site. We always access the carrier through this function, so we can guarantee that `__SENTRY__` is there.
466+
**/
461467
export function getMainCarrier(): Carrier {
462468
const carrier = getGlobalObject();
463469
carrier.__SENTRY__ = carrier.__SENTRY__ || {

packages/hub/src/interfaces.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Client } from '@sentry/types';
1+
import { Client, Integration } from '@sentry/types';
22

33
import { Hub } from './hub';
44
import { Scope } from './scope';
@@ -22,6 +22,7 @@ export interface Carrier {
2222
/**
2323
* Extra Hub properties injected by various SDKs
2424
*/
25+
integrations?: Integration[];
2526
extensions?: {
2627
/** Hack to prevent bundlers from breaking our usage of the domain package in the cross-platform Hub package */
2728
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/node/src/sdk.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,16 @@ export const defaultIntegrations = [
7878
* @see {@link NodeOptions} for documentation on configuration options.
7979
*/
8080
export function init(options: NodeOptions = {}): void {
81-
if (options.defaultIntegrations === undefined) {
82-
options.defaultIntegrations = defaultIntegrations;
83-
}
81+
const carrier = getMainCarrier();
82+
const autoloadedIntegrations = carrier.__SENTRY__?.integrations || [];
83+
84+
options.defaultIntegrations =
85+
options.defaultIntegrations === false
86+
? []
87+
: [
88+
...(Array.isArray(options.defaultIntegrations) ? options.defaultIntegrations : defaultIntegrations),
89+
...autoloadedIntegrations,
90+
];
8491

8592
if (options.dsn === undefined && process.env.SENTRY_DSN) {
8693
options.dsn = process.env.SENTRY_DSN;
@@ -113,7 +120,7 @@ export function init(options: NodeOptions = {}): void {
113120

114121
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
115122
if ((domain as any).active) {
116-
setHubOnCarrier(getMainCarrier(), getCurrentHub());
123+
setHubOnCarrier(carrier, getCurrentHub());
117124
}
118125

119126
initAndBind(NodeClient, options);

packages/node/test/index.test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { SDK_VERSION } from '@sentry/core';
1+
import { initAndBind, SDK_VERSION } from '@sentry/core';
2+
import { getMainCarrier } from '@sentry/hub';
3+
import { Integration } from '@sentry/types';
24
import * as domain from 'domain';
35

46
import {
@@ -15,6 +17,14 @@ import {
1517
} from '../src';
1618
import { NodeBackend } from '../src/backend';
1719

20+
jest.mock('@sentry/core', () => {
21+
const original = jest.requireActual('@sentry/core');
22+
return {
23+
...original,
24+
initAndBind: jest.fn().mockImplementation(original.initAndBind),
25+
};
26+
});
27+
1828
const dsn = 'https://[email protected]/4291';
1929

2030
// eslint-disable-next-line no-var
@@ -26,6 +36,7 @@ describe('SentryNode', () => {
2636
});
2737

2838
beforeEach(() => {
39+
jest.clearAllMocks();
2940
getCurrentHub().pushScope();
3041
});
3142

@@ -270,7 +281,32 @@ describe('SentryNode', () => {
270281
});
271282
});
272283

284+
function withAutoloadedIntegrations(integrations: Integration[], callback: () => void) {
285+
const carrier = getMainCarrier();
286+
carrier.__SENTRY__!.integrations = integrations;
287+
callback();
288+
carrier.__SENTRY__!.integrations = undefined;
289+
delete carrier.__SENTRY__!.integrations;
290+
}
291+
292+
/** JSDoc */
293+
class MockIntegration implements Integration {
294+
public name: string;
295+
296+
public constructor(name: string) {
297+
this.name = name;
298+
}
299+
300+
public setupOnce(): void {
301+
// noop
302+
}
303+
}
304+
273305
describe('SentryNode initialization', () => {
306+
beforeEach(() => {
307+
jest.clearAllMocks();
308+
});
309+
274310
test('global.SENTRY_RELEASE is used to set release on initialization if available', () => {
275311
global.SENTRY_RELEASE = { id: 'foobar' };
276312
init({ dsn });
@@ -333,4 +369,36 @@ describe('SentryNode initialization', () => {
333369
expect(sdkData.version).toEqual(SDK_VERSION);
334370
});
335371
});
372+
373+
describe('autoloaded integrations', () => {
374+
it('should attach single integration to default integrations', () => {
375+
withAutoloadedIntegrations([new MockIntegration('foo')], () => {
376+
init({
377+
defaultIntegrations: [new MockIntegration('bar')],
378+
});
379+
const integrations = (initAndBind as jest.Mock).mock.calls[0][1].defaultIntegrations;
380+
expect(integrations.map(i => i.name)).toEqual(['bar', 'foo']);
381+
});
382+
});
383+
384+
it('should attach multiple integrations to default integrations', () => {
385+
withAutoloadedIntegrations([new MockIntegration('foo'), new MockIntegration('bar')], () => {
386+
init({
387+
defaultIntegrations: [new MockIntegration('baz'), new MockIntegration('qux')],
388+
});
389+
const integrations = (initAndBind as jest.Mock).mock.calls[0][1].defaultIntegrations;
390+
expect(integrations.map(i => i.name)).toEqual(['baz', 'qux', 'foo', 'bar']);
391+
});
392+
});
393+
394+
it('should ignore autoloaded integrations when defaultIntegrations:false', () => {
395+
withAutoloadedIntegrations([new MockIntegration('foo')], () => {
396+
init({
397+
defaultIntegrations: false,
398+
});
399+
const integrations = (initAndBind as jest.Mock).mock.calls[0][1].defaultIntegrations;
400+
expect(integrations).toEqual([]);
401+
});
402+
});
403+
});
336404
});

packages/tracing/src/hubextensions.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { getMainCarrier, Hub } from '@sentry/hub';
22
import {
33
CustomSamplingContext,
4+
Integration,
5+
IntegrationClass,
46
Options,
57
SamplingContext,
68
TransactionContext,
79
TransactionSamplingMethod,
810
} from '@sentry/types';
9-
import { logger } from '@sentry/utils';
11+
import { dynamicRequire, isNodeEnv, loadModule, logger } from '@sentry/utils';
1012

1113
import { registerErrorInstrumentation } from './errors';
1214
import { IdleTransaction } from './idletransaction';
@@ -207,14 +209,61 @@ export function startIdleTransaction(
207209
*/
208210
export function _addTracingExtensions(): void {
209211
const carrier = getMainCarrier();
210-
if (carrier.__SENTRY__) {
211-
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};
212-
if (!carrier.__SENTRY__.extensions.startTransaction) {
213-
carrier.__SENTRY__.extensions.startTransaction = _startTransaction;
214-
}
215-
if (!carrier.__SENTRY__.extensions.traceHeaders) {
216-
carrier.__SENTRY__.extensions.traceHeaders = traceHeaders;
217-
}
212+
if (!carrier.__SENTRY__) {
213+
return;
214+
}
215+
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};
216+
if (!carrier.__SENTRY__.extensions.startTransaction) {
217+
carrier.__SENTRY__.extensions.startTransaction = _startTransaction;
218+
}
219+
if (!carrier.__SENTRY__.extensions.traceHeaders) {
220+
carrier.__SENTRY__.extensions.traceHeaders = traceHeaders;
221+
}
222+
}
223+
224+
/**
225+
* @private
226+
*/
227+
function _autoloadDatabaseIntegrations(): void {
228+
const carrier = getMainCarrier();
229+
if (!carrier.__SENTRY__) {
230+
return;
231+
}
232+
233+
const packageToIntegrationMapping: Record<string, () => Integration> = {
234+
mongodb() {
235+
const integration = dynamicRequire(module, './integrations/mongo') as { Mongo: IntegrationClass<Integration> };
236+
return new integration.Mongo();
237+
},
238+
mongoose() {
239+
const integration = dynamicRequire(module, './integrations/mongo') as { Mongo: IntegrationClass<Integration> };
240+
return new integration.Mongo({ mongoose: true });
241+
},
242+
mysql() {
243+
const integration = dynamicRequire(module, './integrations/mysql') as { Mysql: IntegrationClass<Integration> };
244+
return new integration.Mysql();
245+
},
246+
pg() {
247+
const integration = dynamicRequire(module, './integrations/postgres') as {
248+
Postgres: IntegrationClass<Integration>;
249+
};
250+
return new integration.Postgres();
251+
},
252+
};
253+
254+
const mappedPackages = Object.keys(packageToIntegrationMapping)
255+
.filter(moduleName => !!loadModule(moduleName))
256+
.map(pkg => {
257+
try {
258+
return packageToIntegrationMapping[pkg]();
259+
} catch (e) {
260+
return undefined;
261+
}
262+
})
263+
.filter(p => p) as Integration[];
264+
265+
if (mappedPackages.length > 0) {
266+
carrier.__SENTRY__.integrations = [...(carrier.__SENTRY__.integrations || []), ...mappedPackages];
218267
}
219268
}
220269

@@ -224,6 +273,11 @@ export function _addTracingExtensions(): void {
224273
export function addExtensionMethods(): void {
225274
_addTracingExtensions();
226275

276+
// Detect and automatically load specified integrations.
277+
if (isNodeEnv()) {
278+
_autoloadDatabaseIntegrations();
279+
}
280+
227281
// If an error happens globally, we should make sure transaction status is set to error.
228282
registerErrorInstrumentation();
229283
}

0 commit comments

Comments
 (0)