dd-trace 4.44.0 → 4.46.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 (108) hide show
  1. package/index.d.ts +2 -1
  2. package/package.json +5 -5
  3. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  4. package/packages/datadog-instrumentations/src/apollo-server.js +1 -1
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +4 -4
  6. package/packages/datadog-instrumentations/src/body-parser.js +17 -5
  7. package/packages/datadog-instrumentations/src/cassandra-driver.js +2 -2
  8. package/packages/datadog-instrumentations/src/child_process.js +2 -2
  9. package/packages/datadog-instrumentations/src/connect.js +4 -4
  10. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  11. package/packages/datadog-instrumentations/src/couchbase.js +12 -12
  12. package/packages/datadog-instrumentations/src/cucumber.js +16 -5
  13. package/packages/datadog-instrumentations/src/dns.js +10 -10
  14. package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
  15. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +3 -3
  16. package/packages/datadog-instrumentations/src/express.js +4 -4
  17. package/packages/datadog-instrumentations/src/fastify.js +6 -6
  18. package/packages/datadog-instrumentations/src/fetch.js +1 -1
  19. package/packages/datadog-instrumentations/src/find-my-way.js +2 -2
  20. package/packages/datadog-instrumentations/src/fs.js +2 -2
  21. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +2 -2
  22. package/packages/datadog-instrumentations/src/grpc/client.js +4 -6
  23. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  24. package/packages/datadog-instrumentations/src/hapi.js +10 -13
  25. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -2
  26. package/packages/datadog-instrumentations/src/helpers/register.js +9 -2
  27. package/packages/datadog-instrumentations/src/http/client.js +3 -3
  28. package/packages/datadog-instrumentations/src/jest.js +5 -4
  29. package/packages/datadog-instrumentations/src/knex.js +2 -2
  30. package/packages/datadog-instrumentations/src/koa.js +5 -5
  31. package/packages/datadog-instrumentations/src/ldapjs.js +1 -1
  32. package/packages/datadog-instrumentations/src/mariadb.js +8 -8
  33. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  34. package/packages/datadog-instrumentations/src/microgateway-core.js +4 -4
  35. package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
  36. package/packages/datadog-instrumentations/src/mocha/main.js +91 -70
  37. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -3
  38. package/packages/datadog-instrumentations/src/mocha.js +4 -0
  39. package/packages/datadog-instrumentations/src/moleculer/server.js +2 -2
  40. package/packages/datadog-instrumentations/src/mongodb-core.js +7 -7
  41. package/packages/datadog-instrumentations/src/mongoose.js +5 -6
  42. package/packages/datadog-instrumentations/src/mysql.js +3 -3
  43. package/packages/datadog-instrumentations/src/mysql2.js +6 -6
  44. package/packages/datadog-instrumentations/src/net.js +2 -2
  45. package/packages/datadog-instrumentations/src/next.js +5 -5
  46. package/packages/datadog-instrumentations/src/nyc.js +23 -0
  47. package/packages/datadog-instrumentations/src/openai.js +58 -69
  48. package/packages/datadog-instrumentations/src/oracledb.js +8 -8
  49. package/packages/datadog-instrumentations/src/passport-http.js +1 -1
  50. package/packages/datadog-instrumentations/src/passport-local.js +1 -1
  51. package/packages/datadog-instrumentations/src/passport-utils.js +1 -1
  52. package/packages/datadog-instrumentations/src/pg.js +1 -1
  53. package/packages/datadog-instrumentations/src/pino.js +4 -4
  54. package/packages/datadog-instrumentations/src/playwright.js +6 -4
  55. package/packages/datadog-instrumentations/src/redis.js +2 -2
  56. package/packages/datadog-instrumentations/src/restify.js +4 -4
  57. package/packages/datadog-instrumentations/src/rhea.js +4 -4
  58. package/packages/datadog-instrumentations/src/router.js +5 -5
  59. package/packages/datadog-instrumentations/src/sharedb.js +2 -2
  60. package/packages/datadog-instrumentations/src/vitest.js +22 -5
  61. package/packages/datadog-instrumentations/src/winston.js +2 -3
  62. package/packages/datadog-plugin-cucumber/src/index.js +12 -2
  63. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +21 -10
  64. package/packages/datadog-plugin-hapi/src/index.js +2 -2
  65. package/packages/datadog-plugin-jest/src/index.js +18 -4
  66. package/packages/datadog-plugin-mocha/src/index.js +25 -6
  67. package/packages/datadog-plugin-nyc/src/index.js +35 -0
  68. package/packages/datadog-plugin-openai/src/index.js +58 -47
  69. package/packages/datadog-plugin-playwright/src/index.js +9 -4
  70. package/packages/datadog-plugin-vitest/src/index.js +30 -4
  71. package/packages/datadog-shimmer/src/shimmer.js +144 -10
  72. package/packages/dd-trace/src/appsec/blocking.js +23 -17
  73. package/packages/dd-trace/src/appsec/graphql.js +3 -1
  74. package/packages/dd-trace/src/appsec/iast/iast-log.js +2 -1
  75. package/packages/dd-trace/src/appsec/remote_config/manager.js +4 -1
  76. package/packages/dd-trace/src/appsec/rule_manager.js +8 -0
  77. package/packages/dd-trace/src/appsec/telemetry.js +3 -3
  78. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +40 -1
  79. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -4
  80. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +2 -4
  81. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -1
  82. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +8 -7
  83. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -4
  84. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +2 -4
  85. package/packages/dd-trace/src/ci-visibility/telemetry.js +29 -2
  86. package/packages/dd-trace/src/config.js +157 -142
  87. package/packages/dd-trace/src/lambda/handler.js +1 -0
  88. package/packages/dd-trace/src/lambda/index.js +12 -1
  89. package/packages/dd-trace/src/opentelemetry/context_manager.js +22 -39
  90. package/packages/dd-trace/src/opentelemetry/span_context.js +2 -2
  91. package/packages/dd-trace/src/opentelemetry/tracer.js +23 -14
  92. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +9 -1
  93. package/packages/dd-trace/src/opentracing/propagation/log.js +1 -1
  94. package/packages/dd-trace/src/opentracing/propagation/text_map.js +61 -6
  95. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  96. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -2
  97. package/packages/dd-trace/src/plugins/index.js +1 -0
  98. package/packages/dd-trace/src/plugins/util/git.js +14 -1
  99. package/packages/dd-trace/src/plugins/util/test.js +1 -5
  100. package/packages/dd-trace/src/profiler.js +15 -5
  101. package/packages/dd-trace/src/profiling/config.js +2 -4
  102. package/packages/dd-trace/src/profiling/exporter_cli.js +13 -1
  103. package/packages/dd-trace/src/profiling/exporters/agent.js +7 -1
  104. package/packages/dd-trace/src/profiling/profiler.js +0 -9
  105. package/packages/dd-trace/src/profiling/ssi-heuristics.js +49 -58
  106. package/packages/dd-trace/src/proxy.js +21 -21
  107. package/packages/dd-trace/src/telemetry/index.js +24 -7
  108. package/packages/dd-trace/src/telemetry/logs/index.js +20 -0
@@ -31,6 +31,9 @@ class OpenApiPlugin extends TracingPlugin {
31
31
  static get id () { return 'openai' }
32
32
  static get operation () { return 'request' }
33
33
  static get system () { return 'openai' }
34
+ static get prefix () {
35
+ return 'tracing:apm:openai:request'
36
+ }
34
37
 
35
38
  constructor (...args) {
36
39
  super(...args)
@@ -55,8 +58,10 @@ class OpenApiPlugin extends TracingPlugin {
55
58
  super.configure(config)
56
59
  }
57
60
 
58
- start ({ methodName, args, basePath, apiKey }) {
61
+ bindStart (ctx) {
62
+ const { methodName, args, basePath, apiKey } = ctx
59
63
  const payload = normalizeRequestPayload(methodName, args)
64
+ const store = storage.getStore() || {}
60
65
 
61
66
  const span = this.startSpan('openai.request', {
62
67
  service: this.config.service,
@@ -87,18 +92,16 @@ class OpenApiPlugin extends TracingPlugin {
87
92
  'openai.request.user': payload.user,
88
93
  'openai.request.file_id': payload.file_id // deleteFile, retrieveFile, downloadFile
89
94
  }
90
- })
95
+ }, false)
91
96
 
92
- const fullStore = storage.getStore() || {} // certain request body fields are later used for logs
93
- const store = Object.create(null)
94
- fullStore.openai = store // namespacing these fields
97
+ const openaiStore = Object.create(null)
95
98
 
96
99
  const tags = {} // The remaining tags are added one at a time
97
100
 
98
101
  // createChatCompletion, createCompletion, createImage, createImageEdit, createTranscription, createTranslation
99
102
  if (payload.prompt) {
100
103
  const prompt = payload.prompt
101
- store.prompt = prompt
104
+ openaiStore.prompt = prompt
102
105
  if (typeof prompt === 'string' || (Array.isArray(prompt) && typeof prompt[0] === 'number')) {
103
106
  // This is a single prompt, either String or [Number]
104
107
  tags['openai.request.prompt'] = normalizeStringOrTokenArray(prompt, true)
@@ -114,7 +117,7 @@ class OpenApiPlugin extends TracingPlugin {
114
117
  if (payload.input) {
115
118
  const normalized = normalizeStringOrTokenArray(payload.input, false)
116
119
  tags['openai.request.input'] = truncateText(normalized)
117
- store.input = normalized
120
+ openaiStore.input = normalized
118
121
  }
119
122
 
120
123
  // createChatCompletion, createCompletion
@@ -141,12 +144,12 @@ class OpenApiPlugin extends TracingPlugin {
141
144
  case 'images.edit':
142
145
  case 'createImageVariation':
143
146
  case 'images.createVariation':
144
- commonCreateImageRequestExtraction(tags, payload, store)
147
+ commonCreateImageRequestExtraction(tags, payload, openaiStore)
145
148
  break
146
149
 
147
150
  case 'createChatCompletion':
148
151
  case 'chat.completions.create':
149
- createChatCompletionRequestExtraction(tags, payload, store)
152
+ createChatCompletionRequestExtraction(tags, payload, openaiStore)
150
153
  break
151
154
 
152
155
  case 'createFile':
@@ -160,7 +163,7 @@ class OpenApiPlugin extends TracingPlugin {
160
163
  case 'audio.transcriptions.create':
161
164
  case 'createTranslation':
162
165
  case 'audio.translations.create':
163
- commonCreateAudioRequestExtraction(tags, payload, store)
166
+ commonCreateAudioRequestExtraction(tags, payload, openaiStore)
164
167
  break
165
168
 
166
169
  case 'retrieveModel':
@@ -184,23 +187,32 @@ class OpenApiPlugin extends TracingPlugin {
184
187
 
185
188
  case 'createEdit':
186
189
  case 'edits.create':
187
- createEditRequestExtraction(tags, payload, store)
190
+ createEditRequestExtraction(tags, payload, openaiStore)
188
191
  break
189
192
  }
190
193
 
191
194
  span.addTags(tags)
195
+
196
+ ctx.currentStore = { ...store, span, openai: openaiStore }
197
+
198
+ return ctx.currentStore
192
199
  }
193
200
 
194
- finish (response) {
195
- const span = this.activeSpan
201
+ asyncEnd (ctx) {
202
+ const { result } = ctx
203
+ const store = ctx.currentStore
204
+
205
+ const span = store?.span
206
+ if (!span) return
207
+
196
208
  const error = !!span.context()._tags.error
197
209
 
198
210
  let headers, body, method, path
199
211
  if (!error) {
200
- headers = response.headers
201
- body = response.body
202
- method = response.method
203
- path = response.path
212
+ headers = result.headers
213
+ body = result.data
214
+ method = result.request.method
215
+ path = result.request.path
204
216
  }
205
217
 
206
218
  if (!error && headers?.constructor.name === 'Headers') {
@@ -210,10 +222,9 @@ class OpenApiPlugin extends TracingPlugin {
210
222
 
211
223
  body = coerceResponseBody(body, methodName)
212
224
 
213
- const fullStore = storage.getStore()
214
- const store = fullStore.openai
225
+ const openaiStore = store.openai
215
226
 
216
- if (!error && (path.startsWith('https://') || path.startsWith('http://'))) {
227
+ if (!error && (path?.startsWith('https://') || path?.startsWith('http://'))) {
217
228
  // basic checking for if the path was set as a full URL
218
229
  // not using a full regex as it will likely be "https://api.openai.com/..."
219
230
  path = new URL(path).pathname
@@ -239,11 +250,11 @@ class OpenApiPlugin extends TracingPlugin {
239
250
  'openai.response.created_at': body.created_at
240
251
  }
241
252
 
242
- responseDataExtractionByMethod(methodName, tags, body, store)
253
+ responseDataExtractionByMethod(methodName, tags, body, openaiStore)
243
254
  span.addTags(tags)
244
255
 
245
- super.finish()
246
- this.sendLog(methodName, span, tags, store, error)
256
+ span.finish()
257
+ this.sendLog(methodName, span, tags, openaiStore, error)
247
258
  this.sendMetrics(headers, body, endpoint, span._duration, error, tags)
248
259
  }
249
260
 
@@ -308,15 +319,15 @@ class OpenApiPlugin extends TracingPlugin {
308
319
  }
309
320
  }
310
321
 
311
- sendLog (methodName, span, tags, store, error) {
312
- if (!store) return
313
- if (!Object.keys(store).length) return
322
+ sendLog (methodName, span, tags, openaiStore, error) {
323
+ if (!openaiStore) return
324
+ if (!Object.keys(openaiStore).length) return
314
325
  if (!this.sampler.isSampled()) return
315
326
 
316
327
  const log = {
317
328
  status: error ? 'error' : 'info',
318
329
  message: `sampled ${methodName}`,
319
- ...store
330
+ ...openaiStore
320
331
  }
321
332
 
322
333
  this.logger.log(log, span, tags)
@@ -400,21 +411,21 @@ function countTokens (content, model) {
400
411
  }
401
412
  }
402
413
 
403
- function createEditRequestExtraction (tags, payload, store) {
414
+ function createEditRequestExtraction (tags, payload, openaiStore) {
404
415
  const instruction = payload.instruction
405
416
  tags['openai.request.instruction'] = instruction
406
- store.instruction = instruction
417
+ openaiStore.instruction = instruction
407
418
  }
408
419
 
409
420
  function retrieveModelRequestExtraction (tags, payload) {
410
421
  tags['openai.request.id'] = payload.id
411
422
  }
412
423
 
413
- function createChatCompletionRequestExtraction (tags, payload, store) {
424
+ function createChatCompletionRequestExtraction (tags, payload, openaiStore) {
414
425
  const messages = payload.messages
415
426
  if (!defensiveArrayLength(messages)) return
416
427
 
417
- store.messages = payload.messages
428
+ openaiStore.messages = payload.messages
418
429
  for (let i = 0; i < payload.messages.length; i++) {
419
430
  const message = payload.messages[i]
420
431
  tagChatCompletionRequestContent(message.content, i, tags)
@@ -424,20 +435,20 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
424
435
  }
425
436
  }
426
437
 
427
- function commonCreateImageRequestExtraction (tags, payload, store) {
438
+ function commonCreateImageRequestExtraction (tags, payload, openaiStore) {
428
439
  // createImageEdit, createImageVariation
429
440
  const img = payload.file || payload.image
430
441
  if (img !== null && typeof img === 'object' && img.path) {
431
442
  const file = path.basename(img.path)
432
443
  tags['openai.request.image'] = file
433
- store.file = file
444
+ openaiStore.file = file
434
445
  }
435
446
 
436
447
  // createImageEdit
437
448
  if (payload.mask !== null && typeof payload.mask === 'object' && payload.mask.path) {
438
449
  const mask = path.basename(payload.mask.path)
439
450
  tags['openai.request.mask'] = mask
440
- store.mask = mask
451
+ openaiStore.mask = mask
441
452
  }
442
453
 
443
454
  tags['openai.request.size'] = payload.size
@@ -445,7 +456,7 @@ function commonCreateImageRequestExtraction (tags, payload, store) {
445
456
  tags['openai.request.language'] = payload.language
446
457
  }
447
458
 
448
- function responseDataExtractionByMethod (methodName, tags, body, store) {
459
+ function responseDataExtractionByMethod (methodName, tags, body, openaiStore) {
449
460
  switch (methodName) {
450
461
  case 'createModeration':
451
462
  case 'moderations.create':
@@ -458,7 +469,7 @@ function responseDataExtractionByMethod (methodName, tags, body, store) {
458
469
  case 'chat.completions.create':
459
470
  case 'createEdit':
460
471
  case 'edits.create':
461
- commonCreateResponseExtraction(tags, body, store, methodName)
472
+ commonCreateResponseExtraction(tags, body, openaiStore, methodName)
462
473
  break
463
474
 
464
475
  case 'listFiles':
@@ -474,7 +485,7 @@ function responseDataExtractionByMethod (methodName, tags, body, store) {
474
485
 
475
486
  case 'createEmbedding':
476
487
  case 'embeddings.create':
477
- createEmbeddingResponseExtraction(tags, body)
488
+ createEmbeddingResponseExtraction(tags, body, openaiStore)
478
489
  break
479
490
 
480
491
  case 'createFile':
@@ -629,14 +640,14 @@ function deleteFileResponseExtraction (tags, body) {
629
640
  tags['openai.response.id'] = body.id
630
641
  }
631
642
 
632
- function commonCreateAudioRequestExtraction (tags, body, store) {
643
+ function commonCreateAudioRequestExtraction (tags, body, openaiStore) {
633
644
  tags['openai.request.response_format'] = body.response_format
634
645
  tags['openai.request.language'] = body.language
635
646
 
636
647
  if (body.file !== null && typeof body.file === 'object' && body.file.path) {
637
648
  const filename = path.basename(body.file.path)
638
649
  tags['openai.request.filename'] = filename
639
- store.file = filename
650
+ openaiStore.file = filename
640
651
  }
641
652
  }
642
653
 
@@ -659,8 +670,8 @@ function createRetrieveFileResponseExtraction (tags, body) {
659
670
  tags['openai.response.status_details'] = body.status_details
660
671
  }
661
672
 
662
- function createEmbeddingResponseExtraction (tags, body) {
663
- usageExtraction(tags, body)
673
+ function createEmbeddingResponseExtraction (tags, body, openaiStore) {
674
+ usageExtraction(tags, body, openaiStore)
664
675
 
665
676
  if (!body.data) return
666
677
 
@@ -694,14 +705,14 @@ function createModerationResponseExtraction (tags, body) {
694
705
  }
695
706
 
696
707
  // createCompletion, createChatCompletion, createEdit
697
- function commonCreateResponseExtraction (tags, body, store, methodName) {
698
- usageExtraction(tags, body, methodName)
708
+ function commonCreateResponseExtraction (tags, body, openaiStore, methodName) {
709
+ usageExtraction(tags, body, methodName, openaiStore)
699
710
 
700
711
  if (!body.choices) return
701
712
 
702
713
  tags['openai.response.choices_count'] = body.choices.length
703
714
 
704
- store.choices = body.choices
715
+ openaiStore.choices = body.choices
705
716
 
706
717
  for (let choiceIdx = 0; choiceIdx < body.choices.length; choiceIdx++) {
707
718
  const choice = body.choices[choiceIdx]
@@ -735,7 +746,7 @@ function commonCreateResponseExtraction (tags, body, store, methodName) {
735
746
  }
736
747
 
737
748
  // createCompletion, createChatCompletion, createEdit, createEmbedding
738
- function usageExtraction (tags, body, methodName) {
749
+ function usageExtraction (tags, body, methodName, openaiStore) {
739
750
  let promptTokens = 0
740
751
  let completionTokens = 0
741
752
  let totalTokens = 0
@@ -743,14 +754,14 @@ function usageExtraction (tags, body, methodName) {
743
754
  promptTokens = body.usage.prompt_tokens
744
755
  completionTokens = body.usage.completion_tokens
745
756
  totalTokens = body.usage.total_tokens
746
- } else if (['chat.completions.create', 'completions.create'].includes(methodName)) {
757
+ } else if (body.model && ['chat.completions.create', 'completions.create'].includes(methodName)) {
747
758
  // estimate tokens based on method name for completions and chat completions
748
759
  const { model } = body
749
760
  let promptEstimated = false
750
761
  let completionEstimated = false
751
762
 
752
763
  // prompt tokens
753
- const payload = storage.getStore().openai
764
+ const payload = openaiStore
754
765
  const promptTokensCount = countPromptTokens(methodName, payload, model)
755
766
  promptTokens = promptTokensCount.promptTokens
756
767
  promptEstimated = promptTokensCount.promptEstimated
@@ -14,7 +14,8 @@ const {
14
14
  TEST_CONFIGURATION_BROWSER_NAME,
15
15
  TEST_IS_NEW,
16
16
  TEST_IS_RETRY,
17
- TEST_EARLY_FLAKE_ENABLED
17
+ TEST_EARLY_FLAKE_ENABLED,
18
+ TELEMETRY_TEST_SESSION
18
19
  } = require('../../dd-trace/src/plugins/util/test')
19
20
  const { RESOURCE_NAME } = require('../../../ext/tags')
20
21
  const { COMPONENT } = require('../../dd-trace/src/constants')
@@ -59,6 +60,7 @@ class PlaywrightPlugin extends CiPlugin {
59
60
  this.testSessionSpan.finish()
60
61
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
61
62
  finishAllTraceSpans(this.testSessionSpan)
63
+ this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName })
62
64
  appClosingTelemetry()
63
65
  this.tracer._exporter.flush(onDone)
64
66
  this.numFailedTests = 0
@@ -160,8 +162,6 @@ class PlaywrightPlugin extends CiPlugin {
160
162
  stepSpan.finish(stepStartTime + stepDuration)
161
163
  })
162
164
 
163
- span.finish()
164
-
165
165
  if (testStatus === 'fail') {
166
166
  this.numFailedTests++
167
167
  }
@@ -169,8 +169,13 @@ class PlaywrightPlugin extends CiPlugin {
169
169
  this.telemetry.ciVisEvent(
170
170
  TELEMETRY_EVENT_FINISHED,
171
171
  'test',
172
- { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] }
172
+ {
173
+ hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS],
174
+ isNew,
175
+ browserDriver: 'playwright'
176
+ }
173
177
  )
178
+ span.finish()
174
179
 
175
180
  finishAllTraceSpans(span)
176
181
  })
@@ -7,9 +7,16 @@ const {
7
7
  getTestSuitePath,
8
8
  getTestSuiteCommonTags,
9
9
  TEST_SOURCE_FILE,
10
- TEST_IS_RETRY
10
+ TEST_IS_RETRY,
11
+ TEST_CODE_COVERAGE_LINES_PCT,
12
+ TEST_CODE_OWNERS
11
13
  } = require('../../dd-trace/src/plugins/util/test')
12
14
  const { COMPONENT } = require('../../dd-trace/src/constants')
15
+ const {
16
+ TELEMETRY_EVENT_CREATED,
17
+ TELEMETRY_EVENT_FINISHED,
18
+ TELEMETRY_TEST_SESSION
19
+ } = require('../../dd-trace/src/ci-visibility/telemetry')
13
20
 
14
21
  // Milliseconds that we subtract from the error test duration
15
22
  // so that they do not overlap with the following test
@@ -64,6 +71,9 @@ class VitestPlugin extends CiPlugin {
64
71
  const span = store?.span
65
72
 
66
73
  if (span) {
74
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
75
+ hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS]
76
+ })
67
77
  span.setTag(TEST_STATUS, 'pass')
68
78
  span.finish(this.taskToFinishTime.get(task))
69
79
  finishAllTraceSpans(span)
@@ -75,6 +85,9 @@ class VitestPlugin extends CiPlugin {
75
85
  const span = store?.span
76
86
 
77
87
  if (span) {
88
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
89
+ hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS]
90
+ })
78
91
  span.setTag(TEST_STATUS, 'fail')
79
92
 
80
93
  if (error) {
@@ -91,7 +104,7 @@ class VitestPlugin extends CiPlugin {
91
104
 
92
105
  this.addSub('ci:vitest:test:skip', ({ testName, testSuiteAbsolutePath }) => {
93
106
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
94
- this.startTestSpan(
107
+ const testSpan = this.startTestSpan(
95
108
  testName,
96
109
  testSuite,
97
110
  this.testSuiteSpan,
@@ -99,7 +112,11 @@ class VitestPlugin extends CiPlugin {
99
112
  [TEST_SOURCE_FILE]: testSuite,
100
113
  [TEST_STATUS]: 'skip'
101
114
  }
102
- ).finish()
115
+ )
116
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
117
+ hasCodeowners: !!testSpan.context()._tags[TEST_CODE_OWNERS]
118
+ })
119
+ testSpan.finish()
103
120
  })
104
121
 
105
122
  this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
@@ -124,6 +141,7 @@ class VitestPlugin extends CiPlugin {
124
141
  ...testSuiteMetadata
125
142
  }
126
143
  })
144
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
127
145
  const store = storage.getStore()
128
146
  this.enter(testSuiteSpan, store)
129
147
  this.testSuiteSpan = testSuiteSpan
@@ -137,6 +155,7 @@ class VitestPlugin extends CiPlugin {
137
155
  span.finish()
138
156
  finishAllTraceSpans(span)
139
157
  }
158
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
140
159
  // TODO: too frequent flush - find for method in worker to decrease frequency
141
160
  this.tracer._exporter.flush(onFinish)
142
161
  })
@@ -150,16 +169,23 @@ class VitestPlugin extends CiPlugin {
150
169
  }
151
170
  })
152
171
 
153
- this.addSub('ci:vitest:session:finish', ({ status, onFinish, error }) => {
172
+ this.addSub('ci:vitest:session:finish', ({ status, onFinish, error, testCodeCoverageLinesTotal }) => {
154
173
  this.testSessionSpan.setTag(TEST_STATUS, status)
155
174
  this.testModuleSpan.setTag(TEST_STATUS, status)
156
175
  if (error) {
157
176
  this.testModuleSpan.setTag('error', error)
158
177
  this.testSessionSpan.setTag('error', error)
159
178
  }
179
+ if (testCodeCoverageLinesTotal) {
180
+ this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
181
+ this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
182
+ }
160
183
  this.testModuleSpan.finish()
184
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
161
185
  this.testSessionSpan.finish()
186
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
162
187
  finishAllTraceSpans(this.testSessionSpan)
188
+ this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName })
163
189
  this.tracer._exporter.flush(onFinish)
164
190
  })
165
191
  }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../dd-trace/src/log')
4
+
3
5
  // Use a weak map to avoid polluting the wrapped function/method.
4
6
  const unwrappers = new WeakMap()
5
7
 
@@ -18,9 +20,12 @@ function copyProperties (original, wrapped) {
18
20
  }
19
21
  }
20
22
 
21
- function wrapFn (original, delegate) {
22
- assertFunction(delegate)
23
- assertNotClass(original) // TODO: support constructors of native classes
23
+ function wrapFunction (original, wrapper) {
24
+ if (typeof original === 'function') assertNotClass(original)
25
+ // TODO This needs to be re-done so that this and wrapMethod are distinct.
26
+ const target = { func: original }
27
+ wrapMethod(target, 'func', wrapper, typeof original !== 'function')
28
+ let delegate = target.func
24
29
 
25
30
  const shim = function shim () {
26
31
  return delegate.apply(this, arguments)
@@ -30,17 +35,144 @@ function wrapFn (original, delegate) {
30
35
  delegate = original
31
36
  })
32
37
 
33
- copyProperties(original, shim)
38
+ if (typeof original === 'function') copyProperties(original, shim)
34
39
 
35
40
  return shim
36
41
  }
37
42
 
38
- function wrapMethod (target, name, wrapper) {
39
- assertMethod(target, name)
40
- assertFunction(wrapper)
43
+ const wrapFn = function (original, delegate) {
44
+ throw new Error('calling `wrap()` with 2 args is deprecated. Use wrapFunction instead.')
45
+ }
46
+
47
+ // This is only used in safe mode. It's a simple state machine to track if the
48
+ // original method was called and if it returned. We need this to determine if
49
+ // an error was thrown by the original method, or by us. We'll use one of these
50
+ // per call to a wrapped method.
51
+ class CallState {
52
+ constructor () {
53
+ this.called = false
54
+ this.completed = false
55
+ this.retVal = undefined
56
+ }
57
+
58
+ startCall () {
59
+ this.called = true
60
+ }
61
+
62
+ endCall (retVal) {
63
+ this.completed = true
64
+ this.retVal = retVal
65
+ }
66
+ }
67
+
68
+ function isPromise (obj) {
69
+ return obj && typeof obj === 'object' && typeof obj.then === 'function'
70
+ }
71
+
72
+ let safeMode = !!process.env.DD_INEJCTION_ENABLED
73
+ function setSafe (value) {
74
+ safeMode = value
75
+ }
76
+
77
+ function wrapMethod (target, name, wrapper, noAssert) {
78
+ if (!noAssert) {
79
+ assertMethod(target, name)
80
+ assertFunction(wrapper)
81
+ }
41
82
 
42
83
  const original = target[name]
43
- const wrapped = wrapper(original)
84
+ let wrapped
85
+
86
+ if (safeMode && original) {
87
+ // In this mode, we make a best-effort attempt to handle errors that are thrown
88
+ // by us, rather than wrapped code. With such errors, we log them, and then attempt
89
+ // to return the result as if no wrapping was done at all.
90
+ //
91
+ // Caveats:
92
+ // * If the original function is called in a later iteration of the event loop,
93
+ // and we throw _then_, then it won't be caught by this. In practice, we always call
94
+ // the original function synchronously, so this is not a problem.
95
+ // * While async errors are dealt with here, errors in callbacks are not. This
96
+ // is because we don't necessarily know _for sure_ that any function arguments
97
+ // are wrapped by us. We could wrap them all anyway and just make that assumption,
98
+ // or just assume that the last argument is always a callback set by us if it's a
99
+ // function, but those don't seem like things we can rely on. We could add a
100
+ // `shimmer.markCallbackAsWrapped()` function that's a no-op outside safe-mode,
101
+ // but that means modifying every instrumentation. Even then, the complexity of
102
+ // this code increases because then we'd need to effectively do the reverse of
103
+ // what we're doing for synchronous functions. This is a TODO.
104
+
105
+ // We're going to hold on to current callState in this variable in this scope,
106
+ // which is fine because any time we reference it, we're referencing it synchronously.
107
+ // We'll use it in the our wrapper (which, again, is called syncrhonously), and in the
108
+ // errorHandler, which will already have been bound to this callState.
109
+ let currentCallState
110
+
111
+ // Rather than calling the original function directly from the shim wrapper, we wrap
112
+ // it again so that we can track if it was called and if it returned. This is because
113
+ // we need to know if an error was thrown by the original function, or by us.
114
+ // We could do this inside the `wrapper` function defined below, which would simplify
115
+ // managing the callState, but then we'd be calling `wrapper` on each invocation, so
116
+ // instead we do it here, once.
117
+ const innerWrapped = wrapper(function (...args) {
118
+ // We need to stash the callState here because of recursion.
119
+ const callState = currentCallState
120
+ callState.startCall()
121
+ const retVal = original.apply(this, args)
122
+ if (isPromise(retVal)) {
123
+ retVal.then(callState.endCall.bind(callState))
124
+ } else {
125
+ callState.endCall(retVal)
126
+ }
127
+ return retVal
128
+ })
129
+
130
+ // This is the crux of what we're doing in safe mode. It handles errors
131
+ // that _we_ cause, by logging them, and transparently providing results
132
+ // as if no wrapping was done at all. That means detecting (via callState)
133
+ // whether the function has already run or not, and if it has, returning
134
+ // the result, and otherwise calling the original function unwrapped.
135
+ const handleError = function (args, callState, e) {
136
+ if (callState.completed) {
137
+ // error was thrown after original function returned/resolved, so
138
+ // it was us. log it.
139
+ log.error(e)
140
+ // original ran and returned something. return it.
141
+ return callState.retVal
142
+ }
143
+
144
+ if (!callState.called) {
145
+ // error was thrown before original function was called, so
146
+ // it was us. log it.
147
+ log.error(e)
148
+ // original never ran. call it unwrapped.
149
+ return original.apply(this, args)
150
+ }
151
+
152
+ // error was thrown during original function execution, so
153
+ // it was them. throw.
154
+ throw e
155
+ }
156
+
157
+ // The wrapped function is the one that will be called by the user.
158
+ // It calls our version of the original function, which manages the
159
+ // callState. That way when we use the errorHandler, it can tell where
160
+ // the error originated.
161
+ wrapped = function (...args) {
162
+ currentCallState = new CallState()
163
+ const errorHandler = handleError.bind(this, args, currentCallState)
164
+
165
+ try {
166
+ const retVal = innerWrapped.apply(this, args)
167
+ return isPromise(retVal) ? retVal.catch(errorHandler) : retVal
168
+ } catch (e) {
169
+ return errorHandler(e)
170
+ }
171
+ }
172
+ } else {
173
+ // In non-safe mode, we just wrap the original function directly.
174
+ wrapped = wrapper(original)
175
+ }
44
176
  const descriptor = Object.getOwnPropertyDescriptor(target, name)
45
177
 
46
178
  const attributes = {
@@ -48,7 +180,7 @@ function wrapMethod (target, name, wrapper) {
48
180
  ...descriptor
49
181
  }
50
182
 
51
- copyProperties(original, wrapped)
183
+ if (typeof original === 'function') copyProperties(original, wrapped)
52
184
 
53
185
  if (descriptor) {
54
186
  unwrappers.set(wrapped, () => Object.defineProperty(target, name, descriptor))
@@ -156,7 +288,9 @@ function assertNotClass (target) {
156
288
 
157
289
  module.exports = {
158
290
  wrap,
291
+ wrapFunction,
159
292
  massWrap,
160
293
  unwrap,
161
- massUnwrap
294
+ massUnwrap,
295
+ setSafe
162
296
  }