Skip to content

Commit 4f92ff0

Browse files
authored
ref(node-experimental): Copy transport & client to node-experimental (#10720)
One thing less to depend on from node! The only thing missing to be moved from node then is handlers & non-performance integrations - getting there!
1 parent c970772 commit 4f92ff0

27 files changed

+2287
-125
lines changed

packages/core/src/sdk.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Client, ClientOptions } from '@sentry/types';
1+
import type { Client, ClientOptions, Hub as HubInterface } from '@sentry/types';
22
import { consoleSandbox, logger } from '@sentry/utils';
33
import { getCurrentScope } from './currentScopes';
44

@@ -43,10 +43,19 @@ export function initAndBind<F extends Client, O extends ClientOptions>(
4343
* Make the given client the current client.
4444
*/
4545
export function setCurrentClient(client: Client): void {
46+
getCurrentScope().setClient(client);
47+
48+
// is there a hub too?
4649
// eslint-disable-next-line deprecation/deprecation
47-
const hub = getCurrentHub() as Hub;
50+
const hub = getCurrentHub();
51+
if (isHubClass(hub)) {
52+
// eslint-disable-next-line deprecation/deprecation
53+
const top = hub.getStackTop();
54+
top.client = client;
55+
}
56+
}
57+
58+
function isHubClass(hub: HubInterface): hub is Hub {
4859
// eslint-disable-next-line deprecation/deprecation
49-
const top = hub.getStackTop();
50-
top.client = client;
51-
top.scope.setClient(client);
60+
return !!(hub as Hub).getStackTop;
5261
}

packages/node-experimental/.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ module.exports = {
55
extends: ['../../.eslintrc.js'],
66
rules: {
77
'@sentry-internal/sdk/no-optional-chaining': 'off',
8+
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
9+
'@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
10+
'@sentry-internal/sdk/no-class-field-initializers': 'off',
811
},
912
};

packages/node-experimental/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"@sentry/node": "7.100.0",
5454
"@sentry/opentelemetry": "7.100.0",
5555
"@sentry/types": "7.100.0",
56-
"@sentry/utils": "7.100.0"
56+
"@sentry/utils": "7.100.0",
57+
"@types/node": "14.18.63"
5758
},
5859
"optionalDependencies": {
5960
"opentelemetry-instrumentation-fetch-node": "1.1.0"

packages/node-experimental/src/integrations/http.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';
1+
import type { ClientRequest, ServerResponse } from 'http';
22
import type { Span } from '@opentelemetry/api';
33
import { SpanKind } from '@opentelemetry/api';
44
import { registerInstrumentations } from '@opentelemetry/instrumentation';
@@ -9,6 +9,7 @@ import { _INTERNAL, getClient, getSpanKind, setSpanMetadata } from '@sentry/open
99
import type { IntegrationFn } from '@sentry/types';
1010

1111
import { setIsolationScope } from '../sdk/scope';
12+
import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module';
1213
import { addOriginToSpan } from '../utils/addOriginToSpan';
1314
import { getRequestUrl } from '../utils/getRequestUrl';
1415

@@ -107,16 +108,16 @@ const _httpIntegration = ((options: HttpOptions = {}) => {
107108
export const httpIntegration = defineIntegration(_httpIntegration);
108109

109110
/** Update the span with data we need. */
110-
function _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void {
111+
function _updateSpan(span: Span, request: ClientRequest | HTTPModuleRequestIncomingMessage): void {
111112
addOriginToSpan(span, 'auto.http.otel.http');
112113

113114
if (getSpanKind(span) === SpanKind.SERVER) {
114-
setSpanMetadata(span, { request });
115+
setSpanMetadata(span, { request: request as HTTPModuleRequestIncomingMessage });
115116
}
116117
}
117118

118119
/** Add a breadcrumb for outgoing requests. */
119-
function _addRequestBreadcrumb(span: Span, response: IncomingMessage | ServerResponse): void {
120+
function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse): void {
120121
if (getSpanKind(span) !== SpanKind.CLIENT) {
121122
return;
122123
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7
3+
* With the following licence:
4+
*
5+
* (The MIT License)
6+
*
7+
* Copyright (c) 2013 Nathan Rajlich <[email protected]>*
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining
10+
* a copy of this software and associated documentation files (the
11+
* 'Software'), to deal in the Software without restriction, including
12+
* without limitation the rights to use, copy, modify, merge, publish,
13+
* distribute, sublicense, and/or sell copies of the Software, and to
14+
* permit persons to whom the Software is furnished to do so, subject to
15+
* the following conditions:*
16+
*
17+
* The above copyright notice and this permission notice shall be
18+
* included in all copies or substantial portions of the Software.*
19+
*
20+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27+
*/
28+
29+
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
30+
/* eslint-disable @typescript-eslint/member-ordering */
31+
/* eslint-disable jsdoc/require-jsdoc */
32+
import * as http from 'http';
33+
import type * as net from 'net';
34+
import type { Duplex } from 'stream';
35+
import type * as tls from 'tls';
36+
37+
export * from './helpers';
38+
39+
interface HttpConnectOpts extends net.TcpNetConnectOpts {
40+
secureEndpoint: false;
41+
protocol?: string;
42+
}
43+
44+
interface HttpsConnectOpts extends tls.ConnectionOptions {
45+
secureEndpoint: true;
46+
protocol?: string;
47+
port: number;
48+
}
49+
50+
export type AgentConnectOpts = HttpConnectOpts | HttpsConnectOpts;
51+
52+
const INTERNAL = Symbol('AgentBaseInternalState');
53+
54+
interface InternalState {
55+
defaultPort?: number;
56+
protocol?: string;
57+
currentSocket?: Duplex;
58+
}
59+
60+
export abstract class Agent extends http.Agent {
61+
private [INTERNAL]: InternalState;
62+
63+
// Set by `http.Agent` - missing from `@types/node`
64+
options!: Partial<net.TcpNetConnectOpts & tls.ConnectionOptions>;
65+
keepAlive!: boolean;
66+
67+
constructor(opts?: http.AgentOptions) {
68+
super(opts);
69+
this[INTERNAL] = {};
70+
}
71+
72+
abstract connect(
73+
req: http.ClientRequest,
74+
options: AgentConnectOpts,
75+
): Promise<Duplex | http.Agent> | Duplex | http.Agent;
76+
77+
/**
78+
* Determine whether this is an `http` or `https` request.
79+
*/
80+
isSecureEndpoint(options?: AgentConnectOpts): boolean {
81+
if (options) {
82+
// First check the `secureEndpoint` property explicitly, since this
83+
// means that a parent `Agent` is "passing through" to this instance.
84+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
85+
if (typeof (options as any).secureEndpoint === 'boolean') {
86+
return options.secureEndpoint;
87+
}
88+
89+
// If no explicit `secure` endpoint, check if `protocol` property is
90+
// set. This will usually be the case since using a full string URL
91+
// or `URL` instance should be the most common usage.
92+
if (typeof options.protocol === 'string') {
93+
return options.protocol === 'https:';
94+
}
95+
}
96+
97+
// Finally, if no `protocol` property was set, then fall back to
98+
// checking the stack trace of the current call stack, and try to
99+
// detect the "https" module.
100+
const { stack } = new Error();
101+
if (typeof stack !== 'string') return false;
102+
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
103+
}
104+
105+
createSocket(req: http.ClientRequest, options: AgentConnectOpts, cb: (err: Error | null, s?: Duplex) => void): void {
106+
const connectOpts = {
107+
...options,
108+
secureEndpoint: this.isSecureEndpoint(options),
109+
};
110+
Promise.resolve()
111+
.then(() => this.connect(req, connectOpts))
112+
.then(socket => {
113+
if (socket instanceof http.Agent) {
114+
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
115+
return socket.addRequest(req, connectOpts);
116+
}
117+
this[INTERNAL].currentSocket = socket;
118+
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
119+
super.createSocket(req, options, cb);
120+
}, cb);
121+
}
122+
123+
createConnection(): Duplex {
124+
const socket = this[INTERNAL].currentSocket;
125+
this[INTERNAL].currentSocket = undefined;
126+
if (!socket) {
127+
throw new Error('No socket was returned in the `connect()` function');
128+
}
129+
return socket;
130+
}
131+
132+
get defaultPort(): number {
133+
return this[INTERNAL].defaultPort ?? (this.protocol === 'https:' ? 443 : 80);
134+
}
135+
136+
set defaultPort(v: number) {
137+
if (this[INTERNAL]) {
138+
this[INTERNAL].defaultPort = v;
139+
}
140+
}
141+
142+
get protocol(): string {
143+
return this[INTERNAL].protocol ?? (this.isSecureEndpoint() ? 'https:' : 'http:');
144+
}
145+
146+
set protocol(v: string) {
147+
if (this[INTERNAL]) {
148+
this[INTERNAL].protocol = v;
149+
}
150+
}
151+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7
3+
* With the following licence:
4+
*
5+
* (The MIT License)
6+
*
7+
* Copyright (c) 2013 Nathan Rajlich <[email protected]>*
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining
10+
* a copy of this software and associated documentation files (the
11+
* 'Software'), to deal in the Software without restriction, including
12+
* without limitation the rights to use, copy, modify, merge, publish,
13+
* distribute, sublicense, and/or sell copies of the Software, and to
14+
* permit persons to whom the Software is furnished to do so, subject to
15+
* the following conditions:*
16+
*
17+
* The above copyright notice and this permission notice shall be
18+
* included in all copies or substantial portions of the Software.*
19+
*
20+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27+
*/
28+
29+
/* eslint-disable jsdoc/require-jsdoc */
30+
import * as http from 'http';
31+
import * as https from 'https';
32+
import type { Readable } from 'stream';
33+
// TODO (v8): Remove this when Node < 12 is no longer supported
34+
import type { URL } from 'url';
35+
36+
export type ThenableRequest = http.ClientRequest & {
37+
then: Promise<http.IncomingMessage>['then'];
38+
};
39+
40+
export async function toBuffer(stream: Readable): Promise<Buffer> {
41+
let length = 0;
42+
const chunks: Buffer[] = [];
43+
for await (const chunk of stream) {
44+
length += (chunk as Buffer).length;
45+
chunks.push(chunk);
46+
}
47+
return Buffer.concat(chunks, length);
48+
}
49+
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
export async function json(stream: Readable): Promise<any> {
52+
const buf = await toBuffer(stream);
53+
const str = buf.toString('utf8');
54+
try {
55+
return JSON.parse(str);
56+
} catch (_err: unknown) {
57+
const err = _err as Error;
58+
err.message += ` (input: ${str})`;
59+
throw err;
60+
}
61+
}
62+
63+
export function req(url: string | URL, opts: https.RequestOptions = {}): ThenableRequest {
64+
const href = typeof url === 'string' ? url : url.href;
65+
const req = (href.startsWith('https:') ? https : http).request(url, opts) as ThenableRequest;
66+
const promise = new Promise<http.IncomingMessage>((resolve, reject) => {
67+
req.once('response', resolve).once('error', reject).end() as unknown as ThenableRequest;
68+
});
69+
req.then = promise.then.bind(promise);
70+
return req;
71+
}

0 commit comments

Comments
 (0)