dd-trace 6.11.0 → 6.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/index.d.ts +20 -3
  3. package/package.json +18 -17
  4. package/packages/datadog-instrumentations/src/cucumber.js +4 -4
  5. package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
  6. package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
  7. package/packages/datadog-instrumentations/src/playwright.js +33 -33
  8. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +6 -8
  9. package/packages/datadog-plugin-playwright/src/index.js +4 -0
  10. package/packages/dd-trace/src/appsec/reporter.js +2 -0
  11. package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
  12. package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
  13. package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
  14. package/packages/dd-trace/src/carrier.js +107 -48
  15. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
  16. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
  17. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
  18. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
  19. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
  20. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
  21. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  22. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
  23. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
  24. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
  25. package/packages/dd-trace/src/debugger/constants.js +1 -0
  26. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  27. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
  28. package/packages/dd-trace/src/debugger/index.js +4 -2
  29. package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
  30. package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
  31. package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
  32. package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
  33. package/packages/dd-trace/src/evp_proxy/path.js +22 -0
  34. package/packages/dd-trace/src/exporters/common/request.js +5 -2
  35. package/packages/dd-trace/src/llmobs/experiments/client.js +6 -2
  36. package/packages/dd-trace/src/llmobs/experiments/dataset.js +249 -11
  37. package/packages/dd-trace/src/llmobs/experiments/experiment.js +23 -10
  38. package/packages/dd-trace/src/llmobs/experiments/index.js +18 -6
  39. package/packages/dd-trace/src/llmobs/experiments/noop.js +64 -4
  40. package/packages/dd-trace/src/llmobs/experiments/util.js +60 -4
  41. package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
  42. package/packages/dd-trace/src/openfeature/index.js +7 -4
  43. package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
  44. package/packages/dd-trace/src/openfeature/writers/exposures.js +62 -14
  45. package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
  46. package/packages/dd-trace/src/opentracing/propagation/text_map.js +22 -13
  47. package/packages/dd-trace/src/telemetry/metrics.js +62 -8
  48. package/packages/dd-trace/src/telemetry/send-data.js +1 -0
  49. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  50. package/vendor/dist/https-proxy-agent/LICENSE +22 -0
  51. package/vendor/dist/https-proxy-agent/index.js +4 -0
  52. package/vendor/dist/protobufjs/index.js +1 -1
  53. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  54. package/vendor/dist/proxy-from-env/LICENSE +20 -0
  55. package/vendor/dist/proxy-from-env/index.js +1 -0
@@ -4,4 +4,5 @@ module.exports = {
4
4
  DEBUGGER_DIAGNOSTICS_V1: '/debugger/v1/diagnostics',
5
5
  DEBUGGER_INPUT_V1: '/debugger/v1/input',
6
6
  DEBUGGER_INPUT_V2: '/debugger/v2/input',
7
+ INSPECT_SEGMENT_GLOBAL_PROPERTY: 'debuggerInspectSegment',
7
8
  }
@@ -55,7 +55,7 @@ function compileSegments (segments) {
55
55
  ? `(() => {
56
56
  try {
57
57
  const result = ${compile(json)}
58
- return typeof result === 'string' ? result : $dd_inspect(result, $dd_segmentInspectOptions)
58
+ return typeof result === 'string' ? result : $dd_inspectSegment(result)
59
59
  } catch (e) {
60
60
  return { expr: ${JSON.stringify(dsl)}, message: \`\${e.name}: \${e.message}\` }
61
61
  }
@@ -4,6 +4,7 @@ const { randomUUID } = require('crypto')
4
4
  const { workerData: { probeSamplerBuffer } } = require('worker_threads')
5
5
  const { version } = require('../../../../../package.json')
6
6
  const processTags = require('../../process-tags')
7
+ const { INSPECT_SEGMENT_GLOBAL_PROPERTY } = require('../constants')
7
8
  const {
8
9
  MAX_SAMPLED_PROBES_PER_PAUSE,
9
10
  SAMPLED_PROBE_COUNT_INDEX,
@@ -23,16 +24,8 @@ require('./remote_config')
23
24
 
24
25
  /** @typedef {import('node:inspector').Debugger.EvaluateOnCallFrameReturnType} EvaluateOnCallFrameResult */
25
26
 
26
- const templateExpressionSetupCode = `
27
- const $dd_inspect = global.require('node:util').inspect;
28
- const $dd_segmentInspectOptions = {
29
- depth: 0,
30
- customInspect: false,
31
- maxArrayLength: 3,
32
- maxStringLength: 8 * 1024,
33
- breakLength: Infinity
34
- };
35
- `
27
+ const templateExpressionSetupCode = 'const $dd_inspectSegment = ' +
28
+ `globalThis[Symbol.for('dd-trace')][${JSON.stringify(INSPECT_SEGMENT_GLOBAL_PROPERTY)}];`
36
29
 
37
30
  // Expression to run on a call frame of the paused thread to get its active trace and span id.
38
31
  const getDDTagsExpression = `(() => {
@@ -7,7 +7,7 @@ const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_thr
7
7
  const log = require('../log')
8
8
  const { fetchAgentInfo } = require('../agent/info')
9
9
  const getDebuggerConfig = require('./config')
10
- const { DEBUGGER_DIAGNOSTICS_V1, DEBUGGER_INPUT_V2 } = require('./constants')
10
+ const { DEBUGGER_DIAGNOSTICS_V1, DEBUGGER_INPUT_V2, INSPECT_SEGMENT_GLOBAL_PROPERTY } = require('./constants')
11
11
  const { installProbeSampler, uninstallProbeSampler } = require('./probe_sampler')
12
12
 
13
13
  /**
@@ -64,7 +64,9 @@ function start (config, rcInstance) {
64
64
  const logChannel = new MessageChannel()
65
65
  configChannel = new MessageChannel()
66
66
 
67
- globalThis[Symbol.for('dd-trace')].utilTypes = types
67
+ const debuggerGlobals = globalThis[Symbol.for('dd-trace')]
68
+ debuggerGlobals.utilTypes = types
69
+ debuggerGlobals[INSPECT_SEGMENT_GLOBAL_PROPERTY] = require('./inspect-segment')
68
70
 
69
71
  const probeSamplerBuffer = installProbeSampler()
70
72
 
@@ -0,0 +1,154 @@
1
+ 'use strict'
2
+
3
+ const { inspect, types } = require('node:util')
4
+
5
+ const { NODE_MAJOR } = require('../../../../version')
6
+
7
+ /** @typedef {NonNullable<ReturnType<typeof globalThis.Object.getOwnPropertyDescriptor>>} PropertyDescriptor */
8
+ /** @typedef {Map<unknown, unknown> | Set<unknown>} Collection */
9
+
10
+ const mapEntries = Map.prototype.entries
11
+ const mapSet = Map.prototype.set
12
+ const mapSizeGetter = Object.getOwnPropertyDescriptor(Map.prototype, 'size').get
13
+ const setAdd = Set.prototype.add
14
+ const setSizeGetter = Object.getOwnPropertyDescriptor(Set.prototype, 'size').get
15
+ const setValues = Set.prototype.values
16
+ const mapIteratorNext = Object.getPrototypeOf(mapEntries.call(new Map())).next
17
+ const setIteratorNext = Object.getPrototypeOf(setValues.call(new Set())).next
18
+
19
+ const maxCollectionEntries = 3
20
+ const maxProperties = 5
21
+ const segmentInspectOptions = {
22
+ depth: 0,
23
+ customInspect: false,
24
+ maxArrayLength: maxCollectionEntries,
25
+ maxStringLength: 8 * 1024,
26
+ breakLength: Infinity,
27
+ }
28
+
29
+ module.exports = inspectSegment
30
+
31
+ /**
32
+ * Inspect a dynamic-instrumentation template value without invoking user code.
33
+ * Unlike collections, `util.inspect` has no option for limiting the number of object properties, so this function
34
+ * truncates objects before inspecting them.
35
+ *
36
+ * @param {unknown} value
37
+ * @returns {string}
38
+ */
39
+ function inspectSegment (value) {
40
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
41
+ return inspect(value, segmentInspectOptions)
42
+ }
43
+ if (types.isProxy(value)) return '[Proxy]'
44
+ if (types.isMap(value)) return inspectCollection(value, true)
45
+ if (types.isSet(value)) return inspectCollection(value, false)
46
+ if (
47
+ Array.isArray(value) ||
48
+ types.isTypedArray(value) ||
49
+ types.isAnyArrayBuffer(value) ||
50
+ types.isDataView(value) ||
51
+ types.isWeakMap(value) ||
52
+ types.isWeakSet(value) ||
53
+ types.isMapIterator(value) ||
54
+ types.isSetIterator(value)
55
+ ) {
56
+ return inspect(value, segmentInspectOptions)
57
+ }
58
+
59
+ /** @type {(string | symbol)[]} */
60
+ const keys = Object.keys(value)
61
+ let propertyCount = keys.length
62
+ const symbols = Object.getOwnPropertySymbols(value)
63
+ for (let i = 0; i < symbols.length; i++) {
64
+ if (Object.getOwnPropertyDescriptor(value, symbols[i])?.enumerable === true) {
65
+ propertyCount++
66
+ if (keys.length < maxProperties) keys.push(symbols[i])
67
+ }
68
+ }
69
+
70
+ if (propertyCount <= maxProperties) {
71
+ // TODO: Decide whether allowing util.inspect to invoke Symbol.toStringTag getters is acceptable. If it is,
72
+ // remove inspectionCanRunUserCode and the related omission paths.
73
+ if (inspectionCanRunUserCode(value)) {
74
+ return '[Value omitted: inspection may execute user code]'
75
+ }
76
+ return inspect(value, segmentInspectOptions)
77
+ }
78
+
79
+ const truncated = {}
80
+ for (let i = 0; i < maxProperties; i++) {
81
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(value, keys[i]))
82
+ if (
83
+ (keys[i] === Symbol.toStringTag && descriptor.get !== undefined) ||
84
+ (descriptor.value !== value && inspectionCanRunUserCode(descriptor.value))
85
+ ) {
86
+ return '[Value omitted: inspection may execute user code]'
87
+ }
88
+ if (descriptor.value === value) descriptor.value = truncated
89
+ Object.defineProperty(truncated, keys[i], descriptor)
90
+ }
91
+
92
+ const omitted = propertyCount - maxProperties
93
+ const inspected = inspect(truncated, segmentInspectOptions)
94
+ return `${inspected.slice(0, -2)}, ... ${omitted} more ${omitted === 1 ? 'property' : 'properties'} }`
95
+ }
96
+
97
+ /**
98
+ * Inspect a Map or Set while bounding the number of entries on Node.js 18, where `util.inspect` does not.
99
+ *
100
+ * @param {Collection} value
101
+ * @param {boolean} isMap
102
+ * @returns {string}
103
+ */
104
+ function inspectCollection (value, isMap) {
105
+ if (NODE_MAJOR !== 18) return inspect(value, segmentInspectOptions)
106
+
107
+ const size = (isMap ? mapSizeGetter : setSizeGetter).call(value)
108
+ if (size <= maxCollectionEntries) return inspect(value, segmentInspectOptions)
109
+
110
+ const truncated = isMap ? new Map() : new Set()
111
+ const iterator = (isMap ? mapEntries : setValues).call(value)
112
+ const iteratorNext = isMap ? mapIteratorNext : setIteratorNext
113
+
114
+ for (let i = 0; i < maxCollectionEntries; i++) {
115
+ const result = iteratorNext.call(iterator)
116
+ if (result.done) break
117
+
118
+ if (isMap) {
119
+ const entry = result.value
120
+ const key = entry[0] === value ? truncated : entry[0]
121
+ const entryValue = entry[1] === value ? truncated : entry[1]
122
+ mapSet.call(truncated, key, entryValue)
123
+ } else {
124
+ const entryValue = result.value === value ? truncated : result.value
125
+ setAdd.call(truncated, entryValue)
126
+ }
127
+ }
128
+
129
+ const type = isMap ? 'Map' : 'Set'
130
+ const inspected = inspect(truncated, segmentInspectOptions)
131
+ const normalized = inspected.replace(`${type}(${maxCollectionEntries})`, `${type}(${size})`)
132
+ const remaining = size - maxCollectionEntries
133
+ return `${normalized.slice(0, -2)}, ... ${remaining} more item${remaining === 1 ? '' : 's'} }`
134
+ }
135
+
136
+ /**
137
+ * Determine whether inspecting a value could invoke a proxy trap or toStringTag getter.
138
+ *
139
+ * @param {unknown} value
140
+ * @returns {boolean}
141
+ */
142
+ function inspectionCanRunUserCode (value) {
143
+ const type = typeof value
144
+ if (value === null || (type !== 'object' && type !== 'function')) return false
145
+ if (types.isProxy(value)) return true
146
+
147
+ let current = value
148
+ while (current !== null) {
149
+ if (Object.getOwnPropertyDescriptor(current, Symbol.toStringTag)?.get !== undefined) return true
150
+ current = Object.getPrototypeOf(current)
151
+ if (types.isProxy(current)) return true
152
+ }
153
+ return false
154
+ }
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ EVP_PROXY_PATH_V2: '/evp_proxy/v2',
5
+ EVP_PROXY_PATH_V4: '/evp_proxy/v4',
6
+ EVP_SUBDOMAIN_HEADER_NAME: 'X-Datadog-EVP-Subdomain',
7
+ EVP_EVENT_PLATFORM_SUBDOMAIN: 'event-platform-intake',
8
+ }
@@ -0,0 +1,64 @@
1
+ 'use strict'
2
+
3
+ const { format } = require('node:url')
4
+
5
+ const { HttpsProxyAgent } = require('../../../../vendor/dist/https-proxy-agent')
6
+ const { getProxyForUrl } = require('../../../../vendor/dist/proxy-from-env')
7
+ const log = require('../log')
8
+
9
+ /**
10
+ * @typedef {object} DirectEVPRoute
11
+ * @property {URL} url - Direct intake URL
12
+ * @property {string} basePath - Direct intake base path
13
+ * @property {object} headers - Direct intake authentication headers
14
+ * @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
15
+ */
16
+
17
+ /**
18
+ * Creates an authenticated direct EVP intake route.
19
+ *
20
+ * This helper does not perform local receiver discovery.
21
+ *
22
+ * @param {import('../config/config-base')} config - Tracer configuration
23
+ * @param {string} intake - EVP intake subdomain
24
+ * @returns {DirectEVPRoute|undefined} Direct route when credentials and site are available
25
+ */
26
+ function createDirectEVPRoute (config, intake) {
27
+ const apiKey = config.DD_API_KEY
28
+ if (!apiKey || !config.site) return
29
+
30
+ try {
31
+ const hostname = `${intake}.${config.site}`.toLowerCase()
32
+ const url = new URL(format({
33
+ protocol: 'https:',
34
+ hostname,
35
+ }))
36
+ if (
37
+ url.hostname !== hostname ||
38
+ url.username ||
39
+ url.password ||
40
+ url.port ||
41
+ url.pathname !== '/' ||
42
+ url.search ||
43
+ url.hash
44
+ ) {
45
+ throw new Error('Invalid direct EVP intake URL')
46
+ }
47
+
48
+ const proxyUrl = getProxyForUrl(url.href)
49
+ const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined
50
+
51
+ return {
52
+ url,
53
+ basePath: '',
54
+ headers: {
55
+ 'DD-API-KEY': apiKey,
56
+ },
57
+ ...(agent && { agent }),
58
+ }
59
+ } catch (error) {
60
+ log.debug('Unable to configure direct EVP intake: %s', error.message)
61
+ }
62
+ }
63
+
64
+ module.exports = { createDirectEVPRoute }
@@ -0,0 +1,133 @@
1
+ 'use strict'
2
+
3
+ const { fetchAgentInfo } = require('../agent/info')
4
+ const log = require('../log')
5
+
6
+ const TRAILING_SLASHES = /\/+$/
7
+
8
+ /**
9
+ * Receiver discovery contract
10
+ *
11
+ * The tracer sends `GET /info` to its configured local Agent URL. An
12
+ * Agent-compatible trace receiver produces the response. The tracer does not.
13
+ *
14
+ * The full Agent and serverless-init embed the same trace receiver. A future
15
+ * in-process receiver can expose the same contract. Callers can therefore
16
+ * select capabilities without detecting the receiver implementation.
17
+ *
18
+ * For EVP discovery, `endpoints` advertises registered proxy paths.
19
+ * `evp_proxy_allowed_headers` advertises headers that the proxy forwards to
20
+ * intake. It does not include routing headers that the proxy consumes, such as
21
+ * `X-Datadog-EVP-Subdomain`.
22
+ *
23
+ * An advertised route is not a health check. The receiver can register an EVP
24
+ * route while configuration disables its handler. The request then returns
25
+ * `405`. The `/info` version also identifies the embedded Agent code, not a
26
+ * serverless-init image or deployment type.
27
+ *
28
+ * This module only discovers a candidate route. A missing or unresponsive
29
+ * `/info` endpoint returns an error through the shared request timeout and
30
+ * retry policy. A valid response without a compatible path returns no route.
31
+ * Discovery sends no events, so the caller can safely select direct intake
32
+ * after either result. The caller also owns later delivery failures. Exposure
33
+ * delivery uses retries and can therefore produce duplicates. After local
34
+ * retries fail, the caller can retry through direct intake and use that route
35
+ * for future batches.
36
+ *
37
+ * Reference implementations:
38
+ *
39
+ * Agent `/info` and EVP proxy:
40
+ * https://github.com/DataDog/datadog-agent/tree/main/pkg/trace/api
41
+ *
42
+ * serverless-init entry point and embedded trace receiver:
43
+ * https://github.com/DataDog/datadog-agent/blob/main/cmd/serverless-init/main.go
44
+ * https://github.com/DataDog/datadog-agent/blob/main/pkg/serverless/trace/trace.go
45
+ */
46
+
47
+ /**
48
+ * Selects the first advertised EVP proxy path that the caller supports.
49
+ *
50
+ * @param {object} agentInfo - Agent `/info` response
51
+ * @param {object} options - Selection options
52
+ * @param {string[]} options.supportedPaths - Supported paths in preference order
53
+ * @param {string[]} [options.requiredHeaders] - Headers that the proxy must forward unchanged to intake. Each
54
+ * header must appear in `evp_proxy_allowed_headers`. Do not include routing headers that the Agent consumes.
55
+ * @returns {string|undefined} Selected normalized path
56
+ */
57
+ function selectEVPProxyPath (agentInfo, { supportedPaths, requiredHeaders = [] } = {}) {
58
+ if (!Array.isArray(agentInfo?.endpoints) ||
59
+ !Array.isArray(supportedPaths) ||
60
+ !Array.isArray(requiredHeaders) ||
61
+ requiredHeaders.some(header => typeof header !== 'string')) {
62
+ return
63
+ }
64
+
65
+ const allowedHeaders = agentInfo.evp_proxy_allowed_headers
66
+ if (allowedHeaders !== undefined) {
67
+ if (!Array.isArray(allowedHeaders)) return
68
+
69
+ const normalizedHeaders = new Set()
70
+ for (const header of allowedHeaders) {
71
+ if (typeof header === 'string') {
72
+ normalizedHeaders.add(header.toLowerCase())
73
+ }
74
+ }
75
+
76
+ if (requiredHeaders.some(header => !normalizedHeaders.has(header.toLowerCase()))) {
77
+ return
78
+ }
79
+ }
80
+
81
+ const advertisedPaths = new Set()
82
+ for (const endpoint of agentInfo.endpoints) {
83
+ if (typeof endpoint === 'string') {
84
+ advertisedPaths.add(endpoint.replace(TRAILING_SLASHES, ''))
85
+ }
86
+ }
87
+
88
+ for (const supportedPath of supportedPaths) {
89
+ if (typeof supportedPath !== 'string') continue
90
+
91
+ const normalizedPath = supportedPath.replace(TRAILING_SLASHES, '')
92
+ if (advertisedPaths.has(normalizedPath)) {
93
+ return normalizedPath
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Discovers an EVP proxy route through the configured Agent URL.
100
+ *
101
+ * This function performs discovery only when the caller invokes it. It stores
102
+ * no state. The Agent information client owns its existing response cache.
103
+ *
104
+ * @param {URL} url - Configured Agent URL
105
+ * @param {object} options - Selection options
106
+ * @param {string[]} options.supportedPaths - Supported paths in preference order
107
+ * @param {string[]} [options.requiredHeaders] - Headers that the proxy must forward unchanged to intake. Each
108
+ * header must appear in `evp_proxy_allowed_headers`. Do not include routing headers that the Agent consumes.
109
+ * @param {(error: Error|null, route?: {url: URL, basePath: string}) => void} callback - Result callback
110
+ * @returns {void}
111
+ */
112
+ function discoverEVPProxy (url, options, callback) {
113
+ fetchAgentInfo(url, (error, agentInfo) => {
114
+ if (error) {
115
+ callback(error)
116
+ return
117
+ }
118
+
119
+ const basePath = selectEVPProxyPath(agentInfo, options)
120
+ if (basePath === undefined) {
121
+ callback(null)
122
+ return
123
+ }
124
+
125
+ log.debug('EVP proxy route %s discovered through the configured local receiver', basePath)
126
+ callback(null, { url, basePath })
127
+ })
128
+ }
129
+
130
+ module.exports = {
131
+ discoverEVPProxy,
132
+ selectEVPProxyPath,
133
+ }
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const LEADING_SLASHES = /^\/+/
4
+ const TRAILING_SLASHES = /\/+$/
5
+
6
+ /**
7
+ * Joins a caller-supplied EVP proxy path and product endpoint.
8
+ *
9
+ * This utility does not perform EVP proxy discovery.
10
+ *
11
+ * @param {string} basePath - EVP proxy base path
12
+ * @param {string} endpoint - Product intake endpoint
13
+ * @returns {string} Joined request path
14
+ */
15
+ function joinEVPProxyPath (basePath, endpoint) {
16
+ const normalizedBasePath = basePath.replace(TRAILING_SLASHES, '')
17
+ const normalizedEndpoint = endpoint.replace(LEADING_SLASHES, '')
18
+
19
+ return `${normalizedBasePath}/${normalizedEndpoint}`
20
+ }
21
+
22
+ module.exports = { joinEVPProxyPath }
@@ -93,7 +93,10 @@ function request (data, options, callback) {
93
93
 
94
94
  docker.inject(options.headers)
95
95
 
96
- options.agent = isSecure ? httpsAgent : httpAgent
96
+ const connectionOptions = {
97
+ ...options,
98
+ agent: options.agent ?? (isSecure ? httpsAgent : httpAgent),
99
+ }
97
100
 
98
101
  /**
99
102
  * @param {import('node:http').IncomingMessage} res
@@ -218,7 +221,7 @@ function request (data, options, callback) {
218
221
  }
219
222
  }
220
223
 
221
- const req = client.request(options, (res) => onResponse(res, complete, handleError))
224
+ const req = client.request(connectionOptions, (res) => onResponse(res, complete, handleError))
222
225
 
223
226
  req.once('close', finalize)
224
227
  req.once('timeout', finalize)
@@ -33,7 +33,8 @@ function datasetRecordFromResource (resource) {
33
33
  attrs.input ?? null,
34
34
  attrs.expected_output ?? null,
35
35
  attrs.metadata ?? {},
36
- id
36
+ id,
37
+ attrs.tags ?? []
37
38
  )
38
39
  }
39
40
 
@@ -195,7 +196,7 @@ class ExperimentsClient {
195
196
  const resources = Array.isArray(response?.records)
196
197
  ? response.records
197
198
  : (Array.isArray(response?.data) ? response.data : [])
198
- return resources.map(datasetRecordFromResource)
199
+ return datasetMutationResultFromResources(resources)
199
200
  }
200
201
 
201
202
  async batchUpdateDatasetRecords (projectId, datasetId, attributes) {
@@ -226,6 +227,9 @@ class ExperimentsClient {
226
227
  const query = new URLSearchParams()
227
228
  if (options.cursor) query.set('page[cursor]', options.cursor)
228
229
  if (options.version !== undefined && options.version !== null) query.set('filter[version]', String(options.version))
230
+ if (Array.isArray(options.tags)) {
231
+ for (const tag of options.tags) query.append('filter[tags]', tag)
232
+ }
229
233
  const response = await this.request(
230
234
  'GET',
231
235
  `${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records?${query.toString()}`