dd-trace 5.123.0 → 5.124.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/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
- package/index.d.ts +128 -26
- package/package.json +10 -10
- package/packages/datadog-instrumentations/src/bunyan.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/jest.js +140 -18
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
- package/packages/datadog-instrumentations/src/mariadb.js +7 -0
- package/packages/datadog-instrumentations/src/pino.js +15 -4
- package/packages/datadog-instrumentations/src/playwright.js +34 -20
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
- package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
- package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
- package/packages/datadog-instrumentations/src/winston.js +6 -3
- package/packages/datadog-plugin-cypress/src/support.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +2 -15
- package/packages/datadog-plugin-http2/src/client.js +2 -15
- package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
- package/packages/datadog-plugin-playwright/src/index.js +9 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -15
- package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
- package/packages/dd-trace/src/aiguard/noop.js +9 -1
- package/packages/dd-trace/src/aiguard/redaction.js +183 -0
- package/packages/dd-trace/src/aiguard/sdk.js +7 -2
- package/packages/dd-trace/src/aiguard/tags.js +2 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
- package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
- package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
- package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
- package/packages/dd-trace/src/config/index.js +2 -1
- package/packages/dd-trace/src/config/supported-configurations.json +27 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
- package/packages/dd-trace/src/dogstatsd.js +45 -18
- package/packages/dd-trace/src/encode/agentless-json.js +5 -4
- package/packages/dd-trace/src/exporters/agent/index.js +22 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
- package/packages/dd-trace/src/exporters/common/writer.js +24 -2
- package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
- package/packages/dd-trace/src/external-logger/src/index.js +4 -3
- package/packages/dd-trace/src/flush.js +102 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
- package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
- package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
- package/packages/dd-trace/src/llmobs/index.js +80 -17
- package/packages/dd-trace/src/llmobs/noop.js +2 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
- package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
- package/packages/dd-trace/src/noop/span.js +5 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
- package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
- package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
- package/packages/dd-trace/src/opentracing/span.js +28 -0
- package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
- package/packages/dd-trace/src/plugins/util/test.js +74 -21
- package/packages/dd-trace/src/plugins/util/web.js +2 -65
- package/packages/dd-trace/src/profiling/oom.js +4 -3
- package/packages/dd-trace/src/profiling/profiler.js +7 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
- package/packages/dd-trace/src/proxy.js +36 -8
- package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
- package/packages/dd-trace/src/serverless/vercel.js +122 -0
- package/packages/dd-trace/src/serverless.js +37 -20
- package/packages/dd-trace/src/span_stats.js +40 -3
- package/packages/dd-trace/src/tracer.js +22 -0
- package/packages/dd-trace/src/web-tags-cache.js +67 -13
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
|
@@ -26,6 +26,15 @@ const finishedRegistry = createRegistry('finished')
|
|
|
26
26
|
let OTEL_ENABLED = false
|
|
27
27
|
const ALLOWED = new Set(['string', 'number', 'boolean'])
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} RecordedException
|
|
31
|
+
* @property {string} message
|
|
32
|
+
* @property {string} [name]
|
|
33
|
+
* @property {string} [stack]
|
|
34
|
+
* @typedef {string | number | boolean} SpanEventAttributeScalar
|
|
35
|
+
* @typedef {SpanEventAttributeScalar | string[] | number[] | boolean[]} SpanEventAttributeValue
|
|
36
|
+
*/
|
|
37
|
+
|
|
29
38
|
const integrationCounters = {
|
|
30
39
|
spans_created: {},
|
|
31
40
|
spans_finished: {},
|
|
@@ -306,6 +315,25 @@ class DatadogSpan {
|
|
|
306
315
|
this._events.push(event)
|
|
307
316
|
}
|
|
308
317
|
|
|
318
|
+
/**
|
|
319
|
+
* @param {RecordedException} exception
|
|
320
|
+
* @param {Record<string, SpanEventAttributeValue>} [attributes]
|
|
321
|
+
*/
|
|
322
|
+
recordException (exception, attributes) {
|
|
323
|
+
const { message, name, stack } = exception
|
|
324
|
+
const eventAttributes = { ...attributes }
|
|
325
|
+
eventAttributes['exception.message'] ??= message
|
|
326
|
+
|
|
327
|
+
if (typeof name === 'string') {
|
|
328
|
+
eventAttributes['exception.type'] ??= name
|
|
329
|
+
}
|
|
330
|
+
if (typeof stack === 'string') {
|
|
331
|
+
eventAttributes['exception.stacktrace'] ??= stack
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.addEvent('exception', eventAttributes)
|
|
335
|
+
}
|
|
336
|
+
|
|
309
337
|
finish (finishTime) {
|
|
310
338
|
if (this._duration !== undefined) {
|
|
311
339
|
return
|
|
@@ -105,6 +105,10 @@ const THREAD_ID = String(threadId)
|
|
|
105
105
|
// time onEnter activates the span, and cleared on span finish, which
|
|
106
106
|
// is how a pendingEndpoints waiter recognizes a record it must no
|
|
107
107
|
// longer append to.
|
|
108
|
+
// webTags: the web-server tag bag the record's endpoint comes from, or is
|
|
109
|
+
// waiting on; undefined while the span has no web-server ancestry.
|
|
110
|
+
// Distinguishes a stale announcement from a live one when a nearer
|
|
111
|
+
// web-server span takes over.
|
|
108
112
|
const CachedSym = Symbol('OtelThreadCtx.cached')
|
|
109
113
|
|
|
110
114
|
let started = false
|
|
@@ -137,6 +141,7 @@ function getOrBuildContext (span) {
|
|
|
137
141
|
span[CachedSym] = cached
|
|
138
142
|
}
|
|
139
143
|
cached.context = new ThreadContext(traceId, spanId, attrs)
|
|
144
|
+
cached.webTags = webTags
|
|
140
145
|
if (endpoint === undefined) awaitEndpoint(webTags, cached)
|
|
141
146
|
return cached.context
|
|
142
147
|
}
|
|
@@ -227,21 +232,34 @@ function onEndpointResolved (span) {
|
|
|
227
232
|
if (endpoint === undefined) return
|
|
228
233
|
pendingEndpoints.delete(webTags)
|
|
229
234
|
for (const cached of waiting) {
|
|
230
|
-
|
|
235
|
+
// A record whose span has since been attributed to a nearer web-server span
|
|
236
|
+
// enlisted again under that bag, and this one is no longer its endpoint.
|
|
237
|
+
if (cached.context !== undefined && cached.webTags === webTags) appendEndpoint(cached.context, endpoint)
|
|
231
238
|
}
|
|
232
239
|
}
|
|
233
240
|
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
241
|
+
// webTagsCache has changed which request it attributes this span to: the span
|
|
242
|
+
// itself or an ancestor was recognized as a web-server span, giving a record
|
|
243
|
+
// built without any web-server ancestry its first endpoint, or a nearer
|
|
244
|
+
// web-server span superseded the one this record is showing. Give the record
|
|
245
|
+
// the new request's endpoint if it has settled, or wait for its announcement.
|
|
238
246
|
function onWebTagsResolved (span) {
|
|
239
247
|
if (!started) return
|
|
240
248
|
const cached = span[CachedSym]
|
|
241
249
|
if (cached === undefined || cached.context === undefined) return
|
|
242
250
|
const webTags = webTagsCache.getCachedWebTags(span)
|
|
251
|
+
if (webTags === cached.webTags) return
|
|
252
|
+
// Recorded so that an announcement for the bag this record was previously
|
|
253
|
+
// waiting on no longer applies to it.
|
|
254
|
+
cached.webTags = webTags
|
|
243
255
|
const endpoint = finalEndpoint(webTags)
|
|
244
256
|
if (endpoint === undefined) {
|
|
257
|
+
// A record that already shows the outer request's endpoint goes on showing
|
|
258
|
+
// it until the nearer one settles: the record buffer is append-only, so
|
|
259
|
+
// there is no way to take the attribute back, and rebuilding the
|
|
260
|
+
// ThreadContext would strand every async-context frame holding this one.
|
|
261
|
+
// The work is still nested in the outer request, which makes that the least
|
|
262
|
+
// wrong of the values available.
|
|
245
263
|
awaitEndpoint(webTags, cached)
|
|
246
264
|
} else {
|
|
247
265
|
appendEndpoint(cached.context, endpoint)
|
|
@@ -165,6 +165,7 @@ module.exports = class PluginManager {
|
|
|
165
165
|
codeOriginForSpans,
|
|
166
166
|
dbmPropagationMode,
|
|
167
167
|
dsmEnabled,
|
|
168
|
+
DD_TRACE_HTTP_CLIENT_ERROR_STATUSES,
|
|
168
169
|
DD_TRACE_HTTP_SERVER_ERROR_STATUSES,
|
|
169
170
|
clientIpEnabled,
|
|
170
171
|
clientIpHeader,
|
|
@@ -195,6 +196,7 @@ module.exports = class PluginManager {
|
|
|
195
196
|
site,
|
|
196
197
|
url,
|
|
197
198
|
headers: headerTags || [],
|
|
199
|
+
DD_TRACE_HTTP_CLIENT_ERROR_STATUSES,
|
|
198
200
|
DD_TRACE_HTTP_SERVER_ERROR_STATUSES,
|
|
199
201
|
clientIpHeader,
|
|
200
202
|
DD_TEST_SESSION_NAME,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../log')
|
|
4
|
+
|
|
5
|
+
const statusCodeRangesPattern = /^[1-5]\d{2}(?:-[1-5]\d{2})?(?:,[1-5]\d{2}(?:-[1-5]\d{2})?)*$/
|
|
6
|
+
const whitespacePattern = /\s/g
|
|
7
|
+
const MAX_HTTP_STATUS_CODE = 599
|
|
8
|
+
|
|
9
|
+
const SERVER_ERROR_STATUSES = 'DD_TRACE_HTTP_SERVER_ERROR_STATUSES'
|
|
10
|
+
const CLIENT_ERROR_STATUSES = 'DD_TRACE_HTTP_CLIENT_ERROR_STATUSES'
|
|
11
|
+
|
|
12
|
+
/** @typedef {(code: number) => boolean} StatusValidator */
|
|
13
|
+
|
|
14
|
+
/** @type {StatusValidator} */
|
|
15
|
+
function isNotServerErrorCode (code) {
|
|
16
|
+
return code < 500
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @type {StatusValidator} */
|
|
20
|
+
function isNotClientErrorCode (code) {
|
|
21
|
+
return code < 400 || code >= 500
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ validateStatus?: unknown, DD_TRACE_HTTP_SERVER_ERROR_STATUSES?: unknown }} config
|
|
26
|
+
* @returns {StatusValidator}
|
|
27
|
+
*/
|
|
28
|
+
function getServerStatusValidator (config) {
|
|
29
|
+
return getStatusValidator(config, SERVER_ERROR_STATUSES, '500-599', isNotServerErrorCode)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{ validateStatus?: unknown, DD_TRACE_HTTP_CLIENT_ERROR_STATUSES?: unknown }} config
|
|
34
|
+
* @returns {StatusValidator}
|
|
35
|
+
*/
|
|
36
|
+
function getClientStatusValidator (config) {
|
|
37
|
+
return getStatusValidator(config, CLIENT_ERROR_STATUSES, '400-499', isNotClientErrorCode)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Record<string, unknown>} config
|
|
42
|
+
* @param {string} optionName - Name of the configuration holding the error status ranges.
|
|
43
|
+
* @param {string} defaultRanges - Ranges covered by `defaultValidator`
|
|
44
|
+
* @param {StatusValidator} defaultValidator
|
|
45
|
+
* @returns {StatusValidator}
|
|
46
|
+
*/
|
|
47
|
+
function getStatusValidator (config, optionName, defaultRanges, defaultValidator) {
|
|
48
|
+
if (typeof config.validateStatus === 'function') {
|
|
49
|
+
return /** @type {StatusValidator} */ (config.validateStatus)
|
|
50
|
+
} else if (Object.hasOwn(config, 'validateStatus')) {
|
|
51
|
+
log.error('Expected `validateStatus` to be a function.')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ranges = config[optionName]
|
|
55
|
+
if (ranges === undefined) return defaultValidator
|
|
56
|
+
if (typeof ranges !== 'string') {
|
|
57
|
+
log.error('Expected `%s` to be a string.', optionName)
|
|
58
|
+
return defaultValidator
|
|
59
|
+
}
|
|
60
|
+
if (ranges === defaultRanges) return defaultValidator
|
|
61
|
+
|
|
62
|
+
const normalized = ranges.replaceAll(whitespacePattern, '')
|
|
63
|
+
if (normalized === defaultRanges) return defaultValidator
|
|
64
|
+
if (!statusCodeRangesPattern.test(normalized)) {
|
|
65
|
+
log.error('`%s` must contain comma-separated status codes or ranges from 100 to 599.', optionName)
|
|
66
|
+
return defaultValidator
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const errorStatusCodes = new Uint8Array(MAX_HTTP_STATUS_CODE + 1)
|
|
70
|
+
for (const range of normalized.split(',')) {
|
|
71
|
+
const separator = range.indexOf('-')
|
|
72
|
+
if (separator === -1) {
|
|
73
|
+
errorStatusCodes[Number(range)] = 1
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const first = Number(range.slice(0, separator))
|
|
78
|
+
const second = Number(range.slice(separator + 1))
|
|
79
|
+
const start = Math.min(first, second)
|
|
80
|
+
const end = Math.max(first, second)
|
|
81
|
+
errorStatusCodes.fill(1, start, end + 1)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {number} code
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function isValidStatusCode (code) {
|
|
89
|
+
return errorStatusCodes[code] !== 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return isValidStatusCode
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
getClientStatusValidator,
|
|
97
|
+
getServerStatusValidator,
|
|
98
|
+
}
|
|
@@ -541,6 +541,7 @@ module.exports = {
|
|
|
541
541
|
getRelativeCoverageFiles,
|
|
542
542
|
getLineCoverageBitmap,
|
|
543
543
|
applySkippedCoverageToCoverage,
|
|
544
|
+
getTestCoverageLinesData,
|
|
544
545
|
getTestCoverageLinesPercentage,
|
|
545
546
|
resetCoverage,
|
|
546
547
|
mergeCoverage,
|
|
@@ -1359,6 +1360,34 @@ function getLineCoverageBitmap (lineCoverage, onlyCoveredLines = false) {
|
|
|
1359
1360
|
return bitmap
|
|
1360
1361
|
}
|
|
1361
1362
|
|
|
1363
|
+
function getLineCoverageBitmaps (lineCoverage) {
|
|
1364
|
+
let maxLine = 0
|
|
1365
|
+
const entries = Object.entries(lineCoverage)
|
|
1366
|
+
|
|
1367
|
+
for (const [line] of entries) {
|
|
1368
|
+
const lineNumber = Number(line)
|
|
1369
|
+
if (Number.isSafeInteger(lineNumber) && lineNumber > maxLine) {
|
|
1370
|
+
maxLine = lineNumber
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (maxLine === 0) return {}
|
|
1374
|
+
|
|
1375
|
+
const length = Math.ceil((maxLine + 1) / 8)
|
|
1376
|
+
const coveredBitmap = Buffer.alloc(length)
|
|
1377
|
+
const executableBitmap = Buffer.alloc(length)
|
|
1378
|
+
for (const [line, hits] of entries) {
|
|
1379
|
+
const lineNumber = Number(line)
|
|
1380
|
+
if (!Number.isSafeInteger(lineNumber) || lineNumber <= 0) continue
|
|
1381
|
+
|
|
1382
|
+
const byteIndex = lineNumber >> 3
|
|
1383
|
+
const bit = 1 << (lineNumber % 8)
|
|
1384
|
+
executableBitmap[byteIndex] |= bit
|
|
1385
|
+
if (hits) coveredBitmap[byteIndex] |= bit
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return { coveredBitmap, executableBitmap }
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1362
1391
|
function mergeCoverageBitmaps (targetBitmap, bitmap) {
|
|
1363
1392
|
if (!targetBitmap) {
|
|
1364
1393
|
return Buffer.from(bitmap)
|
|
@@ -1419,16 +1448,6 @@ function getCoverageFileBitmap (bitmap) {
|
|
|
1419
1448
|
}
|
|
1420
1449
|
}
|
|
1421
1450
|
|
|
1422
|
-
function addCoverageFilesToMap (files, targetMap, rootDir) {
|
|
1423
|
-
for (const file of files) {
|
|
1424
|
-
const bitmap = getCoverageFileBitmap(file.bitmap)
|
|
1425
|
-
if (!bitmap) continue
|
|
1426
|
-
|
|
1427
|
-
const filename = rootDir ? getTestSuitePath(file.filename, rootDir) : file.filename
|
|
1428
|
-
targetMap.set(filename, mergeCoverageBitmaps(targetMap.get(filename), bitmap))
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
1451
|
function addSkippedCoverageToMap (skippedCoverage, targetMap) {
|
|
1433
1452
|
if (!skippedCoverage) return
|
|
1434
1453
|
|
|
@@ -1439,23 +1458,57 @@ function addSkippedCoverageToMap (skippedCoverage, targetMap) {
|
|
|
1439
1458
|
}
|
|
1440
1459
|
}
|
|
1441
1460
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1461
|
+
/**
|
|
1462
|
+
* Calculates line coverage and optionally returns executable-line coverage files in the same traversal.
|
|
1463
|
+
* @param {object} coverage
|
|
1464
|
+
* @param {object} [skippedCoverage]
|
|
1465
|
+
* @param {string} [rootDir]
|
|
1466
|
+
* @param {boolean} [includeExecutableFiles]
|
|
1467
|
+
* @returns {{ percentage: number, executableFiles?: Array<{ filename: string, bitmap: Buffer }> }}
|
|
1468
|
+
*/
|
|
1469
|
+
function getTestCoverageLinesData (coverage, skippedCoverage, rootDir, includeExecutableFiles = false) {
|
|
1470
|
+
const coverageMap = getCoverageMap(coverage)
|
|
1471
|
+
const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage)
|
|
1472
|
+
const coverageByFilename = new Map()
|
|
1473
|
+
const executableFiles = includeExecutableFiles ? [] : undefined
|
|
1474
|
+
|
|
1475
|
+
for (const filename of coverageMap.files()) {
|
|
1476
|
+
const fileCoverage = coverageMap.fileCoverageFor(filename)
|
|
1477
|
+
const { coveredBitmap, executableBitmap } = getLineCoverageBitmaps(fileCoverage.getLineCoverage())
|
|
1478
|
+
if (!executableBitmap) continue
|
|
1445
1479
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1480
|
+
const relativeFilename = rootDir ? getTestSuitePath(filename, rootDir) : filename
|
|
1481
|
+
const existingCoverage = coverageByFilename.get(relativeFilename)
|
|
1482
|
+
if (existingCoverage) {
|
|
1483
|
+
existingCoverage.coveredBitmap = mergeCoverageBitmaps(existingCoverage.coveredBitmap, coveredBitmap)
|
|
1484
|
+
existingCoverage.executableBitmap = mergeCoverageBitmaps(existingCoverage.executableBitmap, executableBitmap)
|
|
1485
|
+
} else {
|
|
1486
|
+
coverageByFilename.set(relativeFilename, { coveredBitmap, executableBitmap })
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1449
1489
|
|
|
1450
1490
|
let totalExecutableLines = 0
|
|
1451
1491
|
let totalCoveredLines = 0
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1492
|
+
for (const [filename, { coveredBitmap, executableBitmap }] of coverageByFilename) {
|
|
1493
|
+
const skippedBitmap = skippedCoverageByFilename.get(filename)
|
|
1494
|
+
const combinedCoveredBitmap = skippedBitmap
|
|
1495
|
+
? mergeCoverageBitmaps(coveredBitmap, skippedBitmap)
|
|
1496
|
+
: coveredBitmap
|
|
1497
|
+
totalExecutableLines += countBitmapBits(executableBitmap)
|
|
1498
|
+
totalCoveredLines += countCoveredExecutableBits(combinedCoveredBitmap, executableBitmap)
|
|
1499
|
+
if (executableFiles) {
|
|
1500
|
+
executableFiles.push({ filename, bitmap: executableBitmap })
|
|
1501
|
+
}
|
|
1456
1502
|
}
|
|
1457
1503
|
|
|
1458
|
-
|
|
1504
|
+
const percentage = totalExecutableLines === 0
|
|
1505
|
+
? 0
|
|
1506
|
+
: Math.floor((totalCoveredLines / totalExecutableLines) * 10_000) / 100
|
|
1507
|
+
return { percentage, executableFiles }
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function getTestCoverageLinesPercentage (coverage, skippedCoverage, rootDir) {
|
|
1511
|
+
return getTestCoverageLinesData(coverage, skippedCoverage, rootDir).percentage
|
|
1459
1512
|
}
|
|
1460
1513
|
|
|
1461
1514
|
function isLineCoveredByBitmap (bitmap, line) {
|
|
@@ -13,6 +13,7 @@ const { storage } = require('../../../../datadog-core')
|
|
|
13
13
|
const legacyStorage = storage('legacy')
|
|
14
14
|
const urlFilter = require('./urlfilter')
|
|
15
15
|
const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy')
|
|
16
|
+
const { getServerStatusValidator } = require('./status-validator')
|
|
16
17
|
const { extractURL, obfuscateQs, getQsObfuscator, calculateHttpEndpoint } = require('./url')
|
|
17
18
|
const { NETWORK_PEER_ADDRESS } = require('./http-otel-semantics')
|
|
18
19
|
|
|
@@ -35,11 +36,6 @@ const MANUAL_DROP = tags.MANUAL_DROP
|
|
|
35
36
|
|
|
36
37
|
const contexts = new WeakMap()
|
|
37
38
|
const requests = new WeakMap()
|
|
38
|
-
const statusCodeRangesPattern = /^[1-5]\d{2}(?:-[1-5]\d{2})?(?:,[1-5]\d{2}(?:-[1-5]\d{2})?)*$/
|
|
39
|
-
const whitespacePattern = /\s/g
|
|
40
|
-
const MAX_HTTP_STATUS_CODE = 599
|
|
41
|
-
|
|
42
|
-
/** @typedef {(code: number) => boolean} StatusValidator */
|
|
43
39
|
|
|
44
40
|
// TODO: change this to no longer rely on creating a dummy plugin to be able to access startSpan
|
|
45
41
|
function createWebPlugin (tracer, config = {}) {
|
|
@@ -65,7 +61,7 @@ const web = {
|
|
|
65
61
|
// Ensure the configuration has the correct structure and defaults.
|
|
66
62
|
normalizeConfig (config) {
|
|
67
63
|
const headers = getHeadersToRecord(config)
|
|
68
|
-
const validateStatus =
|
|
64
|
+
const validateStatus = getServerStatusValidator(config)
|
|
69
65
|
const hooks = getHooks(config)
|
|
70
66
|
const filter = urlFilter.getFilter(config)
|
|
71
67
|
const middleware = getMiddlewareSetting(config)
|
|
@@ -563,65 +559,6 @@ function getHeadersToRecord (config) {
|
|
|
563
559
|
return []
|
|
564
560
|
}
|
|
565
561
|
|
|
566
|
-
function isNot500ErrorCode (code) {
|
|
567
|
-
return code < 500
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* @param {{ validateStatus?: unknown, DD_TRACE_HTTP_SERVER_ERROR_STATUSES?: unknown }} config
|
|
572
|
-
* @returns {StatusValidator}
|
|
573
|
-
*/
|
|
574
|
-
function getStatusValidator (config) {
|
|
575
|
-
if (typeof config.validateStatus === 'function') {
|
|
576
|
-
return /** @type {StatusValidator} */ (config.validateStatus)
|
|
577
|
-
} else if (config.hasOwnProperty('validateStatus')) {
|
|
578
|
-
log.error('Expected `validateStatus` to be a function.')
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const { DD_TRACE_HTTP_SERVER_ERROR_STATUSES } = config
|
|
582
|
-
if (DD_TRACE_HTTP_SERVER_ERROR_STATUSES === undefined) return isNot500ErrorCode
|
|
583
|
-
if (typeof DD_TRACE_HTTP_SERVER_ERROR_STATUSES !== 'string') {
|
|
584
|
-
log.error('Expected `DD_TRACE_HTTP_SERVER_ERROR_STATUSES` to be a string.')
|
|
585
|
-
return isNot500ErrorCode
|
|
586
|
-
}
|
|
587
|
-
if (DD_TRACE_HTTP_SERVER_ERROR_STATUSES === '500-599') return isNot500ErrorCode
|
|
588
|
-
|
|
589
|
-
const normalized = DD_TRACE_HTTP_SERVER_ERROR_STATUSES.replaceAll(whitespacePattern, '')
|
|
590
|
-
if (normalized === '500-599') return isNot500ErrorCode
|
|
591
|
-
if (!statusCodeRangesPattern.test(normalized)) {
|
|
592
|
-
log.error(
|
|
593
|
-
'`DD_TRACE_HTTP_SERVER_ERROR_STATUSES` must contain comma-separated status codes or ranges from 100 to 599.'
|
|
594
|
-
)
|
|
595
|
-
return isNot500ErrorCode
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const errorStatusCodes = new Uint8Array(MAX_HTTP_STATUS_CODE + 1)
|
|
599
|
-
const ranges = normalized.split(',')
|
|
600
|
-
for (const range of ranges) {
|
|
601
|
-
const separator = range.indexOf('-')
|
|
602
|
-
if (separator === -1) {
|
|
603
|
-
errorStatusCodes[Number(range)] = 1
|
|
604
|
-
continue
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const first = Number(range.slice(0, separator))
|
|
608
|
-
const second = Number(range.slice(separator + 1))
|
|
609
|
-
const start = Math.min(first, second)
|
|
610
|
-
const end = Math.max(first, second)
|
|
611
|
-
errorStatusCodes.fill(1, start, end + 1)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* @param {number} code
|
|
616
|
-
* @returns {boolean}
|
|
617
|
-
*/
|
|
618
|
-
function isValidStatusCode (code) {
|
|
619
|
-
return errorStatusCodes[code] !== 1
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return isValidStatusCode
|
|
623
|
-
}
|
|
624
|
-
|
|
625
562
|
const noop = () => {}
|
|
626
563
|
|
|
627
564
|
function getHooks (config) {
|
|
@@ -56,16 +56,17 @@ function strategiesToCallbackMode (strategies, callbackMode) {
|
|
|
56
56
|
function buildExportCommand (exporters, tags) {
|
|
57
57
|
const tagString = [...Object.entries(tags),
|
|
58
58
|
['snapshot', snapshotKinds.ON_OUT_OF_MEMORY]].map(([key, value]) => `${key}:${value}`).join(',')
|
|
59
|
-
|
|
59
|
+
let urls = ''
|
|
60
60
|
for (const exporter of exporters) {
|
|
61
61
|
const url = exporter.getExportUrl()
|
|
62
62
|
if (url !== undefined) {
|
|
63
|
-
urls
|
|
63
|
+
if (urls) urls += ','
|
|
64
|
+
urls += url.toString()
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
return [process.execPath,
|
|
67
68
|
path.join(__dirname, 'exporter_cli.js'),
|
|
68
|
-
urls
|
|
69
|
+
urls, tagString, 'space']
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
module.exports = { oomExportStrategies, ensureOOMExportStrategies, strategiesToCallbackMode, buildExportCommand }
|
|
@@ -95,6 +95,10 @@ class Profiler extends EventEmitter {
|
|
|
95
95
|
for (const key of keys) {
|
|
96
96
|
this.#customLabelKeys.add(key)
|
|
97
97
|
}
|
|
98
|
+
this.#applyCustomLabelKeys()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#applyCustomLabelKeys () {
|
|
98
102
|
if (this.#profilers) {
|
|
99
103
|
for (const profiler of this.#profilers) {
|
|
100
104
|
profiler.setCustomLabelKeys?.(this.#customLabelKeys)
|
|
@@ -180,6 +184,9 @@ class Profiler extends EventEmitter {
|
|
|
180
184
|
this.#profilers = profilers
|
|
181
185
|
this.#uploadCompression = uploadCompression
|
|
182
186
|
this.#systemInfoReport = systemInfoReport
|
|
187
|
+
if (this.#customLabelKeys.size > 0) {
|
|
188
|
+
this.#applyCustomLabelKeys()
|
|
189
|
+
}
|
|
183
190
|
|
|
184
191
|
this._setInterval()
|
|
185
192
|
// Log errors if the source map finder fails, but don't prevent the rest
|
|
@@ -124,13 +124,13 @@ class GCDecorator {
|
|
|
124
124
|
}
|
|
125
125
|
let reasonLabel = this.reasonLabels[flags]
|
|
126
126
|
if (!reasonLabel) {
|
|
127
|
-
|
|
127
|
+
let reasonStr = ''
|
|
128
128
|
for (const [key, value] of Object.entries(this.flagObj)) {
|
|
129
129
|
if (value & flags) {
|
|
130
|
-
|
|
130
|
+
if (reasonStr) reasonStr += ','
|
|
131
|
+
reasonStr += key
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
const reasonStr = reasons.join(',')
|
|
134
134
|
reasonLabel = labelFromStr(this.stringTable, this.reasonLabelKey, reasonStr)
|
|
135
135
|
this.reasonLabels[flags] = reasonLabel
|
|
136
136
|
}
|
|
@@ -138,7 +138,7 @@ class NativeWallProfiler {
|
|
|
138
138
|
|
|
139
139
|
this.#pprof.time.start({
|
|
140
140
|
collectCpuTime: this.#cpuProfilingEnabled,
|
|
141
|
-
columnNumbers: '
|
|
141
|
+
columnNumbers: 'emit',
|
|
142
142
|
durationMillis: this.#flushIntervalMillis,
|
|
143
143
|
intervalMicros: this.#samplingIntervalMicros,
|
|
144
144
|
lineNumbers: false,
|
|
@@ -168,8 +168,8 @@ class NativeWallProfiler {
|
|
|
168
168
|
spanFinishCh.subscribe(this.#boundSpanFinished)
|
|
169
169
|
if (this.#endpointCollectionEnabled) {
|
|
170
170
|
// Web-tags cache publishes once per span at the moment its
|
|
171
|
-
// walk-result
|
|
172
|
-
//
|
|
171
|
+
// walk-result changes — exactly when we need to refresh the
|
|
172
|
+
// ProfilingContext snapshot.
|
|
173
173
|
webTagsCache.resolvedCh.subscribe(this.#boundSpanTagsUpdated)
|
|
174
174
|
// Turn on the cache's own tagsUpdate subscription — it's
|
|
175
175
|
// ref-counted, so this composes with any other active consumer.
|
|
@@ -229,13 +229,12 @@ class NativeWallProfiler {
|
|
|
229
229
|
} else if (current !== sampleContext) {
|
|
230
230
|
this.#pprof.time.setContext(sampleContext)
|
|
231
231
|
}
|
|
232
|
-
// Every setContext() call in ACF mode allocates a fresh contextHolder
|
|
233
|
-
// (a node::ObjectWrap with its own v8::Global<v8::Value>) in the native
|
|
234
|
-
// profiler. Skip the call if the CPED already holds this sampleContext,
|
|
235
|
-
// which is the common case when the same span is repeatedly activated:
|
|
236
|
-
// #getProfilingContext caches profilingContext on span[ProfilingContext],
|
|
237
|
-
// so identity comparison short-circuits.
|
|
238
232
|
} else if (current !== sampleContext) {
|
|
233
|
+
// Every setContext() call in ACF mode allocates a fresh contextHolder
|
|
234
|
+
// (a node::ObjectWrap with its own v8::Global<v8::Value>) in the native
|
|
235
|
+
// profiler. We're only incurring the cost when current !== sampleContext
|
|
236
|
+
// and skip it when current === sampleContext, which is the common case
|
|
237
|
+
// when the same span is repeatedly activated.
|
|
239
238
|
this.#pprof.time.setContext(sampleContext)
|
|
240
239
|
}
|
|
241
240
|
} else {
|
|
@@ -266,8 +265,8 @@ class NativeWallProfiler {
|
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
// webTags is snapshotted into the sample context at getProfilingContext
|
|
269
|
-
// time; if the answer
|
|
270
|
-
// web-server
|
|
268
|
+
// time; if the answer changes later — the span or an ancestor becomes a
|
|
269
|
+
// web-server span — #spanTagsUpdated refreshes this field via the shared
|
|
271
270
|
// cache (see web-tags-cache.js).
|
|
272
271
|
const webTags = this.#endpointCollectionEnabled ? webTagsCache.getCachedWebTags(span) : undefined
|
|
273
272
|
|
|
@@ -291,9 +290,10 @@ class NativeWallProfiler {
|
|
|
291
290
|
}
|
|
292
291
|
|
|
293
292
|
// Invoked (via webTagsCache.resolvedCh) once per span at the moment the
|
|
294
|
-
// shared cache
|
|
295
|
-
//
|
|
296
|
-
//
|
|
293
|
+
// shared cache changes its webTags answer: an empty one promoted into a real
|
|
294
|
+
// value, or an outer request's tag bag replaced by a nearer one. Refresh the
|
|
295
|
+
// ProfilingContext snapshot in place so samples already holding it — including
|
|
296
|
+
// those of a span that stays active across the promotion — pick it up.
|
|
297
297
|
#spanTagsUpdated (span) {
|
|
298
298
|
if (!this.#started) return
|
|
299
299
|
const profilingContext = span[ProfilingContext]
|