Skip to content

feat(node): Add new v7 http/s Transports #4781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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