diff --git a/packages/node/package.json b/packages/node/package.json index 00d70742cd85..bc330329fc56 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -32,8 +32,7 @@ "@sentry-internal/tracing": "7.93.0", "@sentry/core": "7.93.0", "@sentry/types": "7.93.0", - "@sentry/utils": "7.93.0", - "https-proxy-agent": "^5.0.0" + "@sentry/utils": "7.93.0" }, "devDependencies": { "@types/cookie": "0.5.2", diff --git a/packages/node/src/proxy/base.ts b/packages/node/src/proxy/base.ts new file mode 100644 index 000000000000..e1ef24c3092e --- /dev/null +++ b/packages/node/src/proxy/base.ts @@ -0,0 +1,151 @@ +/** + * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 + * With the following licence: + * + * (The MIT License) + * + * Copyright (c) 2013 Nathan Rajlich * + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions:* + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software.* + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable jsdoc/require-jsdoc */ +import * as http from 'http'; +import type * as net from 'net'; +import type { Duplex } from 'stream'; +import type * as tls from 'tls'; + +export * from './helpers'; + +interface HttpConnectOpts extends net.TcpNetConnectOpts { + secureEndpoint: false; + protocol?: string; +} + +interface HttpsConnectOpts extends tls.ConnectionOptions { + secureEndpoint: true; + protocol?: string; + port: number; +} + +export type AgentConnectOpts = HttpConnectOpts | HttpsConnectOpts; + +const INTERNAL = Symbol('AgentBaseInternalState'); + +interface InternalState { + defaultPort?: number; + protocol?: string; + currentSocket?: Duplex; +} + +export abstract class Agent extends http.Agent { + private [INTERNAL]: InternalState; + + // Set by `http.Agent` - missing from `@types/node` + options!: Partial; + keepAlive!: boolean; + + constructor(opts?: http.AgentOptions) { + super(opts); + this[INTERNAL] = {}; + } + + abstract connect( + req: http.ClientRequest, + options: AgentConnectOpts, + ): Promise | Duplex | http.Agent; + + /** + * Determine whether this is an `http` or `https` request. + */ + isSecureEndpoint(options?: AgentConnectOpts): boolean { + if (options) { + // First check the `secureEndpoint` property explicitly, since this + // means that a parent `Agent` is "passing through" to this instance. + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + if (typeof (options as any).secureEndpoint === 'boolean') { + return options.secureEndpoint; + } + + // If no explicit `secure` endpoint, check if `protocol` property is + // set. This will usually be the case since using a full string URL + // or `URL` instance should be the most common usage. + if (typeof options.protocol === 'string') { + return options.protocol === 'https:'; + } + } + + // Finally, if no `protocol` property was set, then fall back to + // checking the stack trace of the current call stack, and try to + // detect the "https" module. + const { stack } = new Error(); + if (typeof stack !== 'string') return false; + return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1); + } + + createSocket(req: http.ClientRequest, options: AgentConnectOpts, cb: (err: Error | null, s?: Duplex) => void): void { + const connectOpts = { + ...options, + secureEndpoint: this.isSecureEndpoint(options), + }; + Promise.resolve() + .then(() => this.connect(req, connectOpts)) + .then(socket => { + if (socket instanceof http.Agent) { + // @ts-expect-error `addRequest()` isn't defined in `@types/node` + return socket.addRequest(req, connectOpts); + } + this[INTERNAL].currentSocket = socket; + // @ts-expect-error `createSocket()` isn't defined in `@types/node` + super.createSocket(req, options, cb); + }, cb); + } + + createConnection(): Duplex { + const socket = this[INTERNAL].currentSocket; + this[INTERNAL].currentSocket = undefined; + if (!socket) { + throw new Error('No socket was returned in the `connect()` function'); + } + return socket; + } + + get defaultPort(): number { + return this[INTERNAL].defaultPort ?? (this.protocol === 'https:' ? 443 : 80); + } + + set defaultPort(v: number) { + if (this[INTERNAL]) { + this[INTERNAL].defaultPort = v; + } + } + + get protocol(): string { + return this[INTERNAL].protocol ?? (this.isSecureEndpoint() ? 'https:' : 'http:'); + } + + set protocol(v: string) { + if (this[INTERNAL]) { + this[INTERNAL].protocol = v; + } + } +} diff --git a/packages/node/src/proxy/helpers.ts b/packages/node/src/proxy/helpers.ts new file mode 100644 index 000000000000..119ffd9317ce --- /dev/null +++ b/packages/node/src/proxy/helpers.ts @@ -0,0 +1,71 @@ +/** + * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 + * With the following licence: + * + * (The MIT License) + * + * Copyright (c) 2013 Nathan Rajlich * + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions:* + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software.* + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* eslint-disable jsdoc/require-jsdoc */ +import * as http from 'http'; +import * as https from 'https'; +import type { Readable } from 'stream'; +// TODO (v8): Remove this when Node < 12 is no longer supported +import type { URL } from 'url'; + +export type ThenableRequest = http.ClientRequest & { + then: Promise['then']; +}; + +export async function toBuffer(stream: Readable): Promise { + let length = 0; + const chunks: Buffer[] = []; + for await (const chunk of stream) { + length += (chunk as Buffer).length; + chunks.push(chunk); + } + return Buffer.concat(chunks, length); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function json(stream: Readable): Promise { + const buf = await toBuffer(stream); + const str = buf.toString('utf8'); + try { + return JSON.parse(str); + } catch (_err: unknown) { + const err = _err as Error; + err.message += ` (input: ${str})`; + throw err; + } +} + +export function req(url: string | URL, opts: https.RequestOptions = {}): ThenableRequest { + const href = typeof url === 'string' ? url : url.href; + const req = (href.startsWith('https:') ? https : http).request(url, opts) as ThenableRequest; + const promise = new Promise((resolve, reject) => { + req.once('response', resolve).once('error', reject).end() as unknown as ThenableRequest; + }); + req.then = promise.then.bind(promise); + return req; +} diff --git a/packages/node/src/proxy/index.ts b/packages/node/src/proxy/index.ts new file mode 100644 index 000000000000..4129a9f65cd7 --- /dev/null +++ b/packages/node/src/proxy/index.ts @@ -0,0 +1,226 @@ +/** + * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 + * With the following licence: + * + * (The MIT License) + * + * Copyright (c) 2013 Nathan Rajlich * + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions:* + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software.* + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import assert from 'assert'; +import type * as http from 'http'; +import type { OutgoingHttpHeaders } from 'http'; +import * as net from 'net'; +import * as tls from 'tls'; +// TODO (v8): Remove this when Node < 12 is no longer supported +import { URL } from 'url'; +import { logger } from '@sentry/utils'; +import { Agent } from './base'; +import type { AgentConnectOpts } from './base'; +import { parseProxyResponse } from './parse-proxy-response'; + +function debug(...args: unknown[]): void { + logger.log('[https-proxy-agent]', ...args); +} + +type Protocol = T extends `${infer Protocol}:${infer _}` ? Protocol : never; + +type ConnectOptsMap = { + http: Omit; + https: Omit; +}; + +type ConnectOpts = { + [P in keyof ConnectOptsMap]: Protocol extends P ? ConnectOptsMap[P] : never; +}[keyof ConnectOptsMap]; + +export type HttpsProxyAgentOptions = ConnectOpts & + http.AgentOptions & { + headers?: OutgoingHttpHeaders | (() => OutgoingHttpHeaders); + }; + +/** + * The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to + * the specified "HTTP(s) proxy server" in order to proxy HTTPS requests. + * + * Outgoing HTTP requests are first tunneled through the proxy server using the + * `CONNECT` HTTP request method to establish a connection to the proxy server, + * and then the proxy server connects to the destination target and issues the + * HTTP request from the proxy server. + * + * `https:` requests have their socket connection upgraded to TLS once + * the connection to the proxy server has been established. + */ +export class HttpsProxyAgent extends Agent { + static protocols = ['http', 'https'] as const; + + readonly proxy: URL; + proxyHeaders: OutgoingHttpHeaders | (() => OutgoingHttpHeaders); + connectOpts: net.TcpNetConnectOpts & tls.ConnectionOptions; + + constructor(proxy: Uri | URL, opts?: HttpsProxyAgentOptions) { + super(opts); + this.options = {}; + this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy; + this.proxyHeaders = opts?.headers ?? {}; + debug('Creating new HttpsProxyAgent instance: %o', this.proxy.href); + + // Trim off the brackets from IPv6 addresses + const host = (this.proxy.hostname || this.proxy.host).replace(/^\[|\]$/g, ''); + const port = this.proxy.port ? parseInt(this.proxy.port, 10) : this.proxy.protocol === 'https:' ? 443 : 80; + this.connectOpts = { + // Attempt to negotiate http/1.1 for proxy servers that support http/2 + ALPNProtocols: ['http/1.1'], + ...(opts ? omit(opts, 'headers') : null), + host, + port, + }; + } + + /** + * Called when the node-core HTTP client library is creating a + * new HTTP request. + */ + async connect(req: http.ClientRequest, opts: AgentConnectOpts): Promise { + const { proxy } = this; + + if (!opts.host) { + throw new TypeError('No "host" provided'); + } + + // Create a socket connection to the proxy server. + let socket: net.Socket; + if (proxy.protocol === 'https:') { + debug('Creating `tls.Socket`: %o', this.connectOpts); + const servername = this.connectOpts.servername || this.connectOpts.host; + socket = tls.connect({ + ...this.connectOpts, + servername: servername && net.isIP(servername) ? undefined : servername, + }); + } else { + debug('Creating `net.Socket`: %o', this.connectOpts); + socket = net.connect(this.connectOpts); + } + + const headers: OutgoingHttpHeaders = + typeof this.proxyHeaders === 'function' ? this.proxyHeaders() : { ...this.proxyHeaders }; + const host = net.isIPv6(opts.host) ? `[${opts.host}]` : opts.host; + let payload = `CONNECT ${host}:${opts.port} HTTP/1.1\r\n`; + + // Inject the `Proxy-Authorization` header if necessary. + if (proxy.username || proxy.password) { + const auth = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`; + headers['Proxy-Authorization'] = `Basic ${Buffer.from(auth).toString('base64')}`; + } + + headers.Host = `${host}:${opts.port}`; + + if (!headers['Proxy-Connection']) { + headers['Proxy-Connection'] = this.keepAlive ? 'Keep-Alive' : 'close'; + } + for (const name of Object.keys(headers)) { + payload += `${name}: ${headers[name]}\r\n`; + } + + const proxyResponsePromise = parseProxyResponse(socket); + + socket.write(`${payload}\r\n`); + + const { connect, buffered } = await proxyResponsePromise; + req.emit('proxyConnect', connect); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Not EventEmitter in Node types + this.emit('proxyConnect', connect, req); + + if (connect.statusCode === 200) { + req.once('socket', resume); + + if (opts.secureEndpoint) { + // The proxy is connecting to a TLS server, so upgrade + // this socket connection to a TLS connection. + debug('Upgrading socket connection to TLS'); + const servername = opts.servername || opts.host; + return tls.connect({ + ...omit(opts, 'host', 'path', 'port'), + socket, + servername: net.isIP(servername) ? undefined : servername, + }); + } + + return socket; + } + + // Some other status code that's not 200... need to re-play the HTTP + // header "data" events onto the socket once the HTTP machinery is + // attached so that the node core `http` can parse and handle the + // error status code. + + // Close the original socket, and a new "fake" socket is returned + // instead, so that the proxy doesn't get the HTTP request + // written to it (which may contain `Authorization` headers or other + // sensitive data). + // + // See: https://hackerone.com/reports/541502 + socket.destroy(); + + const fakeSocket = new net.Socket({ writable: false }); + fakeSocket.readable = true; + + // Need to wait for the "socket" event to re-play the "data" events. + req.once('socket', (s: net.Socket) => { + debug('Replaying proxy buffer for failed request'); + assert(s.listenerCount('data') > 0); + + // Replay the "buffered" Buffer onto the fake `socket`, since at + // this point the HTTP module machinery has been hooked up for + // the user. + s.push(buffered); + s.push(null); + }); + + return fakeSocket; + } +} + +function resume(socket: net.Socket | tls.TLSSocket): void { + socket.resume(); +} + +function omit( + obj: T, + ...keys: K +): { + [K2 in Exclude]: T[K2]; +} { + const ret = {} as { + [K in keyof typeof obj]: (typeof obj)[K]; + }; + let key: keyof typeof obj; + for (key in obj) { + if (!keys.includes(key)) { + ret[key] = obj[key]; + } + } + return ret; +} diff --git a/packages/node/src/proxy/parse-proxy-response.ts b/packages/node/src/proxy/parse-proxy-response.ts new file mode 100644 index 000000000000..e351945e3c0f --- /dev/null +++ b/packages/node/src/proxy/parse-proxy-response.ts @@ -0,0 +1,137 @@ +/** + * This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7 + * With the following licence: + * + * (The MIT License) + * + * Copyright (c) 2013 Nathan Rajlich * + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * 'Software'), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions:* + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software.* + * + * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable jsdoc/require-jsdoc */ +import type { IncomingHttpHeaders } from 'http'; +import type { Readable } from 'stream'; +import { logger } from '@sentry/utils'; + +function debug(...args: unknown[]): void { + logger.log('[https-proxy-agent:parse-proxy-response]', ...args); +} + +export interface ConnectResponse { + statusCode: number; + statusText: string; + headers: IncomingHttpHeaders; +} + +export function parseProxyResponse(socket: Readable): Promise<{ connect: ConnectResponse; buffered: Buffer }> { + return new Promise((resolve, reject) => { + // we need to buffer any HTTP traffic that happens with the proxy before we get + // the CONNECT response, so that if the response is anything other than an "200" + // response code, then we can re-play the "data" events on the socket once the + // HTTP parser is hooked up... + let buffersLength = 0; + const buffers: Buffer[] = []; + + function read() { + const b = socket.read(); + if (b) ondata(b); + else socket.once('readable', read); + } + + function cleanup() { + socket.removeListener('end', onend); + socket.removeListener('error', onerror); + socket.removeListener('readable', read); + } + + function onend() { + cleanup(); + debug('onend'); + reject(new Error('Proxy connection ended before receiving CONNECT response')); + } + + function onerror(err: Error) { + cleanup(); + debug('onerror %o', err); + reject(err); + } + + function ondata(b: Buffer) { + buffers.push(b); + buffersLength += b.length; + + const buffered = Buffer.concat(buffers, buffersLength); + const endOfHeaders = buffered.indexOf('\r\n\r\n'); + + if (endOfHeaders === -1) { + // keep buffering + debug('have not received end of HTTP headers yet...'); + read(); + return; + } + + const headerParts = buffered.slice(0, endOfHeaders).toString('ascii').split('\r\n'); + const firstLine = headerParts.shift(); + if (!firstLine) { + socket.destroy(); + return reject(new Error('No header received from proxy CONNECT response')); + } + const firstLineParts = firstLine.split(' '); + const statusCode = +firstLineParts[1]; + const statusText = firstLineParts.slice(2).join(' '); + const headers: IncomingHttpHeaders = {}; + for (const header of headerParts) { + if (!header) continue; + const firstColon = header.indexOf(':'); + if (firstColon === -1) { + socket.destroy(); + return reject(new Error(`Invalid header from proxy CONNECT response: "${header}"`)); + } + const key = header.slice(0, firstColon).toLowerCase(); + const value = header.slice(firstColon + 1).trimStart(); + const current = headers[key]; + if (typeof current === 'string') { + headers[key] = [current, value]; + } else if (Array.isArray(current)) { + current.push(value); + } else { + headers[key] = value; + } + } + debug('got proxy server response: %o %o', firstLine, headers); + cleanup(); + resolve({ + connect: { + statusCode, + statusText, + headers, + }, + buffered, + }); + } + + socket.on('error', onerror); + socket.on('end', onend); + + read(); + }); +} diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 56e71156386a..83d8bab5141a 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -12,7 +12,7 @@ import type { TransportRequestExecutor, } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; -import { HttpsProxyAgent } from 'https-proxy-agent'; +import { HttpsProxyAgent } from '../proxy'; import type { HTTPModule } from './http-module'; diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 57b7fe22d19a..a1f4d38b3da5 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -6,8 +6,8 @@ import { getCurrentScope, makeMain, setUser, spanToJSON, startInactiveSpan } fro import { Hub, addTracingExtensions } from '@sentry/core'; import type { TransactionContext } from '@sentry/types'; import { TRACEPARENT_REGEXP, logger } from '@sentry/utils'; -import * as HttpsProxyAgent from 'https-proxy-agent'; import * as nock from 'nock'; +import { HttpsProxyAgent } from '../../src/proxy'; import type { Breadcrumb } from '../../src'; import { NodeClient } from '../../src/client'; @@ -667,9 +667,10 @@ describe('default protocols', () => { const p = captureBreadcrumb(key); let nockProtocol = 'https'; - const proxy = 'http://:3128'; - const agent = HttpsProxyAgent(proxy); + const proxy = 'http://some.url:3128'; + const agent = new HttpsProxyAgent(proxy); + // TODO (v8): No longer needed once we drop Node 8 support if (NODE_VERSION.major < 9) { nockProtocol = 'http'; } diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index 747c6462b1bb..9611e34102f4 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -17,7 +17,7 @@ jest.mock('@sentry/core', () => { }; }); -import * as httpProxyAgent from 'https-proxy-agent'; +import * as httpProxyAgent from '../../src/proxy'; const SUCCESS = 200; const RATE_LIMIT = 429; diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index 2236cf99cb5c..d26a65ddb5c8 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -19,7 +19,7 @@ jest.mock('@sentry/core', () => { }; }); -import * as httpProxyAgent from 'https-proxy-agent'; +import * as httpProxyAgent from '../../src/proxy'; const SUCCESS = 200; const RATE_LIMIT = 429;