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.
- package/index.d.ts +25 -3
- package/package.json +4 -3
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- package/packages/datadog-instrumentations/src/cassandra-driver.js +5 -2
- package/packages/datadog-instrumentations/src/cucumber.js +103 -30
- package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
- package/packages/datadog-instrumentations/src/graphql.js +0 -5
- package/packages/datadog-instrumentations/src/grpc/client.js +48 -32
- package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/kafka.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +3 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +19 -5
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +14 -13
- package/packages/datadog-instrumentations/src/http/client.js +2 -2
- package/packages/datadog-instrumentations/src/ioredis.js +3 -3
- package/packages/datadog-instrumentations/src/jest.js +33 -36
- package/packages/datadog-instrumentations/src/kafkajs.js +25 -6
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/memcached.js +2 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +272 -91
- package/packages/datadog-instrumentations/src/mocha/utils.js +48 -8
- package/packages/datadog-instrumentations/src/mongodb-core.js +1 -1
- package/packages/datadog-instrumentations/src/mongoose.js +10 -12
- package/packages/datadog-instrumentations/src/mysql.js +2 -2
- package/packages/datadog-instrumentations/src/mysql2.js +1 -1
- package/packages/datadog-instrumentations/src/pg.js +1 -1
- package/packages/datadog-instrumentations/src/playwright.js +22 -5
- package/packages/datadog-instrumentations/src/router.js +4 -2
- package/packages/datadog-instrumentations/src/vitest.js +246 -149
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -19
- package/packages/datadog-plugin-elasticsearch/src/index.js +28 -8
- package/packages/datadog-plugin-graphql/src/utils.js +4 -1
- package/packages/datadog-plugin-kafkajs/src/producer.js +32 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +54 -19
- package/packages/datadog-plugin-redis/src/index.js +37 -2
- package/packages/datadog-plugin-undici/src/index.js +19 -0
- package/packages/datadog-plugin-vitest/src/index.js +19 -7
- package/packages/datadog-shimmer/src/shimmer.js +35 -0
- package/packages/dd-trace/src/appsec/blocking.js +2 -2
- package/packages/dd-trace/src/appsec/index.js +10 -3
- package/packages/dd-trace/src/appsec/reporter.js +19 -5
- package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -1
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +5 -3
- package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -0
- package/packages/dd-trace/src/config/supported-configurations.json +9 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +15 -3
- package/packages/dd-trace/src/datastreams/context.js +4 -2
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +26 -19
- package/packages/dd-trace/src/exporters/common/agents.js +3 -1
- package/packages/dd-trace/src/exporters/common/request.js +3 -1
- package/packages/dd-trace/src/id.js +17 -4
- package/packages/dd-trace/src/lambda/handler.js +2 -4
- package/packages/dd-trace/src/llmobs/sdk.js +10 -0
- package/packages/dd-trace/src/log/writer.js +3 -1
- package/packages/dd-trace/src/noop/span.js +3 -1
- package/packages/dd-trace/src/openfeature/writers/exposures.js +51 -20
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
- package/packages/dd-trace/src/plugins/apollo.js +3 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +3 -13
- package/packages/dd-trace/src/plugins/log_plugin.js +3 -1
- package/packages/dd-trace/src/plugins/tracing.js +5 -3
- package/packages/dd-trace/src/plugins/util/git.js +3 -1
- package/packages/dd-trace/src/plugins/util/test.js +82 -0
- package/packages/dd-trace/src/plugins/util/web.js +11 -0
- package/packages/dd-trace/src/scope.js +7 -5
- package/packages/dd-trace/src/service-naming/extra-services.js +14 -0
- package/vendor/dist/opentracing/LICENSE +0 -201
- package/vendor/dist/opentracing/binary_carrier.d.ts +0 -11
- package/vendor/dist/opentracing/constants.d.ts +0 -61
- package/vendor/dist/opentracing/examples/demo/demo.d.ts +0 -2
- package/vendor/dist/opentracing/ext/tags.d.ts +0 -90
- package/vendor/dist/opentracing/functions.d.ts +0 -20
- package/vendor/dist/opentracing/global_tracer.d.ts +0 -14
- package/vendor/dist/opentracing/index.d.ts +0 -12
- package/vendor/dist/opentracing/index.js +0 -1
- package/vendor/dist/opentracing/mock_tracer/index.d.ts +0 -5
- package/vendor/dist/opentracing/mock_tracer/mock_context.d.ts +0 -13
- package/vendor/dist/opentracing/mock_tracer/mock_report.d.ts +0 -16
- package/vendor/dist/opentracing/mock_tracer/mock_span.d.ts +0 -50
- package/vendor/dist/opentracing/mock_tracer/mock_tracer.d.ts +0 -26
- package/vendor/dist/opentracing/noop.d.ts +0 -8
- package/vendor/dist/opentracing/reference.d.ts +0 -33
- package/vendor/dist/opentracing/span.d.ts +0 -147
- package/vendor/dist/opentracing/span_context.d.ts +0 -26
- package/vendor/dist/opentracing/test/api_compatibility.d.ts +0 -16
- package/vendor/dist/opentracing/test/mocktracer_implemenation.d.ts +0 -3
- package/vendor/dist/opentracing/test/noop_implementation.d.ts +0 -4
- package/vendor/dist/opentracing/test/opentracing_api.d.ts +0 -3
- package/vendor/dist/opentracing/test/unittest.d.ts +0 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// -
|
|
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
|
-
// -
|
|
141
|
-
// -
|
|
142
|
-
|
|
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')
|
|
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')
|
|
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))
|
|
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
|
|
54
|
+
service,
|
|
31
55
|
type: this._spanType,
|
|
32
56
|
meta: {
|
|
33
57
|
'db.type': this._spanType,
|
|
34
58
|
'db.name': db || '0',
|
|
35
|
-
[
|
|
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
|
-
|
|
303
|
-
|
|
312
|
+
const testCommand = ctx.testCommand || 'vitest run'
|
|
313
|
+
const { testSessionId, testModuleId } = ctx
|
|
314
|
+
this.command = testCommand
|
|
304
315
|
this.frameworkVersion = frameworkVersion
|
|
305
|
-
const testSessionSpanContext =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
'
|
|
108
|
-
'
|
|
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
|
-
|
|
354
|
-
|
|
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(
|
|
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
|
-
?
|
|
178
|
-
: { ...storedResponseHeaders, ...
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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)
|
|
16
|
+
if (dataStreamsContext) legacyStorage.enterWith({ ...legacyStorage.getStore(), dataStreamsContext })
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
module.exports = {
|