Skip to content

Commit

Permalink
feat(node): Add new v7 http/s Transports (#4781)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst authored Mar 29, 2022
1 parent 8035b14 commit 58b3ddb
Show file tree
Hide file tree
Showing 8 changed files with 949 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
NewTransport,
TransportMakeRequestResponse,
TransportRequest,
TransportRequestExecutor,
} from './transports/base';
export { SDK_VERSION } from './version';

Expand Down
11 changes: 0 additions & 11 deletions packages/core/src/transports/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,6 @@ export interface BrowserTransportOptions extends BaseTransportOptions {
sendClientReports?: boolean;
}

// TODO: Move into Node transport
export interface NodeTransportOptions extends BaseTransportOptions {
headers?: Record<string, string>;
// Set a HTTP proxy that should be used for outbound requests.
httpProxy?: string;
// Set a HTTPS proxy that should be used for outbound requests.
httpsProxy?: string;
// HTTPS proxy certificates path
caCerts?: string;
}

export interface NewTransport {
send(request: Envelope): PromiseLike<TransportResponse>;
flush(timeout?: number): PromiseLike<boolean>;
Expand Down
15 changes: 13 additions & 2 deletions packages/node/src/backend.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BaseBackend } from '@sentry/core';
import { BaseBackend, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails } from '@sentry/core';
import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types';
import { makeDsn, resolvedSyncPromise } from '@sentry/utils';

import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
import { HTTPSTransport, HTTPTransport } from './transports';
import { HTTPSTransport, HTTPTransport, makeNodeTransport } from './transports';
import { NodeOptions } from './types';

/**
Expand Down Expand Up @@ -50,6 +50,17 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
if (this._options.transport) {
return new this._options.transport(transportOptions);
}

const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel);
const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel);

this._newTransport = makeNodeTransport({
url,
headers: transportOptions.headers,
proxy: transportOptions.httpProxy,
caCerts: transportOptions.caCerts,
});

if (dsn.protocol === 'http') {
return new HTTPTransport(transportOptions);
}
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/transports/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { BaseTransport } from './base';
export { HTTPTransport } from './http';
export { HTTPSTransport } from './https';
export { makeNodeTransport, NodeTransportOptions } from './new';
142 changes: 142 additions & 0 deletions packages/node/src/transports/new.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
BaseTransportOptions,
createTransport,
NewTransport,
TransportMakeRequestResponse,
TransportRequest,
TransportRequestExecutor,
} from '@sentry/core';
import { eventStatusFromHttpCode } from '@sentry/utils';
import * as http from 'http';
import * as https from 'https';
import { URL } from 'url';

import { HTTPModule } from './base/http-module';

// TODO(v7):
// - Rename this file "transport.ts"
// - Move this file one folder upwards
// - Delete "transports" folder
// OR
// - Split this file up and leave it in the transports folder

export interface NodeTransportOptions extends BaseTransportOptions {
/** Define custom headers */
headers?: Record<string, string>;
/** Set a proxy that should be used for outbound requests. */
proxy?: string;
/** HTTPS proxy CA certificates */
caCerts?: string | Buffer | Array<string | Buffer>;
/** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */
httpModule?: HTTPModule;
}

/**
* Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry.
*/
export function makeNodeTransport(options: NodeTransportOptions): NewTransport {
const urlSegments = new URL(options.url);
const isHttps = urlSegments.protocol === 'https:';

// Proxy prioritization: http => `options.proxy` | `process.env.http_proxy`
// Proxy prioritization: https => `options.proxy` | `process.env.https_proxy` | `process.env.http_proxy`
const proxy = applyNoProxyOption(
urlSegments,
options.proxy || (isHttps ? process.env.https_proxy : undefined) || process.env.http_proxy,
);

const nativeHttpModule = isHttps ? https : http;

// TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node
// versions(>= 8) as they had memory leaks when using it: #2555
const agent = proxy
? (new (require('https-proxy-agent'))(proxy) as http.Agent)
: new nativeHttpModule.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 });

const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent);
return createTransport({ bufferSize: options.bufferSize }, requestExecutor);
}

/**
* Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion.
*
* @param transportUrl The URL the transport intends to send events to.
* @param proxy The client configured proxy.
* @returns A proxy the transport should use.
*/
function applyNoProxyOption(transportUrlSegments: URL, proxy: string | undefined): string | undefined {
const { no_proxy } = process.env;

const urlIsExemptFromProxy =
no_proxy &&
no_proxy
.split(',')
.some(
exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption),
);

if (urlIsExemptFromProxy) {
return undefined;
} else {
return proxy;
}
}

/**
* Creates a RequestExecutor to be used with `createTransport`.
*/
function createRequestExecutor(
options: NodeTransportOptions,
httpModule: HTTPModule,
agent: http.Agent,
): TransportRequestExecutor {
const { hostname, pathname, port, protocol, search } = new URL(options.url);

return function makeRequest(request: TransportRequest): Promise<TransportMakeRequestResponse> {
return new Promise((resolve, reject) => {
const req = httpModule.request(
{
method: 'POST',
agent,
headers: options.headers,
hostname,
path: `${pathname}${search}`,
port,
protocol,
ca: options.caCerts,
},
res => {
res.on('data', () => {
// Drain socket
});

res.on('end', () => {
// Drain socket
});

const statusCode = res.statusCode ?? 500;
const status = eventStatusFromHttpCode(statusCode);

res.setEncoding('utf8');

// "Key-value pairs of header names and values. Header names are lower-cased."
// https://nodejs.org/api/http.html#http_message_headers
const retryAfterHeader = res.headers['retry-after'] ?? null;
const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null;

resolve({
headers: {
'retry-after': retryAfterHeader,
'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader,
},
reason: status,
statusCode: statusCode,
});
},
);

req.on('error', reject);
req.end(request.body);
});
};
}
Loading

0 comments on commit 58b3ddb

Please sign in to comment.