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.
Files changed (93) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/ext/exporters.d.ts +1 -1
  3. package/index.d.ts +105 -37
  4. package/init.js +40 -1
  5. package/initialize.mjs +8 -5
  6. package/package.json +29 -29
  7. package/packages/datadog-core/src/storage/index.js +1 -10
  8. package/packages/datadog-esbuild/index.js +5 -1
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  10. package/packages/datadog-instrumentations/src/child_process.js +2 -2
  11. package/packages/datadog-instrumentations/src/cucumber.js +76 -34
  12. package/packages/datadog-instrumentations/src/fs.js +1 -1
  13. package/packages/datadog-instrumentations/src/hapi.js +1 -1
  14. package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
  17. package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
  18. package/packages/datadog-instrumentations/src/http/client.js +1 -1
  19. package/packages/datadog-instrumentations/src/jest.js +17 -2
  20. package/packages/datadog-instrumentations/src/kafkajs.js +1 -1
  21. package/packages/datadog-instrumentations/src/ldapjs.js +2 -2
  22. package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
  24. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
  25. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  26. package/packages/datadog-instrumentations/src/next.js +1 -1
  27. package/packages/datadog-instrumentations/src/pg.js +2 -2
  28. package/packages/datadog-instrumentations/src/playwright.js +47 -33
  29. package/packages/datadog-instrumentations/src/restify.js +1 -1
  30. package/packages/datadog-instrumentations/src/vitest.js +349 -0
  31. package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
  32. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  33. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  34. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
  35. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
  36. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
  37. package/packages/datadog-plugin-child_process/src/index.js +1 -1
  38. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -4
  39. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  40. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +79 -42
  41. package/packages/datadog-plugin-cypress/src/plugin.js +4 -3
  42. package/packages/datadog-plugin-fs/src/index.js +1 -1
  43. package/packages/datadog-plugin-jest/src/index.js +7 -1
  44. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -1
  45. package/packages/datadog-plugin-mocha/src/index.js +25 -4
  46. package/packages/datadog-plugin-mongodb-core/src/index.js +1 -1
  47. package/packages/datadog-plugin-openai/src/index.js +57 -35
  48. package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  50. package/packages/datadog-plugin-sharedb/src/index.js +1 -1
  51. package/packages/datadog-plugin-vitest/src/index.js +167 -0
  52. package/packages/dd-trace/src/analytics_sampler.js +1 -1
  53. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +1 -1
  54. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
  55. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  56. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  57. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +3 -1
  58. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
  59. package/packages/dd-trace/src/appsec/index.js +4 -4
  60. package/packages/dd-trace/src/appsec/passport.js +1 -1
  61. package/packages/dd-trace/src/appsec/rasp.js +32 -5
  62. package/packages/dd-trace/src/appsec/recommended.json +208 -3
  63. package/packages/dd-trace/src/appsec/reporter.js +60 -20
  64. package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
  65. package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
  66. package/packages/dd-trace/src/appsec/standalone.js +130 -0
  67. package/packages/dd-trace/src/appsec/telemetry.js +33 -1
  68. package/packages/dd-trace/src/appsec/waf/index.js +2 -2
  69. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +3 -3
  70. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  71. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  72. package/packages/dd-trace/src/config.js +136 -63
  73. package/packages/dd-trace/src/constants.js +3 -1
  74. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  75. package/packages/dd-trace/src/exporters/agent/index.js +2 -2
  76. package/packages/dd-trace/src/format.js +1 -0
  77. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  78. package/packages/dd-trace/src/opentelemetry/tracer.js +6 -0
  79. package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
  80. package/packages/dd-trace/src/opentracing/span.js +4 -1
  81. package/packages/dd-trace/src/opentracing/tracer.js +2 -2
  82. package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
  83. package/packages/dd-trace/src/plugins/index.js +2 -0
  84. package/packages/dd-trace/src/plugins/util/test.js +5 -1
  85. package/packages/dd-trace/src/priority_sampler.js +2 -5
  86. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  87. package/packages/dd-trace/src/proxy.js +3 -1
  88. package/packages/dd-trace/src/rate_limiter.js +2 -2
  89. package/packages/dd-trace/src/span_stats.js +4 -3
  90. package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
  91. package/packages/dd-trace/src/tracer.js +2 -2
  92. package/packages/dd-trace/src/util.js +6 -1
  93. 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)?)?|api_?key|secret|a(?:ccess|uth)_token|mysql_pwd|credentials|(?:stripe)?token)$'
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 envvarRegex = new RegExp(ENV_PATTERN)
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 (typeof token === 'object') {
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 (envvarRegex.test(token)) {
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', ({ isStep, status, skipReason, errorMessage, isNew, isEfdRetry }) => {
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
- finishedTests.forEach((finishedTest) => {
489
- const cypressTest = cypressTests.find(test => test.title.join(' ') === finishedTest.testName)
490
- if (!cypressTest) {
491
- return
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.testSpan.finish(finishedTest.finishTime)
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
- return on('task', noopTask)
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
- if (err && store && store.span) {
208
- const span = store.span
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' && val !== null && !(val instanceof Array)
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
- let encodingForModel
19
- try {
20
- // eslint-disable-next-line import/no-extraneous-dependencies
21
- encodingForModel = require('tiktoken').encoding_for_model
22
- } catch {
23
- // we will use token count estimations in this case
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' && payload.logit_bias) {
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
- const { tokens, estimated } = countTokens(content, model)
329
- promptTokens += tokens
330
- promptEstimated = estimated
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
- tags[`openai.request.messages.${i}.content`] = truncateText(message.content)
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 nullm and we still want to tag it as 'returned' even when set to 'null'
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()
@@ -54,7 +54,7 @@ function sanitize (input) {
54
54
  }
55
55
 
56
56
  function isObject (val) {
57
- return typeof val === 'object' && val !== null && !(val instanceof Array)
57
+ return val !== null && typeof val === 'object' && !Array.isArray(val)
58
58
  }
59
59
 
60
60
  module.exports = SharedbPlugin