dd-trace 5.103.0 → 5.104.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 (90) hide show
  1. package/index.d.ts +25 -3
  2. package/package.json +4 -3
  3. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  4. package/packages/datadog-instrumentations/src/cassandra-driver.js +5 -2
  5. package/packages/datadog-instrumentations/src/cucumber.js +103 -30
  6. package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
  7. package/packages/datadog-instrumentations/src/graphql.js +0 -5
  8. package/packages/datadog-instrumentations/src/grpc/client.js +48 -32
  9. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +1 -1
  10. package/packages/datadog-instrumentations/src/helpers/kafka.js +17 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +3 -2
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +19 -5
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +14 -13
  14. package/packages/datadog-instrumentations/src/http/client.js +2 -2
  15. package/packages/datadog-instrumentations/src/ioredis.js +3 -3
  16. package/packages/datadog-instrumentations/src/jest.js +33 -36
  17. package/packages/datadog-instrumentations/src/kafkajs.js +25 -6
  18. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  19. package/packages/datadog-instrumentations/src/memcached.js +2 -1
  20. package/packages/datadog-instrumentations/src/mocha/main.js +272 -91
  21. package/packages/datadog-instrumentations/src/mocha/utils.js +48 -8
  22. package/packages/datadog-instrumentations/src/mongodb-core.js +1 -1
  23. package/packages/datadog-instrumentations/src/mongoose.js +10 -12
  24. package/packages/datadog-instrumentations/src/mysql.js +2 -2
  25. package/packages/datadog-instrumentations/src/mysql2.js +1 -1
  26. package/packages/datadog-instrumentations/src/pg.js +1 -1
  27. package/packages/datadog-instrumentations/src/playwright.js +22 -5
  28. package/packages/datadog-instrumentations/src/router.js +4 -2
  29. package/packages/datadog-instrumentations/src/vitest.js +246 -149
  30. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -19
  31. package/packages/datadog-plugin-elasticsearch/src/index.js +28 -8
  32. package/packages/datadog-plugin-graphql/src/utils.js +4 -1
  33. package/packages/datadog-plugin-kafkajs/src/producer.js +32 -0
  34. package/packages/datadog-plugin-mongodb-core/src/index.js +54 -19
  35. package/packages/datadog-plugin-redis/src/index.js +37 -2
  36. package/packages/datadog-plugin-undici/src/index.js +19 -0
  37. package/packages/datadog-plugin-vitest/src/index.js +19 -7
  38. package/packages/datadog-shimmer/src/shimmer.js +35 -0
  39. package/packages/dd-trace/src/appsec/blocking.js +2 -2
  40. package/packages/dd-trace/src/appsec/index.js +10 -3
  41. package/packages/dd-trace/src/appsec/reporter.js +19 -5
  42. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -1
  43. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +5 -3
  44. package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -0
  45. package/packages/dd-trace/src/config/supported-configurations.json +9 -0
  46. package/packages/dd-trace/src/crashtracking/crashtracker.js +15 -3
  47. package/packages/dd-trace/src/datastreams/context.js +4 -2
  48. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +26 -19
  49. package/packages/dd-trace/src/exporters/common/agents.js +3 -1
  50. package/packages/dd-trace/src/exporters/common/request.js +3 -1
  51. package/packages/dd-trace/src/id.js +17 -4
  52. package/packages/dd-trace/src/lambda/handler.js +2 -4
  53. package/packages/dd-trace/src/llmobs/sdk.js +10 -0
  54. package/packages/dd-trace/src/log/writer.js +3 -1
  55. package/packages/dd-trace/src/noop/span.js +3 -1
  56. package/packages/dd-trace/src/openfeature/writers/exposures.js +51 -20
  57. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  58. package/packages/dd-trace/src/plugins/apollo.js +3 -1
  59. package/packages/dd-trace/src/plugins/ci_plugin.js +3 -13
  60. package/packages/dd-trace/src/plugins/log_plugin.js +3 -1
  61. package/packages/dd-trace/src/plugins/tracing.js +5 -3
  62. package/packages/dd-trace/src/plugins/util/git.js +3 -1
  63. package/packages/dd-trace/src/plugins/util/test.js +82 -0
  64. package/packages/dd-trace/src/plugins/util/web.js +11 -0
  65. package/packages/dd-trace/src/scope.js +7 -5
  66. package/packages/dd-trace/src/service-naming/extra-services.js +14 -0
  67. package/vendor/dist/opentracing/LICENSE +0 -201
  68. package/vendor/dist/opentracing/binary_carrier.d.ts +0 -11
  69. package/vendor/dist/opentracing/constants.d.ts +0 -61
  70. package/vendor/dist/opentracing/examples/demo/demo.d.ts +0 -2
  71. package/vendor/dist/opentracing/ext/tags.d.ts +0 -90
  72. package/vendor/dist/opentracing/functions.d.ts +0 -20
  73. package/vendor/dist/opentracing/global_tracer.d.ts +0 -14
  74. package/vendor/dist/opentracing/index.d.ts +0 -12
  75. package/vendor/dist/opentracing/index.js +0 -1
  76. package/vendor/dist/opentracing/mock_tracer/index.d.ts +0 -5
  77. package/vendor/dist/opentracing/mock_tracer/mock_context.d.ts +0 -13
  78. package/vendor/dist/opentracing/mock_tracer/mock_report.d.ts +0 -16
  79. package/vendor/dist/opentracing/mock_tracer/mock_span.d.ts +0 -50
  80. package/vendor/dist/opentracing/mock_tracer/mock_tracer.d.ts +0 -26
  81. package/vendor/dist/opentracing/noop.d.ts +0 -8
  82. package/vendor/dist/opentracing/reference.d.ts +0 -33
  83. package/vendor/dist/opentracing/span.d.ts +0 -147
  84. package/vendor/dist/opentracing/span_context.d.ts +0 -26
  85. package/vendor/dist/opentracing/test/api_compatibility.d.ts +0 -16
  86. package/vendor/dist/opentracing/test/mocktracer_implemenation.d.ts +0 -3
  87. package/vendor/dist/opentracing/test/noop_implementation.d.ts +0 -4
  88. package/vendor/dist/opentracing/test/opentracing_api.d.ts +0 -3
  89. package/vendor/dist/opentracing/test/unittest.d.ts +0 -2
  90. package/vendor/dist/opentracing/tracer.d.ts +0 -127
@@ -5,23 +5,43 @@ const DatabasePlugin = require('../../dd-trace/src/plugins/database')
5
5
  class ElasticsearchPlugin extends DatabasePlugin {
6
6
  static id = 'elasticsearch'
7
7
 
8
+ #urlTag
9
+ #methodTag
10
+ #bodyTag
11
+ #paramsTag
12
+
13
+ constructor (...args) {
14
+ super(...args)
15
+
16
+ // Per-instance because `system` differs on the OpenSearchPlugin subclass.
17
+ const { system } = this
18
+ this.#urlTag = `${system}.url`
19
+ this.#methodTag = `${system}.method`
20
+ this.#bodyTag = `${system}.body`
21
+ this.#paramsTag = `${system}.params`
22
+ }
23
+
8
24
  bindStart (ctx) {
9
25
  const { params } = ctx
10
26
 
11
- const body = getBody(params.body || params.bulkBody)
27
+ const meta = {
28
+ 'db.type': this.system,
29
+ [this.#urlTag]: params.path,
30
+ [this.#methodTag]: params.method,
31
+ [this.#bodyTag]: getBody(params.body || params.bulkBody),
32
+ }
33
+
34
+ const queryString = params.querystring || params.query
35
+ if (queryString) {
36
+ meta[this.#paramsTag] = JSON.stringify(queryString)
37
+ }
12
38
 
13
39
  this.startSpan(this.operationName(), {
14
40
  service: this.serviceName({ pluginConfig: this.config }),
15
41
  resource: `${params.method} ${quantizePath(params.path)}`,
16
42
  type: 'elasticsearch',
17
43
  kind: 'client',
18
- meta: {
19
- 'db.type': this.system,
20
- [`${this.system}.url`]: params.path,
21
- [`${this.system}.method`]: params.method,
22
- [`${this.system}.body`]: body,
23
- [`${this.system}.params`]: JSON.stringify(params.querystring || params.query),
24
- },
44
+ meta,
25
45
  }, ctx)
26
46
 
27
47
  return ctx.currentStore
@@ -7,7 +7,10 @@ function extractErrorIntoSpanEvent (config, span, exc) {
7
7
  attributes.type = exc.name
8
8
  }
9
9
 
10
- if (exc.stack) {
10
+ // graphql-js validation errors carry a lazy `.stack` accessor; reading it
11
+ // here is the only consumer in the pipeline and pays full V8 symbolisation.
12
+ const isValidationOnly = exc.locations && !exc.path && !exc.originalError?.stack
13
+ if (!isValidationOnly && exc.stack) {
11
14
  attributes.stacktrace = exc.stack
12
15
  }
13
16
 
@@ -91,6 +91,38 @@ class KafkajsProducerPlugin extends ProducerPlugin {
91
91
  }
92
92
  }
93
93
 
94
+ finish (ctx) {
95
+ const span = ctx?.currentStore?.span
96
+ const result = ctx?.result
97
+ if (span && Array.isArray(result) && result.length > 0) {
98
+ // The broker response is one entry per (topic, partition). Each entry
99
+ // carries a `baseOffset` — the offset assigned to the first record sent
100
+ // to that partition. We don't know per-partition record counts from the
101
+ // response, only the starting offset.
102
+ const offsets = []
103
+ for (const entry of result) {
104
+ const offsetAsLong = entry.offset ?? entry.baseOffset
105
+ if (entry.partition === undefined || offsetAsLong === undefined) continue
106
+ // Kafka offsets are 64-bit; coercing to Number loses precision past
107
+ // 2^53. Keep them as strings so the tag matches the exact offset on
108
+ // long-lived/high-throughput topics.
109
+ offsets.push({ partition: entry.partition, start_offset: String(offsetAsLong) })
110
+ }
111
+ if (offsets.length > 0) {
112
+ offsets.sort((a, b) => a.partition - b.partition)
113
+ span.setTag('kafka.messages.offsets', JSON.stringify(offsets))
114
+ }
115
+ // Single-message send: the one entry's partition/offset describes the
116
+ // exact record. Also expose them as flat tags for easy filtering.
117
+ if (offsets.length === 1 && ctx.messages?.length === 1) {
118
+ span.setTag('kafka.partition', offsets[0].partition)
119
+ // Set as a string meta tag (not a metric) to preserve full 64-bit precision.
120
+ span.setTag('kafka.message.offset', offsets[0].start_offset)
121
+ }
122
+ }
123
+ super.finish(ctx)
124
+ }
125
+
94
126
  bindStart (ctx) {
95
127
  const { topic, messages, bootstrapServers, clusterId, disableHeaderInjection } = ctx
96
128
  const span = this.startSpan({
@@ -20,6 +20,9 @@ class MongodbCorePlugin extends DatabasePlugin {
20
20
 
21
21
  this.config.heartbeatEnabled = config.heartbeatEnabled ??
22
22
  this._tracerConfig.DD_TRACE_MONGODB_HEARTBEAT_ENABLED
23
+ this.config.obfuscateQuery = normaliseObfuscateQuery(
24
+ config.obfuscateQuery ?? this._tracerConfig.DD_TRACE_MONGODB_OBFUSCATE_QUERY
25
+ )
23
26
  }
24
27
 
25
28
  bindStart (ctx) {
@@ -28,7 +31,7 @@ class MongodbCorePlugin extends DatabasePlugin {
28
31
  if (!this.config.heartbeatEnabled && isHeartbeat(ops, this.config)) {
29
32
  return
30
33
  }
31
- const query = getQuery(ops)
34
+ const query = getQuery(ops, this.config.obfuscateQuery)
32
35
  const resource = truncate(getResource(this, ns, query, name))
33
36
  const serviceResult = this.serviceName({ pluginConfig: this.config })
34
37
  const span = this.startSpan(this.operationName(), {
@@ -106,15 +109,19 @@ function extractQuery (statements) {
106
109
  return extractedQueries
107
110
  }
108
111
 
109
- function getQuery (cmd) {
112
+ /**
113
+ * @param {Record<string, unknown> | unknown[] | undefined} cmd
114
+ * @param {'none' | 'types' | 'redact'} mode
115
+ */
116
+ function getQuery (cmd, mode) {
110
117
  if (!cmd || (typeof cmd !== 'object' && !Array.isArray(cmd))) return
111
118
 
112
- if (Array.isArray(cmd)) return sanitiseAndStringify(extractQuery(cmd))
113
- if (cmd.query) return sanitiseAndStringify(cmd.query)
114
- if (cmd.filter) return sanitiseAndStringify(cmd.filter)
115
- if (cmd.pipeline) return sanitiseAndStringify(cmd.pipeline)
116
- if (cmd.deletes) return sanitiseAndStringify(extractQuery(cmd.deletes))
117
- if (cmd.updates) return sanitiseAndStringify(extractQuery(cmd.updates))
119
+ if (Array.isArray(cmd)) return sanitiseAndStringify(extractQuery(cmd), mode)
120
+ if (cmd.query) return sanitiseAndStringify(cmd.query, mode)
121
+ if (cmd.filter) return sanitiseAndStringify(cmd.filter, mode)
122
+ if (cmd.pipeline) return sanitiseAndStringify(cmd.pipeline, mode)
123
+ if (cmd.deletes) return sanitiseAndStringify(extractQuery(cmd.deletes), mode)
124
+ if (cmd.updates) return sanitiseAndStringify(extractQuery(cmd.updates), mode)
118
125
  }
119
126
 
120
127
  function getResource (plugin, ns, query, operationName) {
@@ -133,37 +140,65 @@ function truncate (input) {
133
140
 
134
141
  // Single-pass sanitisation. The replacer:
135
142
  // - skips functions and coerces bigint to its decimal string,
136
- // - returns '?' for Buffer / BSON Binary on the *original* value (JSON.stringify already invoked
137
- // toJSON before calling us; Buffer / Binary do have toJSON outputs we want to suppress),
143
+ // - collapses Buffer / BSON Binary / BSON types without toJSON (MinKey, MaxKey) to a sentinel,
138
144
  // - lets JSON.stringify call toJSON on other BSON types (ObjectId, Long, Decimal128, Date, Timestamp, ...)
139
145
  // so the result lands here as a primitive or plain object,
140
- // - returns '?' for BSON types without toJSON (MinKey, MaxKey) where `value === original`,
141
- // - tracks depth via an ancestor stack so cycles and depth >= MAX_DEPTH collapse to '?'.
142
- function sanitiseAndStringify (input) {
146
+ // - tracks depth via an ancestor stack so cycles and depth >= MAX_DEPTH collapse to the sentinel,
147
+ // - in `redact` mode, replaces every primitive leaf (including null) with '?',
148
+ // - in `types` mode, replaces every primitive leaf with the typeof of the *original* value (so a
149
+ // BSON Date that flattens to a string still reports as 'object'), and 'null' for null.
150
+ // Keys, operator names, and array / pipeline shape are preserved in both modes so the resulting
151
+ // JSON is still a usable query signature.
152
+ /**
153
+ * @param {Record<string, unknown> | unknown[]} input
154
+ * @param {'none' | 'types' | 'redact'} mode
155
+ */
156
+ function sanitiseAndStringify (input, mode) {
143
157
  const ancestors = []
144
158
  return JSON.stringify(input, function (key, value) {
145
159
  if (typeof value === 'function') return
146
- if (typeof value === 'bigint') return value.toString()
160
+ if (typeof value === 'bigint') {
161
+ if (mode === 'redact') return '?'
162
+ if (mode === 'types') return 'bigint'
163
+ return value.toString()
164
+ }
147
165
 
148
166
  const original = key === '' ? value : this[key]
149
167
  if (typeof original === 'object' && original !== null) {
150
- if (Buffer.isBuffer(original)) return '?'
151
168
  const bsontype = original._bsontype
152
- if (bsontype !== undefined && (bsontype === 'Binary' || value === original)) {
153
- return '?'
169
+ if (Buffer.isBuffer(original) || (bsontype !== undefined && (bsontype === 'Binary' || value === original))) {
170
+ return mode === 'types' ? 'object' : '?'
154
171
  }
155
172
  }
156
173
 
157
- if (value === null || typeof value !== 'object') return value
174
+ if (value === null || typeof value !== 'object') {
175
+ if (key === '' || mode === 'none') return value
176
+ if (mode === 'redact') return '?'
177
+ return original === null ? 'null' : typeof original
178
+ }
158
179
 
159
180
  while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop()
160
- if (ancestors.length >= MAX_DEPTH || ancestors.includes(value)) return '?'
181
+ if (ancestors.length >= MAX_DEPTH || ancestors.includes(value)) {
182
+ return mode === 'types' ? 'object' : '?'
183
+ }
161
184
  ancestors.push(value)
162
185
 
163
186
  return value
164
187
  })
165
188
  }
166
189
 
190
+ /**
191
+ * Coerce the plugin-config and env values for `obfuscateQuery` to one of the three canonical modes.
192
+ * Anything outside the enum — including `undefined` — falls back to `'none'`.
193
+ *
194
+ * @param {unknown} value
195
+ * @returns {'none' | 'types' | 'redact'}
196
+ */
197
+ function normaliseObfuscateQuery (value) {
198
+ if (value === 'types' || value === 'redact') return value
199
+ return 'none'
200
+ }
201
+
167
202
  function isHeartbeat (ops, config) {
168
203
  // Check if it's a heartbeat command https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.md
169
204
  return (
@@ -11,6 +11,17 @@ class RedisPlugin extends CachePlugin {
11
11
  static id = 'redis'
12
12
  static system = 'redis'
13
13
 
14
+ /** @type {string} */
15
+ #rawCommandKey
16
+ /** @type {Map<string | undefined, { name: string, source: string | undefined }>} */
17
+ #serviceByConnection = new Map()
18
+ // `nomenclature.config` identity at last cache fill. `withNamingSchema` swaps it without
19
+ // running this plugin's `configure`, so identity drives invalidation in `bindStart`.
20
+ /** @type {object | undefined} */
21
+ #lastNomenclatureConfig
22
+ /** @type {string | undefined} */
23
+ #cachedOperationName
24
+
14
25
  constructor (...args) {
15
26
  super(...args)
16
27
  this._spanType = 'redis'
@@ -25,14 +36,27 @@ class RedisPlugin extends CachePlugin {
25
36
  return { noop: true }
26
37
  }
27
38
 
39
+ const nomConfig = this._tracer._nomenclature.config
40
+ if (this.#lastNomenclatureConfig !== nomConfig) {
41
+ this.#lastNomenclatureConfig = nomConfig
42
+ this.#cachedOperationName = undefined
43
+ this.#serviceByConnection.clear()
44
+ }
45
+
46
+ let service = this.#serviceByConnection.get(connectionName)
47
+ if (service === undefined) {
48
+ service = this.serviceName({ pluginConfig: this.config, system: this.system, connectionName })
49
+ this.#serviceByConnection.set(connectionName, service)
50
+ }
51
+
28
52
  this.startSpan({
29
53
  resource,
30
- service: this.serviceName({ pluginConfig: this.config, system: this.system, connectionName }),
54
+ service,
31
55
  type: this._spanType,
32
56
  meta: {
33
57
  'db.type': this._spanType,
34
58
  'db.name': db || '0',
35
- [`${this._spanType}.raw_command`]: formatCommand(normalizedCommand, args, argsStartIndex),
59
+ [this.#rawCommandKey]: formatCommand(normalizedCommand, args, argsStartIndex),
36
60
  'out.host': connectionOptions.host,
37
61
  [CLIENT_PORT_KEY]: connectionOptions.port,
38
62
  },
@@ -41,8 +65,19 @@ class RedisPlugin extends CachePlugin {
41
65
  return ctx.currentStore
42
66
  }
43
67
 
68
+ operationName () {
69
+ this.#cachedOperationName ??= super.operationName()
70
+ return this.#cachedOperationName
71
+ }
72
+
44
73
  configure (config) {
45
74
  super.configure(normalizeConfig(config))
75
+ // Subclasses (iovalkey) overwrite `_spanType` in their constructor, before any `configure`,
76
+ // so reading it here is stable.
77
+ this.#rawCommandKey = `${this._spanType}.raw_command`
78
+ this.#lastNomenclatureConfig = undefined
79
+ this.#cachedOperationName = undefined
80
+ this.#serviceByConnection.clear()
46
81
  }
47
82
  }
48
83
 
@@ -27,6 +27,7 @@ class UndiciPlugin extends HttpClientPlugin {
27
27
  // Subscribe to native undici diagnostic channels for undici >= 4.7.0
28
28
  // These channels fire for ALL undici requests (fetch, request, stream, etc.)
29
29
  this.addSub('undici:request:create', this.#onNativeRequestCreate.bind(this))
30
+ this.addSub('undici:request:bodySent', this.#onNativeRequestBodySent.bind(this))
30
31
  this.addSub('undici:request:headers', this.#onNativeRequestHeaders.bind(this))
31
32
  this.addSub('undici:request:trailers', this.#onNativeRequestTrailers.bind(this))
32
33
  this.addSub('undici:request:error', this.#onNativeRequestError.bind(this))
@@ -109,12 +110,30 @@ class UndiciPlugin extends HttpClientPlugin {
109
110
  span,
110
111
  store,
111
112
  uri,
113
+ method,
112
114
  })
113
115
 
114
116
  // Enter the span context
115
117
  storage('legacy').enterWith({ ...store, span })
116
118
  }
117
119
 
120
+ #onNativeRequestBodySent ({ request }) {
121
+ const ctx = requestContexts.get(request)
122
+ if (!ctx || ctx.method !== 'CONNECT') return
123
+
124
+ const { span, store } = ctx
125
+
126
+ this.config.hooks.request(span, null, null)
127
+
128
+ span.finish()
129
+
130
+ requestContexts.delete(request)
131
+
132
+ if (store) {
133
+ storage('legacy').enterWith(store)
134
+ }
135
+ }
136
+
118
137
  #onNativeRequestHeaders ({ request, response }) {
119
138
  const ctx = requestContexts.get(request)
120
139
  if (!ctx) return
@@ -56,6 +56,16 @@ class VitestPlugin extends CiPlugin {
56
56
 
57
57
  this.taskToFinishTime = new WeakMap()
58
58
 
59
+ this.addSub('ci:vitest:session:configuration', ({ onDone }) => {
60
+ const testSessionSpanContext = this.testSessionSpan?.context()
61
+ const testModuleSpanContext = this.testModuleSpan?.context()
62
+ onDone({
63
+ testSessionId: testSessionSpanContext?.toTraceId(),
64
+ testModuleId: testModuleSpanContext?.toSpanId(),
65
+ testCommand: this.command,
66
+ })
67
+ })
68
+
59
69
  this.addSub('ci:vitest:test:is-new', ({ knownTests, testSuiteAbsolutePath, testName, onDone }) => {
60
70
  // if for whatever reason the worker does not receive valid known tests, we don't consider it as new
61
71
  if (!knownTests.vitest) {
@@ -299,14 +309,16 @@ class VitestPlugin extends CiPlugin {
299
309
  this.addBind('ci:vitest:test-suite:start', (ctx) => {
300
310
  const { testSuiteAbsolutePath, frameworkVersion } = ctx
301
311
 
302
- // TODO: Handle case where the command is not set
303
- this.command = this._tracerConfig.DD_CIVISIBILITY_TEST_COMMAND
312
+ const testCommand = ctx.testCommand || 'vitest run'
313
+ const { testSessionId, testModuleId } = ctx
314
+ this.command = testCommand
304
315
  this.frameworkVersion = frameworkVersion
305
- const testSessionSpanContext = this.tracer.extract('text_map', {
306
- // TODO: Handle case where the session ID or module ID is not set
307
- 'x-datadog-trace-id': this._tracerConfig.DD_CIVISIBILITY_TEST_SESSION_ID,
308
- 'x-datadog-parent-id': this._tracerConfig.DD_CIVISIBILITY_TEST_MODULE_ID,
309
- })
316
+ const testSessionSpanContext = testSessionId && testModuleId
317
+ ? this.tracer.extract('text_map', {
318
+ 'x-datadog-trace-id': testSessionId,
319
+ 'x-datadog-parent-id': testModuleId,
320
+ })
321
+ : undefined
310
322
 
311
323
  const trimmedCommand = DD_MAJOR < 6 ? this.command : 'vitest run'
312
324
  // test suites run in a different process, so they also need to init the metadata dictionary
@@ -82,6 +82,40 @@ function wrapFunction (original, wrapper) {
82
82
  return wrapped
83
83
  }
84
84
 
85
+ /**
86
+ * Lean variant of `wrapFunction` for the case where the wrapped value is a
87
+ * user-supplied callback that the user cannot reasonably introspect beyond
88
+ * `name` and `length`, and the wrapper closure is fully controlled by us.
89
+ *
90
+ * Compared to `wrapFunction`, this skips the prototype copy, the
91
+ * `assertNotClass` guard, and the `Reflect.ownKeys` descriptor-copy loop.
92
+ * Only `name` and `length` are preserved, and only when the wrapper's
93
+ * autogenerated values differ -- a wrapper whose closure already has the
94
+ * right arity / name pays no overhead.
95
+ *
96
+ * Use `wrapFunction` instead when any of the following is true: the wrapped
97
+ * function needs to keep its prototype, has custom own properties the caller
98
+ * may read, or is `new`-ed.
99
+ *
100
+ * @param {Function} original - User-supplied callback being wrapped.
101
+ * @param {(original: Function) => Function} wrapper - Factory that receives
102
+ * `original` and returns the wrapper closure.
103
+ * @returns {Function} The wrapper closure with `name` and `length` preserved.
104
+ */
105
+ function wrapCallback (original, wrapper) {
106
+ if (typeof original !== 'function') {
107
+ return original
108
+ }
109
+ const wrapped = wrapper(original)
110
+ if (wrapped.name !== original.name) {
111
+ Object.defineProperty(wrapped, 'name', { value: original.name, configurable: true })
112
+ }
113
+ if (wrapped.length !== original.length) {
114
+ Object.defineProperty(wrapped, 'length', { value: original.length, configurable: true })
115
+ }
116
+ return wrapped
117
+ }
118
+
85
119
  /**
86
120
  * Wraps a method of an object with a wrapper function.
87
121
  *
@@ -280,6 +314,7 @@ function assertNotClass (target) {
280
314
 
281
315
  module.exports = {
282
316
  wrap,
317
+ wrapCallback,
283
318
  wrapFunction,
284
319
  massWrap,
285
320
  }
@@ -104,8 +104,8 @@ function getBlockWithContentData (req, specificType, actionParameters) {
104
104
  const statusCode = actionParameters?.status_code || 403
105
105
 
106
106
  const headers = {
107
- 'Content-Type': type,
108
- 'Content-Length': Buffer.byteLength(body),
107
+ 'content-type': type,
108
+ 'content-length': Buffer.byteLength(body),
109
109
  }
110
110
 
111
111
  return { body, statusCode, headers }
@@ -350,8 +350,15 @@ function onResponseBody ({ req, res, body }) {
350
350
  }
351
351
 
352
352
  function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
353
- if (!isEmpty(responseHeaders)) {
354
- storedResponseHeaders.set(req, responseHeaders)
353
+ // Normalize header names to lowercase so downstream consumers see the same shape
354
+ // regardless of how the caller wrote them.
355
+ const normalizedResponseHeaders = {}
356
+ for (const [key, value] of Object.entries(responseHeaders)) {
357
+ normalizedResponseHeaders[key.toLowerCase()] = value
358
+ }
359
+
360
+ if (!isEmpty(normalizedResponseHeaders)) {
361
+ storedResponseHeaders.set(req, normalizedResponseHeaders)
355
362
  }
356
363
 
357
364
  // TODO: do not call waf if inside block()
@@ -376,7 +383,7 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH
376
383
  const results = waf.run({
377
384
  persistent: {
378
385
  [addresses.HTTP_INCOMING_RESPONSE_CODE]: String(statusCode),
379
- [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: copyHeadersOmitting(responseHeaders, 'set-cookie'),
386
+ [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: copyHeadersOmitting(normalizedResponseHeaders, 'set-cookie'),
380
387
  },
381
388
  }, req)
382
389
 
@@ -50,6 +50,11 @@ const contentHeaderList = [
50
50
  'content-language',
51
51
  ]
52
52
 
53
+ const mandatoryResponseHeaderList = [
54
+ 'content-type',
55
+ 'content-length',
56
+ ]
57
+
53
58
  const responseHeaderList = [
54
59
  ...contentHeaderList,
55
60
  'content-type',
@@ -102,6 +107,8 @@ const EVENT_HEADERS_MAP = mapHeaderAndTags(eventHeadersList, REQUEST_HEADER_TAG_
102
107
 
103
108
  const RESPONSE_HEADERS_MAP = mapHeaderAndTags(responseHeaderList, RESPONSE_HEADER_TAG_PREFIX)
104
109
 
110
+ const MANDATORY_RESPONSE_HEADERS_MAP = mapHeaderAndTags(mandatoryResponseHeaderList, RESPONSE_HEADER_TAG_PREFIX)
111
+
105
112
  const NON_EXTENDED_REQUEST_HEADERS = new Set([...requestHeadersList, ...eventHeadersList])
106
113
  const NON_EXTENDED_RESPONSE_HEADERS = new Set(responseHeaderList)
107
114
  const REDACTED_HEADERS = new Set(redactedHeadersList)
@@ -168,14 +175,21 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
168
175
  // Mandatory
169
176
  const mandatoryCollectedHeaders = filterHeaders(req.headers, REQUEST_HEADERS_MAP)
170
177
 
171
- // Basic collection
172
- if (!shouldCollectEventHeaders) return mandatoryCollectedHeaders
173
-
174
178
  // Skip the spread when the stored side is empty -- common during the early
175
179
  // request lifecycle when no upstream response headers have been captured.
180
+ const liveResponseHeaders = res?.getHeaders?.()
176
181
  const responseHeaders = isEmpty(storedResponseHeaders)
177
- ? res.getHeaders()
178
- : { ...storedResponseHeaders, ...res.getHeaders() }
182
+ ? (liveResponseHeaders ?? {})
183
+ : (liveResponseHeaders ? { ...storedResponseHeaders, ...liveResponseHeaders } : storedResponseHeaders)
184
+
185
+ // content-type and content-length are always reported when appsec is enabled,
186
+ // even without a security event.
187
+ if (!shouldCollectEventHeaders) {
188
+ return Object.assign(
189
+ mandatoryCollectedHeaders,
190
+ filterHeaders(responseHeaders, MANDATORY_RESPONSE_HEADERS_MAP)
191
+ )
192
+ }
179
193
 
180
194
  const requestEventCollectedHeaders = filterHeaders(req.headers, EVENT_HEADERS_MAP)
181
195
  const responseEventCollectedHeaders = filterHeaders(responseHeaders, RESPONSE_HEADERS_MAP)
@@ -14,6 +14,8 @@ const {
14
14
  } = require('../../exporters/common/retry')
15
15
  const { urlToHttpOptions } = require('../../exporters/common/url-to-http-options-polyfill')
16
16
 
17
+ const legacyStorage = storage('legacy')
18
+
17
19
  function parseUrl (urlObjOrString) {
18
20
  if (urlObjOrString !== null && typeof urlObjOrString === 'object') {
19
21
  return urlToHttpOptions(urlObjOrString)
@@ -75,7 +77,7 @@ function request (data, options, callback) {
75
77
  let firstStatusCode = null
76
78
 
77
79
  const makeRequest = () => {
78
- storage('legacy').run({ noop: true }, () => {
80
+ legacyStorage.run({ noop: true }, () => {
79
81
  const req = client.request(opts, (res) => {
80
82
  // Capture non-2xx status code as soon as we see it so telemetry preserves it if the retry
81
83
  // fails with a network error (no HTTP response) before 'end' fires
@@ -8,6 +8,8 @@ const {
8
8
  } = require('../../plugins/util/test')
9
9
  const { storage } = require('../../../../datadog-core')
10
10
 
11
+ const legacyStorage = storage('legacy')
12
+
11
13
  class TestApiManualPlugin extends CiPlugin {
12
14
  static id = 'test-api-manual'
13
15
 
@@ -17,13 +19,13 @@ class TestApiManualPlugin extends CiPlugin {
17
19
  this.sourceRoot = process.cwd()
18
20
 
19
21
  this.unconfiguredAddSub('dd-trace:ci:manual:test:start', ({ testName, testSuite }) => {
20
- const store = storage('legacy').getStore()
22
+ const store = legacyStorage.getStore()
21
23
  const testSuiteRelative = getTestSuitePath(testSuite, this.sourceRoot)
22
24
  const testSpan = this.startTestSpan(testName, testSuiteRelative)
23
25
  this.enter(testSpan, store)
24
26
  })
25
27
  this.unconfiguredAddSub('dd-trace:ci:manual:test:finish', ({ status, error }) => {
26
- const store = storage('legacy').getStore()
28
+ const store = legacyStorage.getStore()
27
29
  const testSpan = store && store.span
28
30
  if (testSpan) {
29
31
  testSpan.setTag(TEST_STATUS, status)
@@ -35,7 +37,7 @@ class TestApiManualPlugin extends CiPlugin {
35
37
  }
36
38
  })
37
39
  this.unconfiguredAddSub('dd-trace:ci:manual:test:addTags', (tags) => {
38
- const store = storage('legacy').getStore()
40
+ const store = legacyStorage.getStore()
39
41
  const testSpan = store && store.span
40
42
  if (testSpan) {
41
43
  testSpan.addTags(tags)
@@ -324,6 +324,7 @@ export interface GeneratedConfig {
324
324
  DD_TRACE_MONGODB_CORE_ENABLED: boolean;
325
325
  DD_TRACE_MONGODB_ENABLED: boolean;
326
326
  DD_TRACE_MONGODB_HEARTBEAT_ENABLED: boolean;
327
+ DD_TRACE_MONGODB_OBFUSCATE_QUERY: "none" | "types" | "redact";
327
328
  DD_TRACE_MONGOOSE_ENABLED: boolean;
328
329
  DD_TRACE_MQUERY_ENABLED: boolean;
329
330
  DD_TRACE_MULTER_ENABLED: boolean;
@@ -3133,6 +3133,15 @@
3133
3133
  "default": "true"
3134
3134
  }
3135
3135
  ],
3136
+ "DD_TRACE_MONGODB_OBFUSCATE_QUERY": [
3137
+ {
3138
+ "implementation": "A",
3139
+ "type": "string",
3140
+ "default": "none",
3141
+ "allowed": "none|types|redact",
3142
+ "transform": "toLowerCase"
3143
+ }
3144
+ ],
3136
3145
  "DD_TRACE_MONGOOSE_ENABLED": [
3137
3146
  {
3138
3147
  "implementation": "A",
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { EOL } = require('node:os')
4
+
3
5
  // Load binding first to not import other modules if it throws
4
6
  const libdatadog = require('@datadog/libdatadog')
5
7
  const binding = libdatadog.load('crashtracker')
@@ -29,19 +31,29 @@ class Crashtracker {
29
31
  start (config) {
30
32
  if (this.#started) return this.configure(config)
31
33
 
32
- this.#started = true
33
-
34
34
  try {
35
35
  binding.init(
36
36
  this.#getConfig(config),
37
37
  this.#getReceiverConfig(),
38
38
  this.#getMetadata(config)
39
39
  )
40
+ this.#started = true
41
+ this.#trackUnhandledExceptions()
40
42
  } catch (e) {
41
43
  log.error('Error initializing crashtracker', e)
42
44
  }
43
45
  }
44
46
 
47
+ #trackUnhandledExceptions () {
48
+ process.once('uncaughtExceptionMonitor', (error, origin) => {
49
+ try {
50
+ binding.reportUncaughtExceptionMonitor(error, origin)
51
+ } catch (e) {
52
+ process.stderr.write(`Error reporting uncaught exception to crashtracker: ${e.toString()}${EOL}`)
53
+ }
54
+ })
55
+ }
56
+
45
57
  withProfilerSerializing (f) {
46
58
  binding.beginProfilerSerializing()
47
59
  try {
@@ -58,7 +70,7 @@ class Crashtracker {
58
70
  #getConfig (config) {
59
71
  const url = getAgentUrl(config)
60
72
 
61
- // Out-of-process symbolication currently (crashtracker 27.0.0) works on
73
+ // Out-of-process symbolication currently works on
62
74
  // Linux only, does not work on Mac.
63
75
  const resolveMode = require('os').platform === 'linux'
64
76
  ? 'EnabledWithSymbolsInReceiver'
@@ -3,15 +3,17 @@
3
3
  const { storage } = require('../../../datadog-core')
4
4
  const log = require('../log')
5
5
 
6
+ const legacyStorage = storage('legacy')
7
+
6
8
  function getDataStreamsContext () {
7
- const store = storage('legacy').getStore()
9
+ const store = legacyStorage.getStore()
8
10
  return (store && store.dataStreamsContext) || null
9
11
  }
10
12
 
11
13
  function setDataStreamsContext (dataStreamsContext) {
12
14
  log.debug('Setting new DSM Context: %j.', dataStreamsContext)
13
15
 
14
- if (dataStreamsContext) storage('legacy').enterWith({ ...(storage('legacy').getStore()), dataStreamsContext })
16
+ if (dataStreamsContext) legacyStorage.enterWith({ ...legacyStorage.getStore(), dataStreamsContext })
15
17
  }
16
18
 
17
19
  module.exports = {