Skip to content

Commit bd1e67f

Browse files
committed
fix(openfeature): honor HTTPS proxies for direct exposures
1 parent 6701ac7 commit bd1e67f

10 files changed

Lines changed: 91 additions & 8 deletions

File tree

LICENSE-3rdparty.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,21 @@
4444
"@oxc-project/types","https://github.com/oxc-project/oxc","['MIT']","['Boshen and oxc contributors']"
4545
"@tybys/wasm-util","https://github.com/toyobayashi/wasm-util","['MIT']","['toyobayashi']"
4646
"@types/estree","https://github.com/DefinitelyTyped/DefinitelyTyped","['MIT']","['DefinitelyTyped']"
47+
"agent-base","https://github.com/TooTallNate/proxy-agents","['MIT']","['Nathan Rajlich']"
4748
"argparse","https://github.com/nodeca/argparse","['Python-2.0']","['nodeca']"
4849
"astring","https://github.com/davidbonnet/astring","['MIT']","['David Bonnet']"
4950
"cjs-module-lexer","https://github.com/nodejs/cjs-module-lexer","['MIT']","['Guy Bedford']"
5051
"crypto-randomuuid","npm:crypto-randomuuid","['MIT']","['Stephen Belanger']"
5152
"dc-polyfill","https://github.com/DataDog/dc-polyfill","['MIT']","['Thomas Hunter II']"
5253
"dd-trace","https://github.com/DataDog/dd-trace-js","['(Apache-2.0 OR BSD-3-Clause)']","['Datadog Inc. <info@datadoghq.com>']"
54+
"debug","https://github.com/debug-js/debug","['MIT']","['TJ Holowaychuk', 'Josh Junon']"
5355
"detect-newline","https://github.com/sindresorhus/detect-newline","['MIT']","['Sindre Sorhus']"
5456
"es-module-lexer","https://github.com/guybedford/es-module-lexer","['MIT']","['Guy Bedford']"
5557
"escape-string-regexp","https://github.com/sindresorhus/escape-string-regexp","['MIT']","['Sindre Sorhus']"
5658
"esquery","https://github.com/estools/esquery","['BSD-3-Clause']","['Joel Feenstra']"
5759
"estraverse","https://github.com/estools/estraverse","['BSD-2-Clause']","['estools']"
5860
"fast-fifo","https://github.com/mafintosh/fast-fifo","['MIT']","['Mathias Buus']"
61+
"https-proxy-agent","https://github.com/TooTallNate/proxy-agents","['MIT']","['Nathan Rajlich']"
5962
"import-in-the-middle","https://github.com/nodejs/import-in-the-middle","['Apache-2.0']","['Bryan English']"
6063
"istanbul-lib-coverage","https://github.com/istanbuljs/istanbuljs","['BSD-3-Clause']","['Krishnan Anantheswaran']"
6164
"jest-docblock","https://github.com/jestjs/jest","['MIT']","['jestjs']"
@@ -68,13 +71,15 @@
6871
"lru-cache","https://github.com/isaacs/node-lru-cache","['ISC']","['Isaac Z. Schlueter']"
6972
"meriyah","https://github.com/meriyah/meriyah","['ISC']","['Kenny F.']"
7073
"module-details-from-path","https://github.com/watson/module-details-from-path","['MIT']","['Thomas Watson']"
74+
"ms","https://github.com/vercel/ms","['MIT']","['Vercel, Inc.']"
7175
"mutexify","https://github.com/mafintosh/mutexify","['MIT']","['Mathias Buus']"
7276
"node-addon-api","https://github.com/nodejs/node-addon-api","['MIT']","['nodejs']"
7377
"node-gyp-build","https://github.com/prebuild/node-gyp-build","['MIT']","['Mathias Buus']"
7478
"opentracing","https://github.com/opentracing/opentracing-javascript","['Apache-2.0']","['opentracing']"
7579
"oxc-parser","https://github.com/oxc-project/oxc","['MIT']","['Boshen and oxc contributors']"
7680
"pprof-format","https://github.com/DataDog/pprof-format","['MIT']","['Datadog Inc.']"
7781
"protobufjs","https://github.com/protobufjs/protobuf.js","['BSD-3-Clause']","['Daniel Wirtz']"
82+
"proxy-from-env","https://github.com/Rob--W/proxy-from-env","['MIT']","['Rob Wu']"
7883
"queue-tick","https://github.com/mafintosh/queue-tick","['MIT']","['Mathias Buus']"
7984
"retry","https://github.com/tim-kos/node-retry","['MIT']","['Tim Koschützki']"
8085
"rfdc","https://github.com/davidmarkclements/rfdc","['MIT']","['David Mark Clements']"

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,10 @@
176176
],
177177
"dependencies": {
178178
"dc-polyfill": "^0.1.11",
179+
"https-proxy-agent": "^7.0.6",
179180
"import-in-the-middle": "^3.3.2",
180-
"opentracing": ">=0.14.7"
181+
"opentracing": ">=0.14.7",
182+
"proxy-from-env": "^2.1.0"
181183
},
182184
"optionalDependencies": {
183185
"@datadog/libdatadog": "0.9.4",

packages/dd-trace/src/evp_proxy/discovery.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
const { format } = require('node:url')
44

5+
const { HttpsProxyAgent } = require('https-proxy-agent')
6+
const { getProxyForUrl } = require('proxy-from-env')
7+
58
const { fetchAgentInfo } = require('../agent/info')
69
const { EVP_SUBDOMAIN_HEADER_NAME } = require('./constants')
710

@@ -15,6 +18,7 @@ const discoveries = new WeakMap()
1518
* @property {URL} url - Route base URL
1619
* @property {string} basePath - EVP proxy base path
1720
* @property {object} headers - Route-specific headers
21+
* @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
1822
* @property {EVPRoute} [fallback] - Direct fallback route
1923
*/
2024

@@ -84,16 +88,21 @@ function createDirectEVPRoute (config, intake) {
8488
if (!apiKey || !config.site) return
8589

8690
try {
91+
const url = new URL(format({
92+
protocol: 'https:',
93+
hostname: `${intake}.${config.site}`,
94+
}))
95+
const proxyUrl = getProxyForUrl(url.href)
96+
97+
const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined
8798
return {
8899
mode: 'direct',
89-
url: new URL(format({
90-
protocol: 'https:',
91-
hostname: `${intake}.${config.site}`,
92-
})),
100+
url,
93101
basePath: '',
94102
headers: {
95103
'DD-API-KEY': apiKey,
96104
},
105+
...(agent && { agent }),
97106
}
98107
} catch {}
99108
}

packages/dd-trace/src/exporters/common/request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function request (data, options, callback) {
9393

9494
docker.inject(options.headers)
9595

96-
options.agent = isSecure ? httpsAgent : httpAgent
96+
options.agent ??= isSecure ? httpsAgent : httpAgent
9797

9898
/**
9999
* @param {import('node:http').IncomingMessage} res

packages/dd-trace/src/openfeature/writers/base.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const log = require('../../log')
2222
* @property {URL} url - Route base URL
2323
* @property {string} endpoint - Route endpoint
2424
* @property {object} headers - Route-specific headers
25+
* @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
2526
*/
2627

2728
/**
@@ -269,6 +270,7 @@ class BaseFFEWriter {
269270
url: route.url,
270271
endpoint: route.endpoint,
271272
requestOptions: {
273+
...(route.agent && { agent: route.agent }),
272274
headers: {
273275
...route.headers,
274276
'Content-Type': 'application/json',

packages/dd-trace/src/openfeature/writers/exposures.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ class ExposuresWriter extends BaseFFEWriter {
9494
* url: URL,
9595
* basePath: string,
9696
* headers: object,
97-
* fallback?: {url: URL, basePath: string, headers: object}
97+
* agent?: import('node:https').Agent,
98+
* fallback?: {url: URL, basePath: string, headers: object, agent?: import('node:https').Agent}
9899
* }} [route] - Selected EVP route
99100
* @returns {void}
100101
*/
@@ -107,11 +108,13 @@ class ExposuresWriter extends BaseFFEWriter {
107108
url: route.fallback.url,
108109
endpoint: joinEVPProxyPath(route.fallback.basePath, EXPOSURES_ENDPOINT),
109110
headers: route.fallback.headers,
111+
agent: route.fallback.agent,
110112
}
111113
this._setRoutes({
112114
url: route.url,
113115
endpoint: joinEVPProxyPath(route.basePath, EXPOSURES_ENDPOINT),
114116
headers: route.headers,
117+
agent: route.agent,
115118
}, fallbackRoute)
116119
}
117120

packages/dd-trace/test/exporters/common/request.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,28 @@ describe('request', function () {
111111
})
112112
})
113113

114+
it('preserves a caller-supplied connection agent', (done) => {
115+
const customAgent = new http.Agent()
116+
const sandbox = sinon.createSandbox()
117+
sandbox.spy(http, 'request')
118+
nock('http://test:123').get('/path').reply(200, 'OK')
119+
120+
request(Buffer.from(''), {
121+
agent: customAgent,
122+
protocol: 'http:',
123+
hostname: 'test',
124+
port: 123,
125+
path: '/path',
126+
method: 'GET',
127+
}, (error) => {
128+
const callOptions = http.request.getCall(0).args[0]
129+
sandbox.restore()
130+
customAgent.destroy()
131+
assert.strictEqual(callOptions.agent, customAgent)
132+
done(error)
133+
})
134+
})
135+
114136
it('does not retry when retries are disabled', (done) => {
115137
maxAttempts = 5
116138
const error = Object.assign(new Error('ECONNRESET'), { code: 'ECONNRESET' })

packages/dd-trace/test/openfeature/evp_proxy-discovery.spec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,24 @@ require('../setup/core')
1111
describe('OpenFeature local EVP proxy discovery', () => {
1212
let fetchAgentInfo
1313
let getLocalEVPProxyDiscovery
14+
let getProxyForUrl
15+
let HttpsProxyAgent
1416
let joinEVPProxyPath
1517
let resolveEVPRoute
1618

1719
beforeEach(() => {
1820
fetchAgentInfo = sinon.stub()
21+
getProxyForUrl = sinon.stub().returns('')
22+
HttpsProxyAgent = sinon.stub().callsFake(proxyUrl => ({ proxyUrl }))
1923

2024
;({
2125
getLocalEVPProxyDiscovery,
2226
joinEVPProxyPath,
2327
resolveEVPRoute,
2428
} = proxyquire('../../src/evp_proxy/discovery', {
2529
'../agent/info': { fetchAgentInfo },
30+
'https-proxy-agent': { HttpsProxyAgent },
31+
'proxy-from-env': { getProxyForUrl },
2632
}))
2733
})
2834

@@ -255,6 +261,34 @@ describe('OpenFeature local EVP proxy discovery', () => {
255261
})
256262
}
257263

264+
it('uses the standard HTTPS proxy for direct intake', (done) => {
265+
const config = {
266+
DD_API_KEY: 'test-api-key',
267+
site: 'datadoghq.com',
268+
url: new URL('http://serverless-init:8126'),
269+
}
270+
const proxyUrl = 'http://proxy:8202'
271+
getProxyForUrl.returns(proxyUrl)
272+
fetchAgentInfo.yields(null, { endpoints: [] })
273+
getLocalEVPProxyDiscovery(config).start()
274+
275+
resolveEVPRoute(
276+
config,
277+
['/evp_proxy/v4', '/evp_proxy/v2'],
278+
'event-platform-intake',
279+
(error, route) => {
280+
assert.strictEqual(error, null)
281+
assert.deepStrictEqual(route.agent, { proxyUrl })
282+
sinon.assert.calledOnceWithExactly(
283+
getProxyForUrl,
284+
'https://event-platform-intake.datadoghq.com/'
285+
)
286+
sinon.assert.calledOnceWithExactly(HttpsProxyAgent, proxyUrl)
287+
done()
288+
}
289+
)
290+
})
291+
258292
it('does not create a direct route without an API key', (done) => {
259293
const config = {
260294
site: 'datadoghq.com',

packages/dd-trace/test/openfeature/writers/exposures.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,12 @@ describe('OpenFeature Exposures Writer', () => {
429429

430430
it('should flush events directly to HTTPS intake without the local EVP prefix', () => {
431431
const url = new URL('https://event-platform-intake.datadoghq.com')
432+
const agent = {}
432433
writer.setEnabled(true, {
433434
mode: 'direct',
434435
url,
435436
basePath: '',
437+
agent,
436438
headers: {
437439
'DD-API-KEY': 'test-api-key',
438440
},
@@ -444,6 +446,7 @@ describe('OpenFeature Exposures Writer', () => {
444446
const [, options] = request.getCall(0).args
445447
assert.strictEqual(options.url, url)
446448
assert.strictEqual(options.path, '/api/v2/exposures')
449+
assert.strictEqual(options.agent, agent)
447450
assert.strictEqual(options.headers['DD-API-KEY'], 'test-api-key')
448451
assert.strictEqual(options.headers['X-Datadog-EVP-Subdomain'], undefined)
449452
})
@@ -457,6 +460,7 @@ describe('OpenFeature Exposures Writer', () => {
457460
it(`should switch to direct intake after definitive local ${name}`, async () => {
458461
const localUrl = new URL('http://serverless-init:8126')
459462
const directUrl = new URL('https://event-platform-intake.datadoghq.com')
463+
const directAgent = {}
460464
request.onFirstCall().yieldsAsync(error, null, statusCode)
461465
writer.setEnabled(true, {
462466
mode: 'local',
@@ -469,6 +473,7 @@ describe('OpenFeature Exposures Writer', () => {
469473
mode: 'direct',
470474
url: directUrl,
471475
basePath: '',
476+
agent: directAgent,
472477
headers: {
473478
'DD-API-KEY': 'test-api-key',
474479
},
@@ -484,6 +489,7 @@ describe('OpenFeature Exposures Writer', () => {
484489
assert.strictEqual(request.firstCall.args[1].path, '/evp_proxy/v4/api/v2/exposures')
485490
assert.strictEqual(request.secondCall.args[1].url, directUrl)
486491
assert.strictEqual(request.secondCall.args[1].path, '/api/v2/exposures')
492+
assert.strictEqual(request.secondCall.args[1].agent, directAgent)
487493
assert.strictEqual(request.secondCall.args[1].headers['DD-API-KEY'], 'test-api-key')
488494

489495
writer.append(exposureEvent)

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2588,7 +2588,7 @@ https-proxy-agent@^5.0.1:
25882588
agent-base "6"
25892589
debug "4"
25902590

2591-
https-proxy-agent@^7.0.5:
2591+
https-proxy-agent@^7.0.5, https-proxy-agent@^7.0.6:
25922592
version "7.0.6"
25932593
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
25942594
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==

0 commit comments

Comments
 (0)