Skip to content

Commit 58b3ddb

Browse files
author
Luca Forstner
authored
feat(node): Add new v7 http/s Transports (#4781)
1 parent 8035b14 commit 58b3ddb

File tree

8 files changed

+949
-13
lines changed

8 files changed

+949
-13
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export {
3535
NewTransport,
3636
TransportMakeRequestResponse,
3737
TransportRequest,
38+
TransportRequestExecutor,
3839
} from './transports/base';
3940
export { SDK_VERSION } from './version';
4041

packages/core/src/transports/base.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,6 @@ export interface BrowserTransportOptions extends BaseTransportOptions {
6767
sendClientReports?: boolean;
6868
}
6969

70-
// TODO: Move into Node transport
71-
export interface NodeTransportOptions extends BaseTransportOptions {
72-
headers?: Record<string, string>;
73-
// Set a HTTP proxy that should be used for outbound requests.
74-
httpProxy?: string;
75-
// Set a HTTPS proxy that should be used for outbound requests.
76-
httpsProxy?: string;
77-
// HTTPS proxy certificates path
78-
caCerts?: string;
79-
}
80-
8170
export interface NewTransport {
8271
send(request: Envelope): PromiseLike<TransportResponse>;
8372
flush(timeout?: number): PromiseLike<boolean>;

packages/node/src/backend.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { BaseBackend } from '@sentry/core';
1+
import { BaseBackend, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails } from '@sentry/core';
22
import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types';
33
import { makeDsn, resolvedSyncPromise } from '@sentry/utils';
44

55
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
6-
import { HTTPSTransport, HTTPTransport } from './transports';
6+
import { HTTPSTransport, HTTPTransport, makeNodeTransport } from './transports';
77
import { NodeOptions } from './types';
88

99
/**
@@ -50,6 +50,17 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
5050
if (this._options.transport) {
5151
return new this._options.transport(transportOptions);
5252
}
53+
54+
const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel);
55+
const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel);
56+
57+
this._newTransport = makeNodeTransport({
58+
url,
59+
headers: transportOptions.headers,
60+
proxy: transportOptions.httpProxy,
61+
caCerts: transportOptions.caCerts,
62+
});
63+
5364
if (dsn.protocol === 'http') {
5465
return new HTTPTransport(transportOptions);
5566
}

packages/node/src/transports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { BaseTransport } from './base';
22
export { HTTPTransport } from './http';
33
export { HTTPSTransport } from './https';
4+
export { makeNodeTransport, NodeTransportOptions } from './new';

packages/node/src/transports/new.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import {
2+
BaseTransportOptions,
3+
createTransport,
4+
NewTransport,
5+
TransportMakeRequestResponse,
6+
TransportRequest,
7+
TransportRequestExecutor,
8+
} from '@sentry/core';
9+
import { eventStatusFromHttpCode } from '@sentry/utils';
10+
import * as http from 'http';
11+
import * as https from 'https';
12+
import { URL } from 'url';
13+
14+
import { HTTPModule } from './base/http-module';
15+
16+
// TODO(v7):
17+
// - Rename this file "transport.ts"
18+
// - Move this file one folder upwards
19+
// - Delete "transports" folder
20+
// OR
21+
// - Split this file up and leave it in the transports folder
22+
23+
export interface NodeTransportOptions extends BaseTransportOptions {
24+
/** Define custom headers */
25+
headers?: Record<string, string>;
26+
/** Set a proxy that should be used for outbound requests. */
27+
proxy?: string;
28+
/** HTTPS proxy CA certificates */
29+
caCerts?: string | Buffer | Array<string | Buffer>;
30+
/** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */
31+
httpModule?: HTTPModule;
32+
}
33+
34+
/**
35+
* Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry.
36+
*/
37+
export function makeNodeTransport(options: NodeTransportOptions): NewTransport {
38+
const urlSegments = new URL(options.url);
39+
const isHttps = urlSegments.protocol === 'https:';
40+
41+
// Proxy prioritization: http => `options.proxy` | `process.env.http_proxy`
42+
// Proxy prioritization: https => `options.proxy` | `process.env.https_proxy` | `process.env.http_proxy`
43+
const proxy = applyNoProxyOption(
44+
urlSegments,
45+
options.proxy || (isHttps ? process.env.https_proxy : undefined) || process.env.http_proxy,
46+
);
47+
48+
const nativeHttpModule = isHttps ? https : http;
49+
50+
// TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node
51+
// versions(>= 8) as they had memory leaks when using it: #2555
52+
const agent = proxy
53+
? (new (require('https-proxy-agent'))(proxy) as http.Agent)
54+
: new nativeHttpModule.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 });
55+
56+
const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent);
57+
return createTransport({ bufferSize: options.bufferSize }, requestExecutor);
58+
}
59+
60+
/**
61+
* Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion.
62+
*
63+
* @param transportUrl The URL the transport intends to send events to.
64+
* @param proxy The client configured proxy.
65+
* @returns A proxy the transport should use.
66+
*/
67+
function applyNoProxyOption(transportUrlSegments: URL, proxy: string | undefined): string | undefined {
68+
const { no_proxy } = process.env;
69+
70+
const urlIsExemptFromProxy =
71+
no_proxy &&
72+
no_proxy
73+
.split(',')
74+
.some(
75+
exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption),
76+
);
77+
78+
if (urlIsExemptFromProxy) {
79+
return undefined;
80+
} else {
81+
return proxy;
82+
}
83+
}
84+
85+
/**
86+
* Creates a RequestExecutor to be used with `createTransport`.
87+
*/
88+
function createRequestExecutor(
89+
options: NodeTransportOptions,
90+
httpModule: HTTPModule,
91+
agent: http.Agent,
92+
): TransportRequestExecutor {
93+
const { hostname, pathname, port, protocol, search } = new URL(options.url);
94+
95+
return function makeRequest(request: TransportRequest): Promise<TransportMakeRequestResponse> {
96+
return new Promise((resolve, reject) => {
97+
const req = httpModule.request(
98+
{
99+
method: 'POST',
100+
agent,
101+
headers: options.headers,
102+
hostname,
103+
path: `${pathname}${search}`,
104+
port,
105+
protocol,
106+
ca: options.caCerts,
107+
},
108+
res => {
109+
res.on('data', () => {
110+
// Drain socket
111+
});
112+
113+
res.on('end', () => {
114+
// Drain socket
115+
});
116+
117+
const statusCode = res.statusCode ?? 500;
118+
const status = eventStatusFromHttpCode(statusCode);
119+
120+
res.setEncoding('utf8');
121+
122+
// "Key-value pairs of header names and values. Header names are lower-cased."
123+
// https://nodejs.org/api/http.html#http_message_headers
124+
const retryAfterHeader = res.headers['retry-after'] ?? null;
125+
const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null;
126+
127+
resolve({
128+
headers: {
129+
'retry-after': retryAfterHeader,
130+
'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader,
131+
},
132+
reason: status,
133+
statusCode: statusCode,
134+
});
135+
},
136+
);
137+
138+
req.on('error', reject);
139+
req.end(request.body);
140+
});
141+
};
142+
}

0 commit comments

Comments
 (0)