dd-trace 6.1.0 → 6.3.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 (58) hide show
  1. package/README.md +1 -1
  2. package/index.d.ts +7 -0
  3. package/initialize.mjs +4 -2
  4. package/package.json +5 -5
  5. package/packages/datadog-instrumentations/src/cookie.js +7 -1
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -1
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +30 -54
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
  12. package/packages/datadog-instrumentations/src/http/server.js +27 -0
  13. package/packages/datadog-instrumentations/src/mercurius.js +11 -0
  14. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +248 -1
  15. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
  16. package/packages/datadog-instrumentations/src/playwright.js +40 -3
  17. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  18. package/packages/datadog-instrumentations/src/vitest-util.js +0 -3
  19. package/packages/datadog-instrumentations/src/vitest-worker.js +0 -1
  20. package/packages/datadog-plugin-azure-functions/src/index.js +2 -1
  21. package/packages/datadog-plugin-graphql/src/execute.js +4 -15
  22. package/packages/datadog-plugin-graphql/src/index.js +6 -0
  23. package/packages/datadog-plugin-graphql/src/request.js +104 -0
  24. package/packages/datadog-plugin-graphql/src/utils.js +177 -0
  25. package/packages/datadog-plugin-graphql/src/validate.js +24 -1
  26. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +144 -26
  27. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/utils.js +30 -0
  28. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  29. package/packages/datadog-plugin-vitest/src/index.js +0 -8
  30. package/packages/datadog-shimmer/src/shimmer.js +9 -2
  31. package/packages/dd-trace/src/appsec/channels.js +1 -0
  32. package/packages/dd-trace/src/appsec/index.js +6 -3
  33. package/packages/dd-trace/src/appsec/lambda.js +8 -8
  34. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  35. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +23 -16
  36. package/packages/dd-trace/src/config/config-types.d.ts +8 -0
  37. package/packages/dd-trace/src/config/defaults.js +20 -15
  38. package/packages/dd-trace/src/config/index.js +13 -3
  39. package/packages/dd-trace/src/config/parsers.js +79 -0
  40. package/packages/dd-trace/src/datastreams/writer.js +14 -2
  41. package/packages/dd-trace/src/encode/0.4.js +23 -2
  42. package/packages/dd-trace/src/encode/0.5.js +12 -1
  43. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
  44. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
  45. package/packages/dd-trace/src/exporters/common/writer.js +17 -1
  46. package/packages/dd-trace/src/guardrails/index.js +3 -1
  47. package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
  48. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  49. package/packages/dd-trace/src/llmobs/tagger.js +15 -1
  50. package/packages/dd-trace/src/msgpack/chunk.js +33 -1
  51. package/packages/dd-trace/src/msgpack/index.js +6 -1
  52. package/packages/dd-trace/src/opentelemetry/span_processor.js +7 -5
  53. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +32 -16
  54. package/packages/dd-trace/src/plugins/index.js +3 -0
  55. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -11
  56. package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
  57. package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
  58. package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
@@ -1,55 +1,173 @@
1
1
  'use strict'
2
2
 
3
+ const Plugin = require('../../dd-trace/src/plugins/plugin')
3
4
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
5
 
5
- class McpToolCallPlugin extends TracingPlugin {
6
- static id = 'modelcontextprotocol_client'
7
- static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_callTool'
6
+ const {
7
+ DISTRIBUTED_TRACE_META_KEY,
8
+ tagErrorResult,
9
+ } = require('./utils')
8
10
 
9
- bindStart (ctx) {
10
- const params = ctx.arguments?.[0]
11
- const toolName = params?.name
11
+ const MCP_REQUEST_SPAN_NAME = 'mcp.request'
12
+ const CLIENT_TOOL_CALL_RESOURCE = 'client_tool_call'
13
+ const CLIENT_LIST_TOOLS_RESOURCE = 'ClientSession.list_tools'
14
+ const SERVER_TOOL_CALL_RESOURCE = 'server_tool_call'
12
15
 
13
- this.startSpan('mcp.client.tool.call', {
14
- resource: toolName,
16
+ function getServerRequestResource (request) {
17
+ if (request?.method === 'tools/call') return SERVER_TOOL_CALL_RESOURCE
18
+ return request?.method
19
+ }
20
+
21
+ class McpPlugin extends TracingPlugin {
22
+ bindStart (ctx) {
23
+ const spanOptions = {
24
+ resource: this.getResource(ctx),
15
25
  type: 'mcp',
16
- kind: 'client',
17
- }, ctx)
26
+ kind: this.constructor.kind,
27
+ }
28
+ const childOf = this.getChildOf(ctx)
29
+ if (childOf) spanOptions.childOf = childOf
18
30
 
31
+ this.startSpan(this.constructor.spanName, spanOptions, ctx)
32
+ const span = ctx.currentStore?.span
33
+ if (span) this.onStart(span, ctx)
19
34
  return ctx.currentStore
20
35
  }
21
36
 
22
37
  asyncEnd (ctx) {
23
- const result = ctx.result
24
- if (result?.isError) {
25
- const span = ctx.currentStore?.span
26
- const errorText = result.content?.find?.(c => c.type === 'text')?.text || 'Tool call returned isError: true'
27
- span?.setTag('error', new Error(errorText))
28
- }
38
+ const span = ctx.currentStore?.span
39
+ if (span) this.onEnd(span, ctx)
29
40
  super.finish(ctx)
30
41
  }
42
+
43
+ getResource () {}
44
+ getChildOf () {}
45
+ onStart () {}
46
+ onEnd () {}
31
47
  }
32
48
 
33
- class McpListToolsPlugin extends TracingPlugin {
49
+ class McpPropagationPlugin extends Plugin {
50
+ static id = 'modelcontextprotocol_propagation'
51
+
52
+ constructor (...args) {
53
+ super(...args)
54
+ this.addSub('apm:mcp:client:request:inject', this.injectTraceContext.bind(this))
55
+ }
56
+
57
+ injectTraceContext (ctx) {
58
+ const span = this.tracer.scope().active()
59
+ if (!span) return
60
+
61
+ const traceContext = {}
62
+ this.tracer.inject(span, 'text_map', traceContext)
63
+ ctx.traceContext = traceContext
64
+ }
65
+ }
66
+
67
+ class McpToolCallPlugin extends McpPlugin {
68
+ static id = 'modelcontextprotocol_client'
69
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_callTool'
70
+ static spanName = MCP_REQUEST_SPAN_NAME
71
+ static kind = 'client'
72
+ getResource () { return CLIENT_TOOL_CALL_RESOURCE }
73
+ onEnd (span, ctx) { tagErrorResult(span, ctx.result) }
74
+ }
75
+
76
+ class McpListToolsPlugin extends McpPlugin {
34
77
  static id = 'modelcontextprotocol_list_tools'
35
78
  static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_listTools'
79
+ static spanName = MCP_REQUEST_SPAN_NAME
80
+ static kind = 'client'
81
+ getResource () { return CLIENT_LIST_TOOLS_RESOURCE }
82
+ }
36
83
 
37
- bindStart (ctx) {
38
- this.startSpan('mcp.tools.list', {
39
- resource: 'tools/list',
40
- type: 'mcp',
41
- kind: 'client',
42
- }, ctx)
84
+ class McpListResourcesPlugin extends McpPlugin {
85
+ static id = 'modelcontextprotocol_list_resources'
86
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_listResources'
87
+ static spanName = 'mcp.resources.list'
88
+ static kind = 'client'
89
+ getResource () { return 'resources/list' }
90
+ }
43
91
 
44
- return ctx.currentStore
92
+ class McpReadResourcePlugin extends McpPlugin {
93
+ static id = 'modelcontextprotocol_read_resource'
94
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_readResource'
95
+ static spanName = 'mcp.resource.read'
96
+ static kind = 'client'
97
+ getResource () { return 'resources/read' }
98
+ }
99
+
100
+ class McpListPromptsPlugin extends McpPlugin {
101
+ static id = 'modelcontextprotocol_list_prompts'
102
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_listPrompts'
103
+ static spanName = 'mcp.prompts.list'
104
+ static kind = 'client'
105
+ getResource () { return 'prompts/list' }
106
+ }
107
+
108
+ class McpGetPromptPlugin extends McpPlugin {
109
+ static id = 'modelcontextprotocol_get_prompt'
110
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:Client_getPrompt'
111
+ static spanName = 'mcp.prompt.get'
112
+ static kind = 'client'
113
+ getResource () { return 'prompts/get' }
114
+ }
115
+
116
+ class McpServerRequestPlugin extends McpPlugin {
117
+ static id = 'modelcontextprotocol_server'
118
+ static prefix = 'tracing:apm:mcp:server:request'
119
+ static spanName = MCP_REQUEST_SPAN_NAME
120
+ static kind = 'server'
121
+ getResource (ctx) { return getServerRequestResource(ctx.request) }
122
+ getChildOf (ctx) {
123
+ const traceContext = ctx.request?.params?._meta?.[DISTRIBUTED_TRACE_META_KEY]
124
+ if (!traceContext || typeof traceContext !== 'object') return
125
+
126
+ const childOf = this.tracer.extract('text_map', traceContext)
127
+ const activeSpan = this.activeSpan
128
+ if (!childOf || activeSpan?.context().toTraceId() === childOf.toTraceId()) return
129
+
130
+ return childOf
45
131
  }
46
132
 
47
- asyncEnd (ctx) {
48
- super.finish(ctx)
133
+ onEnd (span, ctx) {
134
+ if (ctx.error) {
135
+ span.setTag('error', ctx.error)
136
+ } else {
137
+ tagErrorResult(span, ctx.result)
138
+ }
49
139
  }
50
140
  }
51
141
 
142
+ class McpServerToolCallPlugin extends McpPlugin {
143
+ static id = 'modelcontextprotocol_server_tool'
144
+ static prefix = 'tracing:orchestrion:@modelcontextprotocol/sdk:McpServer_executeToolHandler'
145
+ static spanName = 'mcp.server.tool.call'
146
+ static kind = 'internal'
147
+
148
+ /**
149
+ * @param {...unknown} args Plugin constructor arguments.
150
+ */
151
+ constructor (...args) {
152
+ super(...args)
153
+ this.toolNames = new WeakMap()
154
+ this.addSub('apm:mcp:server:tool:registered', ({ tool, name }) => {
155
+ this.toolNames.set(tool, name)
156
+ })
157
+ }
158
+
159
+ getResource (ctx) { return this.toolNames.get(ctx.arguments?.[0]) }
160
+ onEnd (span, ctx) { tagErrorResult(span, ctx.result) }
161
+ }
162
+
52
163
  module.exports = [
164
+ McpPropagationPlugin,
53
165
  McpToolCallPlugin,
54
166
  McpListToolsPlugin,
167
+ McpListResourcesPlugin,
168
+ McpReadResourcePlugin,
169
+ McpListPromptsPlugin,
170
+ McpGetPromptPlugin,
171
+ McpServerRequestPlugin,
172
+ McpServerToolCallPlugin,
55
173
  ]
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ const { ERROR_MESSAGE, ERROR_TYPE } = require('../../dd-trace/src/constants')
4
+
5
+ const DISTRIBUTED_TRACE_META_KEY = '_dd_trace_context'
6
+
7
+ function getFirstTextContent (content) {
8
+ if (!Array.isArray(content)) return
9
+
10
+ for (const item of content) {
11
+ if (item.type === 'text' && item.text) return item.text
12
+ }
13
+ }
14
+
15
+ function setErrorTags (span, message) {
16
+ span.setTag('error', 1)
17
+ span.setTag(ERROR_TYPE, 'Error')
18
+ span.setTag(ERROR_MESSAGE, message)
19
+ }
20
+
21
+ function tagErrorResult (span, result) {
22
+ if (result?.isError) {
23
+ setErrorTags(span, getFirstTextContent(result.content) || 'Tool call returned isError: true')
24
+ }
25
+ }
26
+
27
+ module.exports = {
28
+ DISTRIBUTED_TRACE_META_KEY,
29
+ tagErrorResult,
30
+ }
@@ -68,7 +68,7 @@ class PlaywrightPlugin extends CiPlugin {
68
68
  }) => {
69
69
  const testSuite = getTestSuitePath(filePath, this.repositoryRoot)
70
70
  const isModified = isModifiedTest(testSuite, 0, 0, modifiedFiles, this.constructor.id)
71
- onDone({ isModified })
71
+ onDone(isModified)
72
72
  })
73
73
 
74
74
  this.addSub('ci:playwright:session:finish', ({
@@ -67,7 +67,6 @@ class VitestPlugin extends CiPlugin {
67
67
  testCommand: this.command,
68
68
  repositoryRoot: this.repositoryRoot,
69
69
  codeOwnersEntries: this.codeOwnersEntries,
70
- testEnvironmentMetadata: this.testEnvironmentMetadata,
71
70
  })
72
71
  })
73
72
 
@@ -324,7 +323,6 @@ class VitestPlugin extends CiPlugin {
324
323
  const {
325
324
  codeOwnersEntries,
326
325
  repositoryRoot,
327
- testEnvironmentMetadata,
328
326
  requestErrorTags,
329
327
  testSuiteAbsolutePath,
330
328
  frameworkVersion,
@@ -334,12 +332,6 @@ class VitestPlugin extends CiPlugin {
334
332
 
335
333
  const testCommand = ctx.testCommand || 'vitest run'
336
334
  const { testSessionId, testModuleId } = ctx
337
- if (testEnvironmentMetadata) {
338
- this.testEnvironmentMetadata = {
339
- ...this.testEnvironmentMetadata,
340
- ...testEnvironmentMetadata,
341
- }
342
- }
343
335
  this._setRepositoryRoot(repositoryRoot, codeOwnersEntries)
344
336
  this.command = testCommand
345
337
  this.frameworkVersion = frameworkVersion
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { isModuleNamespaceObject } = require('node:util').types
4
+
3
5
  /**
4
6
  * @type {Set<string | symbol>}
5
7
  */
@@ -168,14 +170,19 @@ function wrap (target, name, wrapper, options) {
168
170
 
169
171
  copyProperties(original, wrapped)
170
172
 
171
- if (descriptor.writable) {
173
+ const immutableModuleNamespace = descriptor.configurable === false &&
174
+ descriptor.writable && isModuleNamespaceObject(target)
175
+
176
+ if (descriptor.writable && !immutableModuleNamespace) {
172
177
  if (descriptor.configurable && descriptor.enumerable) {
173
178
  target[name] = wrapped
174
179
  return target
175
180
  }
176
181
  descriptor.value = wrapped
177
182
  } else {
178
- if (descriptor.set && options?.replaceGetter) {
183
+ if (immutableModuleNamespace) {
184
+ descriptor.value = wrapped
185
+ } else if (descriptor.set && options?.replaceGetter) {
179
186
  // A lazy accessor pair (e.g. Node 20's `fs.opendir`). `original` already
180
187
  // resolved the value through the getter. Keep the property an accessor pair
181
188
  // so the shape stays observationally identical — a downstream consumer may
@@ -25,6 +25,7 @@ module.exports = {
25
25
  httpClientRequestStart: dc.channel('apm:http:client:request:start'),
26
26
  httpClientResponseStart: dc.channel('apm:http:client:response:start'),
27
27
  httpClientResponseFinish: dc.channel('apm:http:client:response:finish'),
28
+ informationalResponse: dc.channel('datadog:http:server:informational-response:start'),
28
29
  incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
29
30
  incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
30
31
  lambdaStartInvocation: dc.channel('datadog:lambda:start-invocation'),
@@ -29,6 +29,7 @@ const {
29
29
  responseBody,
30
30
  responseWriteHead,
31
31
  responseSetHeader,
32
+ informationalResponse,
32
33
  routerParam,
33
34
  fastifyResponseChannel,
34
35
  fastifyPathParams,
@@ -98,7 +99,8 @@ function enable (_config) {
98
99
  responseBody.subscribe(onResponseBody)
99
100
  fastifyResponseChannel.subscribe(onResponseBody)
100
101
  responseWriteHead.subscribe(onResponseWriteHead)
101
- responseSetHeader.subscribe(onResponseSetHeader)
102
+ responseSetHeader.subscribe(onResponseOperation)
103
+ informationalResponse.subscribe(onResponseOperation)
102
104
  stripeCheckoutSessionCreate.subscribe(onStripeCheckoutSessionCreate)
103
105
  stripePaymentIntentCreate.subscribe(onStripePaymentIntentCreate)
104
106
  stripeConstructEvent.subscribe(onStripeConstructEvent)
@@ -403,7 +405,7 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH
403
405
  handleResults(results?.actions, req, res, rootSpan, abortController)
404
406
  }
405
407
 
406
- function onResponseSetHeader ({ res, abortController }) {
408
+ function onResponseOperation ({ res, abortController }) {
407
409
  if (isBlocked(res)) {
408
410
  abortController?.abort()
409
411
  }
@@ -547,7 +549,8 @@ function disable () {
547
549
  if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
548
550
  if (fastifyResponseChannel.hasSubscribers) fastifyResponseChannel.unsubscribe(onResponseBody)
549
551
  if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
550
- if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
552
+ if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseOperation)
553
+ if (informationalResponse.hasSubscribers) informationalResponse.unsubscribe(onResponseOperation)
551
554
  if (stripeCheckoutSessionCreate.hasSubscribers) stripeCheckoutSessionCreate.unsubscribe(onStripeCheckoutSessionCreate)
552
555
  if (stripePaymentIntentCreate.hasSubscribers) stripePaymentIntentCreate.unsubscribe(onStripePaymentIntentCreate)
553
556
  if (stripeConstructEvent.hasSubscribers) stripeConstructEvent.unsubscribe(onStripeConstructEvent)
@@ -7,9 +7,7 @@ const addresses = require('./addresses')
7
7
  const Reporter = require('./reporter')
8
8
  const waf = require('./waf')
9
9
 
10
- // Tracks spans for which start-invocation has been processed, so that
11
- // end-invocation can gate correctly
12
- const activeInvocations = new WeakSet()
10
+ const activeInvocations = new WeakMap()
13
11
 
14
12
  /**
15
13
  * Maps pre-extracted HTTP data from the Lambda event to WAF addresses,
@@ -31,7 +29,8 @@ function onLambdaStartInvocation (data) {
31
29
  return
32
30
  }
33
31
 
34
- activeInvocations.add(span)
32
+ const req = { headers: headers ?? {} }
33
+ activeInvocations.set(span, req)
35
34
 
36
35
  span.addTags({
37
36
  '_dd.appsec.enabled': 1,
@@ -74,7 +73,7 @@ function onLambdaStartInvocation (data) {
74
73
  persistent[addresses.HTTP_INCOMING_COOKIES] = cookies
75
74
  }
76
75
 
77
- waf.run({ persistent }, span, undefined, span)
76
+ waf.run({ persistent }, req, undefined, span)
78
77
  } catch (err) {
79
78
  log.error('[ASM] Error in Lambda start-invocation handler', err)
80
79
  }
@@ -100,6 +99,7 @@ function onLambdaEndInvocation (data) {
100
99
  return
101
100
  }
102
101
 
102
+ const req = activeInvocations.get(span)
103
103
  activeInvocations.delete(span)
104
104
 
105
105
  let hasPersistentData = false
@@ -118,12 +118,12 @@ function onLambdaEndInvocation (data) {
118
118
  }
119
119
 
120
120
  if (hasPersistentData) {
121
- waf.run({ persistent }, span, undefined, span)
121
+ waf.run({ persistent }, req, undefined, span)
122
122
  }
123
123
 
124
- waf.disposeContext(span)
124
+ waf.disposeContext(req)
125
125
 
126
- Reporter.finishRequest(span, null, {}, undefined, span)
126
+ Reporter.finishRequest(req, null, {}, undefined, span)
127
127
  } catch (err) {
128
128
  log.error('[ASM] Error in Lambda end-invocation handler', err)
129
129
  }
@@ -42,7 +42,7 @@ class WAFManager {
42
42
  this.ddwafVersion = this.ddwafVersion || 'unknown'
43
43
  Reporter.reportWafInit(this.ddwafVersion, 'unknown')
44
44
 
45
- log.error('[ASM] AppSec could not load native package. In-app WAF features will not be available.')
45
+ log.error('[ASM] AppSec could not load native package. In-app WAF features will not be available.', err)
46
46
 
47
47
  throw err
48
48
  }
@@ -51,12 +51,20 @@ const limits = {
51
51
  }
52
52
 
53
53
  /**
54
- * Remove empty collection arrays before sending snapshots through the Test Optimization logs path.
54
+ * @param {object} value - Snapshot object or sub-object.
55
+ * @returns {boolean}
56
+ */
57
+ function isCapturedValue (value) {
58
+ return typeof value.type === 'string'
59
+ }
60
+
61
+ /**
62
+ * Remove empty capture containers before sending snapshots through the Test Optimization logs path.
55
63
  *
56
64
  * @param {object} value - Snapshot object or sub-object.
57
65
  * @returns {void}
58
66
  */
59
- function removeEmptyCollectionProperties (value) {
67
+ function removeEmptyCaptureProperties (value) {
60
68
  const stack = [value]
61
69
 
62
70
  while (stack.length > 0) {
@@ -72,26 +80,25 @@ function removeEmptyCollectionProperties (value) {
72
80
  continue
73
81
  }
74
82
 
75
- if (Array.isArray(current.elements)) {
76
- if (current.elements.length === 0) {
83
+ if (isCapturedValue(current)) {
84
+ if (current.elements === null || (Array.isArray(current.elements) && current.elements.length === 0)) {
77
85
  delete current.elements
78
- } else {
79
- stack.push(current.elements)
80
86
  }
81
- }
82
87
 
83
- if (Array.isArray(current.entries)) {
84
- if (current.entries.length === 0) {
88
+ if (current.entries === null || (Array.isArray(current.entries) && current.entries.length === 0)) {
85
89
  delete current.entries
86
- } else {
87
- stack.push(current.entries)
88
90
  }
89
- }
90
91
 
91
- for (const key of Object.keys(current)) {
92
- if (key === 'elements' || key === 'entries') continue
92
+ if (current.fields === null || (
93
+ current.fields &&
94
+ typeof current.fields === 'object' &&
95
+ Object.keys(current.fields).length === 0
96
+ )) {
97
+ delete current.fields
98
+ }
99
+ }
93
100
 
94
- const child = current[key]
101
+ for (const child of Object.values(current)) {
95
102
  if (child && typeof child === 'object') {
96
103
  stack.push(child)
97
104
  }
@@ -129,7 +136,7 @@ session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint]
129
136
  language: 'javascript',
130
137
  }
131
138
 
132
- removeEmptyCollectionProperties(snapshot.captures)
139
+ removeEmptyCaptureProperties(snapshot.captures)
133
140
 
134
141
  if (processTags) {
135
142
  snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject
@@ -70,3 +70,11 @@ export type ConfigKey = KnownStringKeys<ConfigProperties>
70
70
  export type ConfigPath = ConfigPathFor<ConfigProperties>
71
71
  export type ConfigPathValue<TPath extends ConfigPath> = ConfigPathValueFor<ConfigProperties, TPath>
72
72
  export type ConfigDefaults = Partial<{ [TPath in ConfigPath]: ConfigPathValue<TPath> }>
73
+ export type ConfigurationOption = {
74
+ property?: string
75
+ parser?: (value: unknown, optionName: string, source: string) => unknown
76
+ type: string
77
+ canonicalName?: string
78
+ transformer?: (value: unknown, optionName: string, source: string) => unknown
79
+ telemetryTransformer?: (value: unknown) => unknown
80
+ }
@@ -1,9 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
3
4
  const util = require('util')
4
5
 
5
6
  const { DD_MAJOR } = require('../../../../version')
6
- const { parsers, transformers, telemetryTransformers, setWarnInvalidValue } = require('./parsers')
7
+ const {
8
+ parsers,
9
+ programmaticTypeCoercions,
10
+ transformers,
11
+ telemetryTransformers,
12
+ setWarnInvalidValue,
13
+ } = require('./parsers')
7
14
  const applyMajorOverrides = require('./major-overrides')
8
15
  const {
9
16
  supportedConfigurations,
@@ -70,6 +77,7 @@ for (const [name, value] of Object.entries(defaults)) {
70
77
  function generateTelemetry (value = null, origin, optionName) {
71
78
  const tableEntry = configurationsTable[optionName]
72
79
  const { type, canonicalName = optionName } = tableEntry ?? { type: typeof value }
80
+ const error = parseErrors.get(`${canonicalName}${origin}`)
73
81
  // Sensitive configurations are excluded from configuration telemetry: their
74
82
  // entry is never added to `configWithOrigin`, the single source for every
75
83
  // telemetry path (app-started and app-client-configuration-change).
@@ -81,7 +89,9 @@ function generateTelemetry (value = null, origin, optionName) {
81
89
  // TODO: Validate that space separated tags are parsed by the backend. Optimizations would be possible with that.
82
90
  // TODO: How to handle telemetry reporting for aliases?
83
91
  if (value !== null) {
84
- if (telemetryTransformers[type]) {
92
+ if (error && typeof value === 'object') {
93
+ value = util.inspect(value, { customInspect: false, getters: false })
94
+ } else if (telemetryTransformers[type]) {
85
95
  value = telemetryTransformers[type](value)
86
96
  } else if (typeof value === 'object' && value !== null) {
87
97
  // Custom optionsTable entries (no `configurationsTable` row, e.g. `logger`)
@@ -100,7 +110,6 @@ function generateTelemetry (value = null, origin, optionName) {
100
110
  origin,
101
111
  seq_id: seqId++,
102
112
  }
103
- const error = parseErrors.get(`${canonicalName}${origin}`)
104
113
  if (error) {
105
114
  parseErrors.delete(`${canonicalName}${origin}`)
106
115
  telemetryEntry.error = error
@@ -135,12 +144,8 @@ function generateTelemetry (value = null, origin, optionName) {
135
144
  const optionsTable = {
136
145
  // Additional properties that are not supported by the supported-configurations.json file.
137
146
  lookup: {
138
- transformer (value) {
139
- if (typeof value === 'function') {
140
- return value
141
- }
142
- },
143
147
  property: 'lookup',
148
+ type: 'FUNCTION',
144
149
  },
145
150
  logger: {
146
151
  transformer (object) {
@@ -165,12 +170,15 @@ const optionsTable = {
165
170
  }
166
171
  },
167
172
  property: 'logger',
173
+ type: 'MAP',
168
174
  },
169
175
  isCiVisibility: {
170
176
  property: 'isCiVisibility',
177
+ type: 'BOOLEAN',
171
178
  },
172
179
  plugins: {
173
180
  property: 'plugins',
181
+ type: 'BOOLEAN',
174
182
  },
175
183
  }
176
184
 
@@ -185,13 +193,7 @@ const parser = (value, optionName, source) => {
185
193
 
186
194
  /**
187
195
  * @template {import('./config-types').ConfigPath} TPath
188
- * @type {Partial<Record<TPath, {
189
- * property?: string,
190
- * parser: (value: unknown, optionName: string, source: string) => unknown,
191
- * canonicalName?: string,
192
- * transformer?: (value: unknown, optionName: string, source: string) => unknown,
193
- * telemetryTransformer?: (value: unknown) => unknown
194
- * }>>} ConfigurationsTable
196
+ * @type {Partial<Record<TPath, import('./config-types').ConfigurationOption>>} ConfigurationsTable
195
197
  */
196
198
  const configurationsTable = {}
197
199
 
@@ -231,6 +233,7 @@ for (const [canonicalName, entries] of Object.entries(supportedConfigurations))
231
233
  ? `${entry.namespace}.${canonicalName}`
232
234
  : (entry.internalPropertyName ?? entry.configurationNames?.[0] ?? canonicalName)
233
235
  const type = entry.type.toUpperCase()
236
+ assert.ok(programmaticTypeCoercions[type])
234
237
 
235
238
  let transformer = transformers[entry.transform]
236
239
  if (entry.allowed) {
@@ -353,6 +356,8 @@ module.exports = {
353
356
  parseErrors,
354
357
 
355
358
  generateTelemetry,
359
+
360
+ warnInvalidValue,
356
361
  }
357
362
 
358
363
  // `dns` is instrumented, so requiring it pulls in the dns plugin, which loads
@@ -35,9 +35,10 @@ const {
35
35
  configWithOrigin,
36
36
  parseErrors,
37
37
  generateTelemetry,
38
+ warnInvalidValue,
38
39
  } = require('./defaults')
39
40
  const { normalizeService } = require('./normalize-service')
40
- const { transformers } = require('./parsers')
41
+ const { programmaticTypeCoercions, transformers } = require('./parsers')
41
42
 
42
43
  const RUNTIME_ID = uuid()
43
44
 
@@ -55,6 +56,7 @@ const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
55
56
  * @typedef {import('../../../../index').TracerOptions} TracerOptions
56
57
  * @typedef {import('./config-types').ConfigKey} ConfigKey
57
58
  * @typedef {import('./config-types').ConfigPath} ConfigPath
59
+ * @typedef {import('./config-types').ConfigurationOption} ConfigurationOption
58
60
  * @typedef {{
59
61
  * value: import('./config-types').ConfigPathValue<ConfigPath>,
60
62
  * source: TelemetrySource
@@ -121,6 +123,9 @@ function setAndTrack (config, name, value, rawValue = value, source = 'calculate
121
123
  if (value == null) {
122
124
  // TODO: This works as before while ignoring undefined programmatic options is not ideal.
123
125
  if (source !== 'default') {
126
+ if (rawValue !== value) {
127
+ generateTelemetry(rawValue, source, name)
128
+ }
124
129
  return
125
130
  }
126
131
  } else if (source === 'calculated' || source === 'remote_config') {
@@ -285,8 +290,13 @@ class Config extends ConfigBase {
285
290
  continue
286
291
  }
287
292
  }
288
- // TODO: Coerce mismatched types to the expected type, if possible. E.g., strings <> numbers
289
- const transformed = value !== undefined && entry.transformer ? entry.transformer(value, fullName, source) : value
293
+ const coerced = programmaticTypeCoercions[entry.type](value)
294
+ if (coerced === undefined && value !== undefined) {
295
+ warnInvalidValue(value, fullName, source, `Invalid ${entry.type} input`)
296
+ }
297
+ const transformed = coerced !== undefined && entry.transformer
298
+ ? entry.transformer(coerced, fullName, source)
299
+ : coerced
290
300
  setAndTrack(this, entry.property ?? name, transformed, value, source)
291
301
  }
292
302
  }