Skip to content

Commit 8966bf7

Browse files
committed
feat: Instrument http integration to emit breadcrumbs and/or spans
1 parent 3601e3d commit 8966bf7

File tree

2 files changed

+123
-125
lines changed

2 files changed

+123
-125
lines changed

packages/integrations/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { CaptureConsole } from './captureconsole';
33
export { Debug } from './debug';
44
export { Dedupe } from './dedupe';
55
export { Ember } from './ember';
6+
export { Express } from './express';
67
export { ExtraErrorData } from './extraerrordata';
78
export { ReportingObserver } from './reportingobserver';
89
export { RewriteFrames } from './rewriteframes';
Lines changed: 122 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
1-
import { getCurrentHub } from '@sentry/core';
1+
import { getCurrentHub, Span } from '@sentry/core';
22
import { Integration } from '@sentry/types';
33
import { fill } from '@sentry/utils';
44
import * as http from 'http';
5-
import * as util from 'util';
6-
7-
let lastResponse: http.ServerResponse | undefined;
8-
9-
/**
10-
* Request interface which can carry around unified url
11-
* independently of used framework
12-
*/
13-
interface SentryRequest extends http.IncomingMessage {
14-
__ravenBreadcrumbUrl?: string;
15-
}
5+
import * as https from 'https';
166

177
/** http module integration */
188
export class Http implements Integration {
@@ -25,19 +15,118 @@ export class Http implements Integration {
2515
*/
2616
public static id: string = 'Http';
2717

18+
/**
19+
* @inheritDoc
20+
*/
21+
private readonly _breadcrumbs: boolean;
22+
23+
/**
24+
* @inheritDoc
25+
*/
26+
private readonly _tracing: boolean;
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public constructor(options: { breadcrumbs?: boolean; tracing?: boolean } = {}) {
32+
this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs;
33+
this._tracing = typeof options.tracing === 'undefined' ? false : options.tracing;
34+
}
35+
2836
/**
2937
* @inheritDoc
3038
*/
3139
public setupOnce(): void {
32-
const nativeModule = require('module');
33-
fill(nativeModule, '_load', loadWrapper(nativeModule));
34-
// observation: when the https module does its own require('http'), it *does not* hit our hooked require to instrument http on the fly
35-
// but if we've previously instrumented http, https *does* get our already-instrumented version
36-
// this is because raven's transports are required before this instrumentation takes place, which loads https (and http)
37-
// so module cache will have uninstrumented http; proactively loading it here ensures instrumented version is in module cache
38-
// alternatively we could refactor to load our transports later, but this is easier and doesn't have much drawback
39-
require('http');
40+
// No need to instrument if we don't want to track anything
41+
if (!this._breadcrumbs && !this._tracing) {
42+
return;
43+
}
44+
45+
const handlerWrapper = createHandlerWrapper(this._breadcrumbs, this._tracing);
46+
47+
const httpModule = require('http');
48+
fill(httpModule, 'get', handlerWrapper);
49+
fill(httpModule, 'request', handlerWrapper);
50+
51+
const httpsModule = require('https');
52+
fill(httpsModule, 'get', handlerWrapper);
53+
fill(httpsModule, 'request', handlerWrapper);
54+
}
55+
}
56+
57+
/**
58+
* Wrapper function for internal `request` and `get` calls within `http` and `https` modules
59+
*/
60+
function createHandlerWrapper(
61+
breadcrumbsEnabled: boolean,
62+
tracingEnabled: boolean,
63+
): (originalHandler: () => http.ClientRequest) => (options: string | http.ClientRequestArgs) => http.ClientRequest {
64+
return function handlerWrapper(
65+
originalHandler: () => http.ClientRequest,
66+
): (options: string | http.ClientRequestArgs) => http.ClientRequest {
67+
return function(this: typeof http | typeof https, options: string | http.ClientRequestArgs): http.ClientRequest {
68+
const requestUrl = extractUrl(options);
69+
70+
if (isSentryRequest(requestUrl)) {
71+
return originalHandler.apply(this, arguments);
72+
}
73+
74+
let span: Span;
75+
if (tracingEnabled) {
76+
span = getCurrentHub().startSpan({
77+
description: `${typeof options === 'string' ? 'GET' : options.method}|${requestUrl}`,
78+
op: 'request',
79+
});
80+
}
81+
82+
return originalHandler
83+
.apply(this, arguments)
84+
.once('response', function(this: http.IncomingMessage, res: http.ServerResponse): void {
85+
if (breadcrumbsEnabled) {
86+
addRequestBreadcrumb('response', requestUrl, this, res);
87+
}
88+
if (tracingEnabled) {
89+
span.setSuccess();
90+
span.finish();
91+
}
92+
})
93+
.once('error', function(this: http.IncomingMessage): void {
94+
if (breadcrumbsEnabled) {
95+
addRequestBreadcrumb('error', requestUrl, this);
96+
}
97+
if (tracingEnabled) {
98+
span.setFailure();
99+
span.finish();
100+
}
101+
});
102+
};
103+
};
104+
}
105+
106+
/**
107+
* Captures Breadcrumb based on provided request/response pair
108+
*/
109+
function addRequestBreadcrumb(event: string, url: string, req: http.IncomingMessage, res?: http.ServerResponse): void {
110+
if (!getCurrentHub().getIntegration(Http)) {
111+
return;
40112
}
113+
114+
getCurrentHub().addBreadcrumb(
115+
{
116+
category: 'http',
117+
data: {
118+
method: req.method,
119+
status_code: res && res.statusCode,
120+
url,
121+
},
122+
type: 'http',
123+
},
124+
{
125+
event,
126+
request: req,
127+
response: res,
128+
},
129+
);
41130
}
42131

43132
/**
@@ -46,10 +135,7 @@ export class Http implements Integration {
46135
* @param options url that should be returned or an object containing it's parts.
47136
* @returns constructed url
48137
*/
49-
function createBreadcrumbUrl(options: string | http.ClientRequestArgs): string {
50-
// We could just always reconstruct this from this.agent, this._headers, this.path, etc
51-
// but certain other http-instrumenting libraries (like nock, which we use for tests) fail to
52-
// maintain the guarantee that after calling origClientRequest, those fields will be populated
138+
function extractUrl(options: string | http.ClientRequestArgs): string {
53139
if (typeof options === 'string') {
54140
return options;
55141
}
@@ -62,108 +148,19 @@ function createBreadcrumbUrl(options: string | http.ClientRequestArgs): string {
62148
}
63149

64150
/**
65-
* Wrapper function for internal _load calls within `require`
66-
*/
67-
function loadWrapper(nativeModule: any): any {
68-
// We need to use some functional-style currying to pass values around
69-
// as we cannot rely on `bind`, because this has to preserve correct
70-
// context for native calls
71-
return function(originalLoad: () => any): any {
72-
return function(this: SentryRequest, moduleId: string): any {
73-
const originalModule = originalLoad.apply(nativeModule, arguments);
74-
75-
if (moduleId !== 'http' || originalModule.__sentry__) {
76-
return originalModule;
77-
}
78-
79-
const origClientRequest = originalModule.ClientRequest;
80-
const clientRequest = function(
81-
this: SentryRequest,
82-
options: http.ClientRequestArgs | string,
83-
callback: () => void,
84-
): any {
85-
// Note: this won't capture a breadcrumb if a response never comes
86-
// It would be useful to know if that was the case, though, so
87-
// TODO: revisit to see if we can capture sth indicating response never came
88-
// possibility: capture one breadcrumb for "req sent" and one for "res recvd"
89-
// seems excessive but solves the problem and *is* strictly more information
90-
// could be useful for weird response sequencing bug scenarios
91-
92-
origClientRequest.call(this, options, callback);
93-
this.__ravenBreadcrumbUrl = createBreadcrumbUrl(options);
94-
};
95-
96-
util.inherits(clientRequest, origClientRequest);
97-
98-
fill(clientRequest.prototype, 'emit', emitWrapper);
99-
100-
fill(originalModule, 'ClientRequest', function(): any {
101-
return clientRequest;
102-
});
103-
104-
// http.request orig refs module-internal ClientRequest, not exported one, so
105-
// it still points at orig ClientRequest after our monkeypatch; these reimpls
106-
// just get that reference updated to use our new ClientRequest
107-
fill(originalModule, 'request', function(): any {
108-
return function(options: http.ClientRequestArgs, callback: () => void): any {
109-
return new originalModule.ClientRequest(options, callback) as http.IncomingMessage;
110-
};
111-
});
112-
113-
fill(originalModule, 'get', function(): any {
114-
return function(options: http.ClientRequestArgs, callback: () => void): any {
115-
const req = originalModule.request(options, callback);
116-
req.end();
117-
return req;
118-
};
119-
});
120-
121-
originalModule.__sentry__ = true;
122-
return originalModule;
123-
};
124-
};
125-
}
126-
127-
/**
128-
* Wrapper function for request's `emit` calls
151+
* Checks whether given url points to Sentry server
152+
* @param url url to verify
129153
*/
130-
function emitWrapper(origEmit: EventListener): (event: string, response: http.ServerResponse) => EventListener {
131-
return function(this: SentryRequest, event: string, response: http.ServerResponse): any {
132-
// I'm not sure why but Node.js (at least in v8.X)
133-
// is emitting all events twice :|
134-
if (lastResponse === undefined || lastResponse !== response) {
135-
lastResponse = response;
136-
} else {
137-
return origEmit.apply(this, arguments);
138-
}
154+
function isSentryRequest(url: string): boolean {
155+
const client = getCurrentHub().getClient();
156+
if (!url || !client) {
157+
return false;
158+
}
139159

140-
const client = getCurrentHub().getClient();
141-
if (client) {
142-
const dsn = client.getDsn();
143-
144-
const isInterestingEvent = event === 'response' || event === 'error';
145-
const isNotSentryRequest = dsn && this.__ravenBreadcrumbUrl && this.__ravenBreadcrumbUrl.indexOf(dsn.host) === -1;
146-
147-
if (isInterestingEvent && isNotSentryRequest && getCurrentHub().getIntegration(Http)) {
148-
getCurrentHub().addBreadcrumb(
149-
{
150-
category: 'http',
151-
data: {
152-
method: this.method,
153-
status_code: response.statusCode,
154-
url: this.__ravenBreadcrumbUrl,
155-
},
156-
type: 'http',
157-
},
158-
{
159-
event,
160-
request: this,
161-
response,
162-
},
163-
);
164-
}
165-
}
160+
const dsn = client.getDsn();
161+
if (!dsn) {
162+
return false;
163+
}
166164

167-
return origEmit.apply(this, arguments);
168-
};
165+
return url.indexOf(dsn.host) !== -1;
169166
}

0 commit comments

Comments
 (0)