Skip to content

Commit b95bee2

Browse files
authored
feat: Node integrations (#1357)
1 parent ee57888 commit b95bee2

File tree

10 files changed

+352
-3
lines changed

10 files changed

+352
-3
lines changed

packages/node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@sentry/core": "0.5.4",
1919
"@sentry/shim": "0.5.4",
2020
"@sentry/types": "0.5.4",
21+
"@sentry/utils": "0.5.4",
2122
"raven": "^2.6.0"
2223
},
2324
"devDependencies": {

packages/node/src/backend.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface NodeOptions extends Options {
4646
/** The Sentry Node SDK Backend. */
4747
export class NodeBackend implements Backend {
4848
/** Creates a new Node backend instance. */
49-
public constructor(private readonly options: NodeOptions) {}
49+
public constructor(private readonly options: NodeOptions = {}) {}
5050

5151
/**
5252
* @inheritDoc
@@ -62,8 +62,16 @@ export class NodeBackend implements Backend {
6262
);
6363
}
6464

65+
Raven.config(dsn, this.options);
66+
67+
// We need to leave it here for now, as we are skipping `install` call,
68+
// due to integrations migration
69+
// TODO: Remove it once we fully migrate our code
6570
const { onFatalError } = this.options;
66-
Raven.config(dsn, this.options).install(onFatalError);
71+
if (onFatalError) {
72+
Raven.onFatalError = onFatalError;
73+
}
74+
Raven.installed = true;
6775

6876
// Hook into Raven's breadcrumb mechanism. This allows us to intercept both
6977
// breadcrumbs created internally by Raven and pass them to the Client
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { addBreadcrumb } from '@sentry/shim';
2+
import { Integration, Severity } from '@sentry/types';
3+
import { fill } from '@sentry/utils';
4+
import { format } from 'util';
5+
6+
/**
7+
* Wrapper function for internal _load calls within `require`
8+
*/
9+
function loadWrapper(nativeModule: any): any {
10+
// We need to use some functional-style currying to pass values around
11+
// as we cannot rely on `bind`, because this has to preserve correct
12+
// context for native calls
13+
return function(originalLoad: () => any): any {
14+
return function(moduleId: string): any {
15+
const originalModule = originalLoad.apply(nativeModule, arguments);
16+
17+
if (moduleId !== 'console') {
18+
return originalModule;
19+
}
20+
21+
['debug', 'info', 'warn', 'error', 'log'].forEach(
22+
consoleWrapper(originalModule),
23+
);
24+
25+
return originalModule;
26+
};
27+
};
28+
}
29+
30+
/**
31+
* Wrapper function that'll be used for every console level
32+
*/
33+
function consoleWrapper(originalModule: any): any {
34+
return function(level: string): any {
35+
if (!(level in originalModule)) {
36+
return;
37+
}
38+
39+
fill(originalModule, level, function(originalConsoleLevel: () => any): any {
40+
let sentryLevel: Severity;
41+
42+
switch (level) {
43+
case 'debug':
44+
sentryLevel = Severity.Debug;
45+
break;
46+
case 'error':
47+
sentryLevel = Severity.Error;
48+
break;
49+
case 'info':
50+
sentryLevel = Severity.Info;
51+
break;
52+
case 'warn':
53+
sentryLevel = Severity.Warning;
54+
break;
55+
default:
56+
sentryLevel = Severity.Log;
57+
}
58+
59+
return function(): any {
60+
addBreadcrumb({
61+
category: 'console',
62+
level: sentryLevel,
63+
message: format.apply(undefined, arguments),
64+
});
65+
66+
originalConsoleLevel.apply(originalModule, arguments);
67+
};
68+
});
69+
};
70+
}
71+
72+
/** Console module integration */
73+
export class Console implements Integration {
74+
/**
75+
* @inheritDoc
76+
*/
77+
public name: string = 'Console';
78+
/**
79+
* @inheritDoc
80+
*/
81+
public install(): void {
82+
const nativeModule = require('module');
83+
fill(nativeModule, '_load', loadWrapper(nativeModule));
84+
// special case: since console is built-in and app-level code won't require() it, do that here
85+
require('console');
86+
}
87+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { addBreadcrumb, getCurrentClient } from '@sentry/shim';
2+
import { Integration } from '@sentry/types';
3+
import { fill } from '@sentry/utils';
4+
import { ClientRequest, ClientRequestArgs, ServerResponse } from 'http';
5+
import { inherits } from 'util';
6+
7+
let lastResponse: ServerResponse | undefined;
8+
9+
/**
10+
* Request interface which can carry around unified url
11+
* independently of used framework
12+
*/
13+
interface SentryRequest extends Request {
14+
__ravenBreadcrumbUrl?: string;
15+
}
16+
17+
/**
18+
* Function that can combine together a url that'll be used for our breadcrumbs.
19+
*
20+
* @param options url that should be returned or an object containing it's parts.
21+
* @returns constructed url
22+
*/
23+
function createBreadcrumbUrl(options: string | ClientRequestArgs): string {
24+
// We could just always reconstruct this from this.agent, this._headers, this.path, etc
25+
// but certain other http-instrumenting libraries (like nock, which we use for tests) fail to
26+
// maintain the guarantee that after calling origClientRequest, those fields will be populated
27+
if (typeof options === 'string') {
28+
return options;
29+
} else {
30+
const protocol = options.protocol || '';
31+
const hostname = options.hostname || options.host || '';
32+
// Don't log standard :80 (http) and :443 (https) ports to reduce the noise
33+
const port =
34+
!options.port || options.port === 80 || options.port === 443
35+
? ''
36+
: `:${options.port}`;
37+
const path = options.path || '/';
38+
return `${protocol}//${hostname}${port}${path}`;
39+
}
40+
}
41+
42+
/**
43+
* Wrapper function for internal _load calls within `require`
44+
*/
45+
function loadWrapper(nativeModule: any): any {
46+
// We need to use some functional-style currying to pass values around
47+
// as we cannot rely on `bind`, because this has to preserve correct
48+
// context for native calls
49+
return function(originalLoad: () => any): any {
50+
return function(this: SentryRequest, moduleId: string): any {
51+
const originalModule = originalLoad.apply(nativeModule, arguments);
52+
53+
if (moduleId !== 'http') {
54+
return originalModule;
55+
}
56+
57+
const origClientRequest = originalModule.ClientRequest;
58+
const clientRequest = function(
59+
this: SentryRequest,
60+
options: ClientRequestArgs | string,
61+
callback: () => void,
62+
): any {
63+
// Note: this won't capture a breadcrumb if a response never comes
64+
// It would be useful to know if that was the case, though, so
65+
// todo: revisit to see if we can capture sth indicating response never came
66+
// possibility: capture one breadcrumb for "req sent" and one for "res recvd"
67+
// seems excessive but solves the problem and *is* strictly more information
68+
// could be useful for weird response sequencing bug scenarios
69+
70+
origClientRequest.call(this, options, callback);
71+
this.__ravenBreadcrumbUrl = createBreadcrumbUrl(options);
72+
};
73+
74+
inherits(clientRequest, origClientRequest);
75+
76+
fill(clientRequest.prototype, 'emit', emitWrapper);
77+
78+
fill(originalModule, 'ClientRequest', function(): any {
79+
return clientRequest;
80+
});
81+
82+
// http.request orig refs module-internal ClientRequest, not exported one, so
83+
// it still points at orig ClientRequest after our monkeypatch; these reimpls
84+
// just get that reference updated to use our new ClientRequest
85+
fill(originalModule, 'request', function(): any {
86+
return function(options: ClientRequestArgs, callback: () => void): any {
87+
return new originalModule.ClientRequest(
88+
options,
89+
callback,
90+
) as ClientRequest;
91+
};
92+
});
93+
94+
fill(originalModule, 'get', function(): any {
95+
return function(options: ClientRequestArgs, callback: () => void): any {
96+
const req = originalModule.request(options, callback);
97+
req.end();
98+
return req;
99+
};
100+
});
101+
102+
return originalModule;
103+
};
104+
};
105+
}
106+
107+
/**
108+
* Wrapper function for request's `emit` calls
109+
*/
110+
function emitWrapper(
111+
origEmit: EventListener,
112+
): (event: string, response: ServerResponse) => EventListener {
113+
return function(
114+
this: SentryRequest,
115+
event: string,
116+
response: ServerResponse,
117+
): any {
118+
// I'm not sure why but Node.js (at least in v8.X)
119+
// is emitting all events twice :|
120+
if (lastResponse === undefined || lastResponse !== response) {
121+
lastResponse = response;
122+
} else {
123+
return origEmit.apply(this, arguments);
124+
}
125+
126+
const DSN = getCurrentClient().getDSN();
127+
128+
const isInterestingEvent = event === 'response' || event === 'error';
129+
const isNotSentryRequest =
130+
DSN &&
131+
this.__ravenBreadcrumbUrl &&
132+
!this.__ravenBreadcrumbUrl.includes(DSN.host);
133+
134+
if (isInterestingEvent && isNotSentryRequest) {
135+
addBreadcrumb({
136+
category: 'http',
137+
data: {
138+
method: this.method,
139+
status_code: response.statusCode,
140+
141+
url: this.__ravenBreadcrumbUrl,
142+
},
143+
type: 'http',
144+
});
145+
}
146+
147+
return origEmit.apply(this, arguments);
148+
};
149+
}
150+
151+
/** http module integration */
152+
export class Http implements Integration {
153+
/**
154+
* @inheritDoc
155+
*/
156+
public name: string = 'Console';
157+
/**
158+
* @inheritDoc
159+
*/
160+
public install(): void {
161+
const nativeModule = require('module');
162+
fill(nativeModule, '_load', loadWrapper(nativeModule));
163+
// observation: when the https module does its own require('http'), it *does not* hit our hooked require to instrument http on the fly
164+
// but if we've previously instrumented http, https *does* get our already-instrumented version
165+
// this is because raven's transports are required before this instrumentation takes place, which loads https (and http)
166+
// so module cache will have uninstrumented http; proactively loading it here ensures instrumented version is in module cache
167+
// alternatively we could refactor to load our transports later, but this is easier and doesn't have much drawback
168+
require('http');
169+
}
170+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { Console } from './console';
2+
export { Http } from './http';
3+
export { OnUncaughtException } from './onuncaughtexception';
4+
export { OnUnhandledRejection } from './onunhandledrejection';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Integration } from '@sentry/types';
2+
import { Raven } from '../raven';
3+
4+
/** Global Promise Rejection handler */
5+
export class OnUncaughtException implements Integration {
6+
/**
7+
* @inheritDoc
8+
*/
9+
public name: string = 'OnUncaughtException';
10+
/**
11+
* @inheritDoc
12+
*/
13+
public install(): void {
14+
global.process.on(
15+
'uncaughtException',
16+
Raven.uncaughtErrorHandler.bind(Raven),
17+
);
18+
}
19+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { captureException, configureScope, withScope } from '@sentry/shim';
2+
import { Integration } from '@sentry/types';
3+
4+
/** Global Promise Rejection handler */
5+
export class OnUnhandledRejection implements Integration {
6+
/**
7+
* @inheritDoc
8+
*/
9+
public name: string = 'OnUnhandledRejection';
10+
/**
11+
* @inheritDoc
12+
*/
13+
public install(): void {
14+
global.process.on('unhandledRejection', (reason, promise: any = {}) => {
15+
const context = (promise.domain && promise.domain.sentryContext) || {};
16+
withScope(() => {
17+
configureScope(scope => {
18+
// Preserve backwards compatibility with raven-node for now
19+
if (context.user) {
20+
scope.setUser(context.user);
21+
}
22+
if (context.tags) {
23+
Object.keys(context.tags).forEach(key => {
24+
scope.setTag(key, context.tags[key]);
25+
});
26+
}
27+
if (context.extra) {
28+
Object.keys(context.extra).forEach(key => {
29+
scope.setExtra(key, context.extra[key]);
30+
});
31+
}
32+
scope.setExtra('unhandledPromiseRejection', true);
33+
});
34+
captureException(reason);
35+
});
36+
});
37+
}
38+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": ["../../tslint.json"],
3+
"rules": {
4+
"no-unsafe-any": false,
5+
"only-arrow-functions": false
6+
}
7+
}

packages/node/src/raven.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export interface RavenInternal {
2828
send: SendMethod;
2929
transport: RavenTransport;
3030
version: string;
31+
// TODO: Remove once integrations are ported
32+
onFatalError(error: Error): void;
33+
installed: boolean;
34+
uncaughtErrorHandler(): void;
3135
}
3236

3337
/** Casted raven instance with access to internal functions. */

0 commit comments

Comments
 (0)