dd-trace 4.41.0 → 4.43.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/LICENSE-3rdparty.csv +1 -2
- package/ext/exporters.d.ts +1 -1
- package/index.d.ts +105 -37
- package/init.js +40 -1
- package/initialize.mjs +8 -5
- package/package.json +29 -29
- package/packages/datadog-core/src/storage/index.js +1 -10
- package/packages/datadog-esbuild/index.js +5 -1
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
- package/packages/datadog-instrumentations/src/child_process.js +2 -2
- package/packages/datadog-instrumentations/src/cucumber.js +76 -34
- package/packages/datadog-instrumentations/src/fs.js +1 -1
- package/packages/datadog-instrumentations/src/hapi.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
- package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
- package/packages/datadog-instrumentations/src/http/client.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +17 -2
- package/packages/datadog-instrumentations/src/kafkajs.js +1 -1
- package/packages/datadog-instrumentations/src/ldapjs.js +2 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
- package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
- package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/next.js +1 -1
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +47 -33
- package/packages/datadog-instrumentations/src/restify.js +1 -1
- package/packages/datadog-instrumentations/src/vitest.js +349 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
- package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
- package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
- package/packages/datadog-plugin-child_process/src/index.js +1 -1
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -4
- package/packages/datadog-plugin-cucumber/src/index.js +24 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +79 -42
- package/packages/datadog-plugin-cypress/src/plugin.js +4 -3
- package/packages/datadog-plugin-fs/src/index.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +7 -1
- package/packages/datadog-plugin-kafkajs/src/producer.js +1 -1
- package/packages/datadog-plugin-mocha/src/index.js +25 -4
- package/packages/datadog-plugin-mongodb-core/src/index.js +1 -1
- package/packages/datadog-plugin-openai/src/index.js +57 -35
- package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
- package/packages/datadog-plugin-playwright/src/index.js +4 -1
- package/packages/datadog-plugin-sharedb/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +167 -0
- package/packages/dd-trace/src/analytics_sampler.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
- package/packages/dd-trace/src/appsec/index.js +4 -4
- package/packages/dd-trace/src/appsec/passport.js +1 -1
- package/packages/dd-trace/src/appsec/rasp.js +32 -5
- package/packages/dd-trace/src/appsec/recommended.json +208 -3
- package/packages/dd-trace/src/appsec/reporter.js +60 -20
- package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
- package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
- package/packages/dd-trace/src/appsec/standalone.js +130 -0
- package/packages/dd-trace/src/appsec/telemetry.js +33 -1
- package/packages/dd-trace/src/appsec/waf/index.js +2 -2
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +3 -3
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/config.js +136 -63
- package/packages/dd-trace/src/constants.js +3 -1
- package/packages/dd-trace/src/datastreams/processor.js +3 -2
- package/packages/dd-trace/src/exporters/agent/index.js +2 -2
- package/packages/dd-trace/src/format.js +1 -0
- package/packages/dd-trace/src/opentelemetry/span.js +1 -1
- package/packages/dd-trace/src/opentelemetry/tracer.js +6 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
- package/packages/dd-trace/src/opentracing/span.js +4 -1
- package/packages/dd-trace/src/opentracing/tracer.js +2 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +5 -1
- package/packages/dd-trace/src/priority_sampler.js +2 -5
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/proxy.js +3 -1
- package/packages/dd-trace/src/rate_limiter.js +2 -2
- package/packages/dd-trace/src/span_stats.js +4 -3
- package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
- package/packages/dd-trace/src/tracer.js +2 -2
- package/packages/dd-trace/src/util.js +6 -1
- package/packages/datadog-core/src/storage/async_hooks.js +0 -49
|
@@ -7,10 +7,10 @@ const PROCESS_DENYLIST = ['md5']
|
|
|
7
7
|
|
|
8
8
|
const VARNAMES_REGEX = /\$([\w\d_]*)(?:[^\w\d_]|$)/gmi
|
|
9
9
|
// eslint-disable-next-line max-len
|
|
10
|
-
const PARAM_PATTERN = '^-{0,2}(?:p(?:ass(?:w(?:or)?d)?)?|
|
|
10
|
+
const PARAM_PATTERN = '^-{0,2}(?:p(?:ass(?:w(?:or)?d)?)?|address|api[-_]?key|e?mail|secret(?:[-_]?key)?|a(?:ccess|uth)[-_]?token|mysql_pwd|credentials|(?:stripe)?token)$'
|
|
11
11
|
const regexParam = new RegExp(PARAM_PATTERN, 'i')
|
|
12
12
|
const ENV_PATTERN = '^(\\w+=\\w+;)*\\w+=\\w+;?$'
|
|
13
|
-
const
|
|
13
|
+
const envVarRegex = new RegExp(ENV_PATTERN)
|
|
14
14
|
const REDACTED = '?'
|
|
15
15
|
|
|
16
16
|
function extractVarNames (expression) {
|
|
@@ -61,7 +61,9 @@ function scrubChildProcessCmd (expression) {
|
|
|
61
61
|
for (let index = 0; index < expressionTokens.length; index++) {
|
|
62
62
|
const token = expressionTokens[index]
|
|
63
63
|
|
|
64
|
-
if (
|
|
64
|
+
if (token === null) {
|
|
65
|
+
continue
|
|
66
|
+
} else if (typeof token === 'object') {
|
|
65
67
|
if (token.pattern) {
|
|
66
68
|
result.push(token.pattern)
|
|
67
69
|
} else if (token.op) {
|
|
@@ -70,7 +72,7 @@ function scrubChildProcessCmd (expression) {
|
|
|
70
72
|
result.push(`#${token.comment}`)
|
|
71
73
|
}
|
|
72
74
|
} else if (!foundBinary) {
|
|
73
|
-
if (
|
|
75
|
+
if (envVarRegex.test(token)) {
|
|
74
76
|
const envSplit = token.split('=')
|
|
75
77
|
|
|
76
78
|
if (!ALLOWED_ENV_VARIABLES.includes(envSplit[0])) {
|
|
@@ -195,6 +195,17 @@ class CucumberPlugin extends CiPlugin {
|
|
|
195
195
|
this.enter(testSpan, store)
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
this.addSub('ci:cucumber:test:retry', (isFlakyRetry) => {
|
|
199
|
+
const store = storage.getStore()
|
|
200
|
+
const span = store.span
|
|
201
|
+
if (isFlakyRetry) {
|
|
202
|
+
span.setTag(TEST_IS_RETRY, 'true')
|
|
203
|
+
}
|
|
204
|
+
span.setTag(TEST_STATUS, 'fail')
|
|
205
|
+
span.finish()
|
|
206
|
+
finishAllTraceSpans(span)
|
|
207
|
+
})
|
|
208
|
+
|
|
198
209
|
this.addSub('ci:cucumber:test-step:start', ({ resource }) => {
|
|
199
210
|
const store = storage.getStore()
|
|
200
211
|
const childOf = store ? store.span : store
|
|
@@ -239,7 +250,15 @@ class CucumberPlugin extends CiPlugin {
|
|
|
239
250
|
})
|
|
240
251
|
})
|
|
241
252
|
|
|
242
|
-
this.addSub('ci:cucumber:test:finish', ({
|
|
253
|
+
this.addSub('ci:cucumber:test:finish', ({
|
|
254
|
+
isStep,
|
|
255
|
+
status,
|
|
256
|
+
skipReason,
|
|
257
|
+
errorMessage,
|
|
258
|
+
isNew,
|
|
259
|
+
isEfdRetry,
|
|
260
|
+
isFlakyRetry
|
|
261
|
+
}) => {
|
|
243
262
|
const span = storage.getStore().span
|
|
244
263
|
const statusTag = isStep ? 'step.status' : TEST_STATUS
|
|
245
264
|
|
|
@@ -260,6 +279,10 @@ class CucumberPlugin extends CiPlugin {
|
|
|
260
279
|
span.setTag(ERROR_MESSAGE, errorMessage)
|
|
261
280
|
}
|
|
262
281
|
|
|
282
|
+
if (isFlakyRetry > 0) {
|
|
283
|
+
span.setTag(TEST_IS_RETRY, 'true')
|
|
284
|
+
}
|
|
285
|
+
|
|
263
286
|
span.finish()
|
|
264
287
|
if (!isStep) {
|
|
265
288
|
this.telemetry.ciVisEvent(
|
|
@@ -28,7 +28,8 @@ const {
|
|
|
28
28
|
TEST_SOURCE_FILE,
|
|
29
29
|
TEST_IS_NEW,
|
|
30
30
|
TEST_IS_RETRY,
|
|
31
|
-
TEST_EARLY_FLAKE_ENABLED
|
|
31
|
+
TEST_EARLY_FLAKE_ENABLED,
|
|
32
|
+
NUM_FAILED_TEST_RETRIES
|
|
32
33
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
33
34
|
const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
|
|
34
35
|
const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
|
|
@@ -207,10 +208,40 @@ class CypressPlugin {
|
|
|
207
208
|
this.knownTests = []
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
// Init function returns a promise that resolves with the Cypress configuration
|
|
212
|
+
// Depending on the received configuration, the Cypress configuration can be modified:
|
|
213
|
+
// for example, to enable retries for failed tests.
|
|
210
214
|
init (tracer, cypressConfig) {
|
|
211
215
|
this._isInit = true
|
|
212
216
|
this.tracer = tracer
|
|
213
217
|
this.cypressConfig = cypressConfig
|
|
218
|
+
|
|
219
|
+
this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration)
|
|
220
|
+
.then((libraryConfigurationResponse) => {
|
|
221
|
+
if (libraryConfigurationResponse.err) {
|
|
222
|
+
log.error(libraryConfigurationResponse.err)
|
|
223
|
+
} else {
|
|
224
|
+
const {
|
|
225
|
+
libraryConfig: {
|
|
226
|
+
isSuitesSkippingEnabled,
|
|
227
|
+
isCodeCoverageEnabled,
|
|
228
|
+
isEarlyFlakeDetectionEnabled,
|
|
229
|
+
earlyFlakeDetectionNumRetries,
|
|
230
|
+
isFlakyTestRetriesEnabled
|
|
231
|
+
}
|
|
232
|
+
} = libraryConfigurationResponse
|
|
233
|
+
this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
|
|
234
|
+
this.isCodeCoverageEnabled = isCodeCoverageEnabled
|
|
235
|
+
this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
|
|
236
|
+
this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
|
|
237
|
+
this.isFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled
|
|
238
|
+
if (this.isFlakyTestRetriesEnabled) {
|
|
239
|
+
this.cypressConfig.retries.runMode = NUM_FAILED_TEST_RETRIES
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return this.cypressConfig
|
|
243
|
+
})
|
|
244
|
+
return this.libraryConfigurationPromise
|
|
214
245
|
}
|
|
215
246
|
|
|
216
247
|
getTestSuiteSpan (suite) {
|
|
@@ -297,29 +328,13 @@ class CypressPlugin {
|
|
|
297
328
|
}
|
|
298
329
|
|
|
299
330
|
async beforeRun (details) {
|
|
331
|
+
// We need to make sure that the plugin is initialized before running the tests
|
|
332
|
+
// This is for the case where the user has not returned the promise from the init function
|
|
333
|
+
await this.libraryConfigurationPromise
|
|
300
334
|
this.command = getCypressCommand(details)
|
|
301
335
|
this.frameworkVersion = getCypressVersion(details)
|
|
302
336
|
this.rootDir = getRootDir(details)
|
|
303
337
|
|
|
304
|
-
const libraryConfigurationResponse = await getLibraryConfiguration(this.tracer, this.testConfiguration)
|
|
305
|
-
|
|
306
|
-
if (libraryConfigurationResponse.err) {
|
|
307
|
-
log.error(libraryConfigurationResponse.err)
|
|
308
|
-
} else {
|
|
309
|
-
const {
|
|
310
|
-
libraryConfig: {
|
|
311
|
-
isSuitesSkippingEnabled,
|
|
312
|
-
isCodeCoverageEnabled,
|
|
313
|
-
isEarlyFlakeDetectionEnabled,
|
|
314
|
-
earlyFlakeDetectionNumRetries
|
|
315
|
-
}
|
|
316
|
-
} = libraryConfigurationResponse
|
|
317
|
-
this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
|
|
318
|
-
this.isCodeCoverageEnabled = isCodeCoverageEnabled
|
|
319
|
-
this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
|
|
320
|
-
this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
|
|
321
|
-
}
|
|
322
|
-
|
|
323
338
|
if (this.isEarlyFlakeDetectionEnabled) {
|
|
324
339
|
const knownTestsResponse = await getKnownTests(
|
|
325
340
|
this.tracer,
|
|
@@ -485,29 +500,51 @@ class CypressPlugin {
|
|
|
485
500
|
// This is not always the case, such as when an `after` hook fails:
|
|
486
501
|
// Cypress will report the last run test as failed, but we don't know that yet at `dd:afterEach`
|
|
487
502
|
let latestError
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (!
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
if (cypressTest.displayError) {
|
|
494
|
-
latestError = new Error(cypressTest.displayError)
|
|
495
|
-
}
|
|
496
|
-
const cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
|
|
497
|
-
// update test status
|
|
498
|
-
if (cypressTestStatus !== finishedTest.testStatus) {
|
|
499
|
-
finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
|
|
500
|
-
finishedTest.testSpan.setTag('error', latestError)
|
|
501
|
-
}
|
|
502
|
-
if (this.itrCorrelationId) {
|
|
503
|
-
finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
|
|
504
|
-
}
|
|
505
|
-
if (spec.absolute && this.repositoryRoot) {
|
|
506
|
-
finishedTest.testSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot))
|
|
507
|
-
} else {
|
|
508
|
-
finishedTest.testSpan.setTag(TEST_SOURCE_FILE, spec.relative)
|
|
503
|
+
|
|
504
|
+
const finishedTestsByTestName = finishedTests.reduce((acc, finishedTest) => {
|
|
505
|
+
if (!acc[finishedTest.testName]) {
|
|
506
|
+
acc[finishedTest.testName] = []
|
|
509
507
|
}
|
|
510
|
-
finishedTest.
|
|
508
|
+
acc[finishedTest.testName].push(finishedTest)
|
|
509
|
+
return acc
|
|
510
|
+
}, {})
|
|
511
|
+
|
|
512
|
+
Object.entries(finishedTestsByTestName).forEach(([testName, finishedTestAttempts]) => {
|
|
513
|
+
finishedTestAttempts.forEach((finishedTest, attemptIndex) => {
|
|
514
|
+
// TODO: there could be multiple if there have been retries!
|
|
515
|
+
// potentially we need to match the test status!
|
|
516
|
+
const cypressTest = cypressTests.find(test => test.title.join(' ') === testName)
|
|
517
|
+
if (!cypressTest) {
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
// finishedTests can include multiple tests with the same name if they have been retried
|
|
521
|
+
// by early flake detection. Cypress is unaware of this so .attempts does not necessarily have
|
|
522
|
+
// the same length as `finishedTestAttempts`
|
|
523
|
+
let cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
|
|
524
|
+
if (cypressTest.attempts && cypressTest.attempts[attemptIndex]) {
|
|
525
|
+
cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state]
|
|
526
|
+
if (attemptIndex > 0) {
|
|
527
|
+
finishedTest.testSpan.setTag(TEST_IS_RETRY, 'true')
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (cypressTest.displayError) {
|
|
531
|
+
latestError = new Error(cypressTest.displayError)
|
|
532
|
+
}
|
|
533
|
+
// Update test status
|
|
534
|
+
if (cypressTestStatus !== finishedTest.testStatus) {
|
|
535
|
+
finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
|
|
536
|
+
finishedTest.testSpan.setTag('error', latestError)
|
|
537
|
+
}
|
|
538
|
+
if (this.itrCorrelationId) {
|
|
539
|
+
finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
|
|
540
|
+
}
|
|
541
|
+
if (spec.absolute && this.repositoryRoot) {
|
|
542
|
+
finishedTest.testSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot))
|
|
543
|
+
} else {
|
|
544
|
+
finishedTest.testSpan.setTag(TEST_SOURCE_FILE, spec.relative)
|
|
545
|
+
}
|
|
546
|
+
finishedTest.testSpan.finish(finishedTest.finishTime)
|
|
547
|
+
})
|
|
511
548
|
})
|
|
512
549
|
|
|
513
550
|
if (this.testSuiteSpan) {
|
|
@@ -22,13 +22,14 @@ module.exports = (on, config) => {
|
|
|
22
22
|
// The tracer was not init correctly for whatever reason (such as invalid DD_SITE)
|
|
23
23
|
if (tracer._tracer instanceof NoopTracer) {
|
|
24
24
|
// We still need to register these tasks or the support file will fail
|
|
25
|
-
|
|
25
|
+
on('task', noopTask)
|
|
26
|
+
return config
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
cypressPlugin.init(tracer, config)
|
|
29
|
-
|
|
30
29
|
on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
|
|
31
30
|
on('after:spec', cypressPlugin.afterSpec.bind(cypressPlugin))
|
|
32
31
|
on('after:run', cypressPlugin.afterRun.bind(cypressPlugin))
|
|
33
32
|
on('task', cypressPlugin.getTasks())
|
|
33
|
+
|
|
34
|
+
return cypressPlugin.init(tracer, config)
|
|
34
35
|
}
|
|
@@ -29,7 +29,7 @@ class FsPlugin extends TracingPlugin {
|
|
|
29
29
|
resource: operation,
|
|
30
30
|
kind: 'internal',
|
|
31
31
|
meta: {
|
|
32
|
-
'file.descriptor': (typeof fd === 'object' || typeof fd === 'number') ? fd.toString() : '',
|
|
32
|
+
'file.descriptor': ((fd !== null && typeof fd === 'object') || typeof fd === 'number') ? fd.toString() : '',
|
|
33
33
|
'file.dest': params.dest || params.newPath || (params.target && params.path),
|
|
34
34
|
'file.flag': String(flag || defaultFlag || ''),
|
|
35
35
|
'file.gid': gid || '',
|
|
@@ -148,6 +148,7 @@ class JestPlugin extends CiPlugin {
|
|
|
148
148
|
config._ddIsEarlyFlakeDetectionEnabled = !!this.libraryConfig?.isEarlyFlakeDetectionEnabled
|
|
149
149
|
config._ddEarlyFlakeDetectionNumRetries = this.libraryConfig?.earlyFlakeDetectionNumRetries ?? 0
|
|
150
150
|
config._ddRepositoryRoot = this.repositoryRoot
|
|
151
|
+
config._ddIsFlakyTestRetriesEnabled = this.libraryConfig?.isFlakyTestRetriesEnabled ?? false
|
|
151
152
|
})
|
|
152
153
|
})
|
|
153
154
|
|
|
@@ -324,7 +325,8 @@ class JestPlugin extends CiPlugin {
|
|
|
324
325
|
testStartLine,
|
|
325
326
|
testSourceFile,
|
|
326
327
|
isNew,
|
|
327
|
-
isEfdRetry
|
|
328
|
+
isEfdRetry,
|
|
329
|
+
isJestRetry
|
|
328
330
|
} = test
|
|
329
331
|
|
|
330
332
|
const extraTags = {
|
|
@@ -349,6 +351,10 @@ class JestPlugin extends CiPlugin {
|
|
|
349
351
|
}
|
|
350
352
|
}
|
|
351
353
|
|
|
354
|
+
if (isJestRetry) {
|
|
355
|
+
extraTags[TEST_IS_RETRY] = 'true'
|
|
356
|
+
}
|
|
357
|
+
|
|
352
358
|
return super.startTestSpan(name, suite, this.testSuiteSpan, extraTags)
|
|
353
359
|
}
|
|
354
360
|
}
|
|
@@ -81,7 +81,7 @@ class KafkajsProducerPlugin extends ProducerPlugin {
|
|
|
81
81
|
span.setTag(BOOTSTRAP_SERVERS_KEY, bootstrapServers)
|
|
82
82
|
}
|
|
83
83
|
for (const message of messages) {
|
|
84
|
-
if (typeof message === 'object') {
|
|
84
|
+
if (message !== null && typeof message === 'object') {
|
|
85
85
|
this.tracer.inject(span, 'text_map', message.headers)
|
|
86
86
|
if (this.config.dsmEnabled) {
|
|
87
87
|
const payloadSize = getMessageSize(message)
|
|
@@ -175,13 +175,15 @@ class MochaPlugin extends CiPlugin {
|
|
|
175
175
|
this.tracer._exporter.flush()
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
this.addSub('ci:mocha:test:finish', (status) => {
|
|
178
|
+
this.addSub('ci:mocha:test:finish', ({ status, hasBeenRetried }) => {
|
|
179
179
|
const store = storage.getStore()
|
|
180
180
|
const span = store?.span
|
|
181
181
|
|
|
182
182
|
if (span) {
|
|
183
183
|
span.setTag(TEST_STATUS, status)
|
|
184
|
-
|
|
184
|
+
if (hasBeenRetried) {
|
|
185
|
+
span.setTag(TEST_IS_RETRY, 'true')
|
|
186
|
+
}
|
|
185
187
|
span.finish()
|
|
186
188
|
this.telemetry.ciVisEvent(
|
|
187
189
|
TELEMETRY_EVENT_FINISHED,
|
|
@@ -204,8 +206,8 @@ class MochaPlugin extends CiPlugin {
|
|
|
204
206
|
|
|
205
207
|
this.addSub('ci:mocha:test:error', (err) => {
|
|
206
208
|
const store = storage.getStore()
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
const span = store?.span
|
|
210
|
+
if (err && span) {
|
|
209
211
|
if (err.constructor.name === 'Pending' && !this.forbidPending) {
|
|
210
212
|
span.setTag(TEST_STATUS, 'skip')
|
|
211
213
|
} else {
|
|
@@ -215,6 +217,25 @@ class MochaPlugin extends CiPlugin {
|
|
|
215
217
|
}
|
|
216
218
|
})
|
|
217
219
|
|
|
220
|
+
this.addSub('ci:mocha:test:retry', (isFirstAttempt) => {
|
|
221
|
+
const store = storage.getStore()
|
|
222
|
+
const span = store?.span
|
|
223
|
+
if (span) {
|
|
224
|
+
span.setTag(TEST_STATUS, 'fail')
|
|
225
|
+
if (!isFirstAttempt) {
|
|
226
|
+
span.setTag(TEST_IS_RETRY, 'true')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
span.finish()
|
|
230
|
+
this.telemetry.ciVisEvent(
|
|
231
|
+
TELEMETRY_EVENT_FINISHED,
|
|
232
|
+
'test',
|
|
233
|
+
{ hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] }
|
|
234
|
+
)
|
|
235
|
+
finishAllTraceSpans(span)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
218
239
|
this.addSub('ci:mocha:test:parameterize', ({ title, params }) => {
|
|
219
240
|
this._testTitleToParams[title] = params
|
|
220
241
|
})
|
|
@@ -115,7 +115,7 @@ function limitDepth (input) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
function isObject (val) {
|
|
118
|
-
return typeof val === 'object' &&
|
|
118
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function isBSON (val) {
|
|
@@ -7,6 +7,7 @@ const { storage } = require('../../datadog-core')
|
|
|
7
7
|
const services = require('./services')
|
|
8
8
|
const Sampler = require('../../dd-trace/src/sampler')
|
|
9
9
|
const { MEASURED } = require('../../../ext/tags')
|
|
10
|
+
const { estimateTokens } = require('./token-estimator')
|
|
10
11
|
|
|
11
12
|
// String#replaceAll unavailable on Node.js@v14 (dd-trace@<=v3)
|
|
12
13
|
const RE_NEWLINE = /\n/g
|
|
@@ -15,14 +16,17 @@ const RE_TAB = /\t/g
|
|
|
15
16
|
// TODO: In the future we should refactor config.js to make it requirable
|
|
16
17
|
let MAX_TEXT_LEN = 128
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
|
|
19
|
+
function safeRequire (path) {
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
22
|
+
return require(path)
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
const encodingForModel = safeRequire('tiktoken')?.encoding_for_model
|
|
29
|
+
|
|
26
30
|
class OpenApiPlugin extends TracingPlugin {
|
|
27
31
|
static get id () { return 'openai' }
|
|
28
32
|
static get operation () { return 'request' }
|
|
@@ -114,7 +118,7 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
// createChatCompletion, createCompletion
|
|
117
|
-
if (typeof payload.logit_bias === 'object'
|
|
121
|
+
if (payload.logit_bias !== null && typeof payload.logit_bias === 'object') {
|
|
118
122
|
for (const [tokenId, bias] of Object.entries(payload.logit_bias)) {
|
|
119
123
|
tags[`openai.request.logit_bias.${tokenId}`] = bias
|
|
120
124
|
}
|
|
@@ -305,6 +309,7 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
305
309
|
}
|
|
306
310
|
|
|
307
311
|
sendLog (methodName, span, tags, store, error) {
|
|
312
|
+
if (!store) return
|
|
308
313
|
if (!Object.keys(store).length) return
|
|
309
314
|
if (!this.sampler.isSampled()) return
|
|
310
315
|
|
|
@@ -325,9 +330,22 @@ function countPromptTokens (methodName, payload, model) {
|
|
|
325
330
|
const messages = payload.messages
|
|
326
331
|
for (const message of messages) {
|
|
327
332
|
const content = message.content
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
333
|
+
if (typeof content === 'string') {
|
|
334
|
+
const { tokens, estimated } = countTokens(content, model)
|
|
335
|
+
promptTokens += tokens
|
|
336
|
+
promptEstimated = estimated
|
|
337
|
+
} else if (Array.isArray(content)) {
|
|
338
|
+
for (const c of content) {
|
|
339
|
+
if (c.type === 'text') {
|
|
340
|
+
const { tokens, estimated } = countTokens(c.text, model)
|
|
341
|
+
promptTokens += tokens
|
|
342
|
+
promptEstimated = estimated
|
|
343
|
+
}
|
|
344
|
+
// unsupported token computation for image_url
|
|
345
|
+
// as even though URL is a string, its true token count
|
|
346
|
+
// is based on the image itself, something onerous to do client-side
|
|
347
|
+
}
|
|
348
|
+
}
|
|
331
349
|
}
|
|
332
350
|
} else if (methodName === 'completions.create') {
|
|
333
351
|
let prompt = payload.prompt
|
|
@@ -382,25 +400,6 @@ function countTokens (content, model) {
|
|
|
382
400
|
}
|
|
383
401
|
}
|
|
384
402
|
|
|
385
|
-
// If model is unavailable or tiktoken is not imported, then provide a very rough estimate of the number of tokens
|
|
386
|
-
// Approximate using the following assumptions:
|
|
387
|
-
// * English text
|
|
388
|
-
// * 1 token ~= 4 chars
|
|
389
|
-
// * 1 token ~= ¾ words
|
|
390
|
-
function estimateTokens (content) {
|
|
391
|
-
let estimatedTokens = 0
|
|
392
|
-
if (typeof content === 'string') {
|
|
393
|
-
const estimation1 = content.length / 4
|
|
394
|
-
|
|
395
|
-
const matches = content.match(/[\w']+|[.,!?;~@#$%^&*()+/-]/g)
|
|
396
|
-
const estimation2 = matches ? matches.length * 0.75 : 0 // in the case of an empty string
|
|
397
|
-
estimatedTokens = Math.round((1.5 * estimation1 + 0.5 * estimation2) / 2)
|
|
398
|
-
} else if (Array.isArray(content) && typeof content[0] === 'number') {
|
|
399
|
-
estimatedTokens = content.length
|
|
400
|
-
}
|
|
401
|
-
return estimatedTokens
|
|
402
|
-
}
|
|
403
|
-
|
|
404
403
|
function createEditRequestExtraction (tags, payload, store) {
|
|
405
404
|
const instruction = payload.instruction
|
|
406
405
|
tags['openai.request.instruction'] = instruction
|
|
@@ -418,7 +417,7 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
|
|
|
418
417
|
store.messages = payload.messages
|
|
419
418
|
for (let i = 0; i < payload.messages.length; i++) {
|
|
420
419
|
const message = payload.messages[i]
|
|
421
|
-
|
|
420
|
+
tagChatCompletionRequestContent(message.content, i, tags)
|
|
422
421
|
tags[`openai.request.messages.${i}.role`] = message.role
|
|
423
422
|
tags[`openai.request.messages.${i}.name`] = message.name
|
|
424
423
|
tags[`openai.request.messages.${i}.finish_reason`] = message.finish_reason
|
|
@@ -428,14 +427,14 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
|
|
|
428
427
|
function commonCreateImageRequestExtraction (tags, payload, store) {
|
|
429
428
|
// createImageEdit, createImageVariation
|
|
430
429
|
const img = payload.file || payload.image
|
|
431
|
-
if (img && typeof img === 'object' && img.path) {
|
|
430
|
+
if (img !== null && typeof img === 'object' && img.path) {
|
|
432
431
|
const file = path.basename(img.path)
|
|
433
432
|
tags['openai.request.image'] = file
|
|
434
433
|
store.file = file
|
|
435
434
|
}
|
|
436
435
|
|
|
437
436
|
// createImageEdit
|
|
438
|
-
if (payload.mask && typeof payload.mask === 'object' && payload.mask.path) {
|
|
437
|
+
if (payload.mask !== null && typeof payload.mask === 'object' && payload.mask.path) {
|
|
439
438
|
const mask = path.basename(payload.mask.path)
|
|
440
439
|
tags['openai.request.mask'] = mask
|
|
441
440
|
store.mask = mask
|
|
@@ -634,7 +633,7 @@ function commonCreateAudioRequestExtraction (tags, body, store) {
|
|
|
634
633
|
tags['openai.request.response_format'] = body.response_format
|
|
635
634
|
tags['openai.request.language'] = body.language
|
|
636
635
|
|
|
637
|
-
if (body.file && typeof body.file === 'object' && body.file.path) {
|
|
636
|
+
if (body.file !== null && typeof body.file === 'object' && body.file.path) {
|
|
638
637
|
const filename = path.basename(body.file.path)
|
|
639
638
|
tags['openai.request.filename'] = filename
|
|
640
639
|
store.file = filename
|
|
@@ -647,7 +646,7 @@ function commonFileRequestExtraction (tags, body) {
|
|
|
647
646
|
// User can provider either exact file contents or a file read stream
|
|
648
647
|
// With the stream we extract the filepath
|
|
649
648
|
// This is a best effort attempt to extract the filename during the request
|
|
650
|
-
if (body.file && typeof body.file === 'object' && body.file.path) {
|
|
649
|
+
if (body.file !== null && typeof body.file === 'object' && body.file.path) {
|
|
651
650
|
tags['openai.request.filename'] = path.basename(body.file.path)
|
|
652
651
|
}
|
|
653
652
|
}
|
|
@@ -707,7 +706,7 @@ function commonCreateResponseExtraction (tags, body, store, methodName) {
|
|
|
707
706
|
for (let choiceIdx = 0; choiceIdx < body.choices.length; choiceIdx++) {
|
|
708
707
|
const choice = body.choices[choiceIdx]
|
|
709
708
|
|
|
710
|
-
// logprobs can be
|
|
709
|
+
// logprobs can be null and we still want to tag it as 'returned' even when set to 'null'
|
|
711
710
|
const specifiesLogProb = Object.keys(choice).indexOf('logprobs') !== -1
|
|
712
711
|
|
|
713
712
|
tags[`openai.response.choices.${choiceIdx}.finish_reason`] = choice.finish_reason
|
|
@@ -781,6 +780,7 @@ function truncateApiKey (apiKey) {
|
|
|
781
780
|
*/
|
|
782
781
|
function truncateText (text) {
|
|
783
782
|
if (!text) return
|
|
783
|
+
if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
|
|
784
784
|
|
|
785
785
|
text = text
|
|
786
786
|
.replace(RE_NEWLINE, '\\n')
|
|
@@ -793,6 +793,28 @@ function truncateText (text) {
|
|
|
793
793
|
return text
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
function tagChatCompletionRequestContent (contents, messageIdx, tags) {
|
|
797
|
+
if (typeof contents === 'string') {
|
|
798
|
+
tags[`openai.request.messages.${messageIdx}.content`] = contents
|
|
799
|
+
} else if (Array.isArray(contents)) {
|
|
800
|
+
// content can also be an array of objects
|
|
801
|
+
// which represent text input or image url
|
|
802
|
+
for (const contentIdx in contents) {
|
|
803
|
+
const content = contents[contentIdx]
|
|
804
|
+
const type = content.type
|
|
805
|
+
tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.type`] = content.type
|
|
806
|
+
if (type === 'text') {
|
|
807
|
+
tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.text`] = truncateText(content.text)
|
|
808
|
+
} else if (type === 'image_url') {
|
|
809
|
+
tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.image_url.url`] =
|
|
810
|
+
truncateText(content.image_url.url)
|
|
811
|
+
}
|
|
812
|
+
// unsupported type otherwise, won't be tagged
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// unsupported type otherwise, won't be tagged
|
|
816
|
+
}
|
|
817
|
+
|
|
796
818
|
// The server almost always responds with JSON
|
|
797
819
|
function coerceResponseBody (body, methodName) {
|
|
798
820
|
switch (methodName) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// If model is unavailable or tiktoken is not imported, then provide a very rough estimate of the number of tokens
|
|
4
|
+
// Approximate using the following assumptions:
|
|
5
|
+
// * English text
|
|
6
|
+
// * 1 token ~= 4 chars
|
|
7
|
+
// * 1 token ~= ¾ words
|
|
8
|
+
module.exports.estimateTokens = function (content) {
|
|
9
|
+
let estimatedTokens = 0
|
|
10
|
+
if (typeof content === 'string') {
|
|
11
|
+
const estimation1 = content.length / 4
|
|
12
|
+
|
|
13
|
+
const matches = content.match(/[\w']+|[.,!?;~@#$%^&*()+/-]/g)
|
|
14
|
+
const estimation2 = matches ? matches.length * 0.75 : 0 // in the case of an empty string
|
|
15
|
+
estimatedTokens = Math.round((1.5 * estimation1 + 0.5 * estimation2) / 2)
|
|
16
|
+
} else if (Array.isArray(content) && typeof content[0] === 'number') {
|
|
17
|
+
estimatedTokens = content.length
|
|
18
|
+
}
|
|
19
|
+
return estimatedTokens
|
|
20
|
+
}
|
|
@@ -116,7 +116,7 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
116
116
|
|
|
117
117
|
this.enter(span, store)
|
|
118
118
|
})
|
|
119
|
-
this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags, isNew, isEfdRetry }) => {
|
|
119
|
+
this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags, isNew, isEfdRetry, isRetry }) => {
|
|
120
120
|
const store = storage.getStore()
|
|
121
121
|
const span = store && store.span
|
|
122
122
|
if (!span) return
|
|
@@ -135,6 +135,9 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
135
135
|
span.setTag(TEST_IS_RETRY, 'true')
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
if (isRetry) {
|
|
139
|
+
span.setTag(TEST_IS_RETRY, 'true')
|
|
140
|
+
}
|
|
138
141
|
|
|
139
142
|
steps.forEach(step => {
|
|
140
143
|
const stepStartTime = step.startTime.getTime()
|