dd-trace 5.31.0 → 5.32.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 (62) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +9 -7
  3. package/package.json +5 -4
  4. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  5. package/packages/datadog-instrumentations/src/cucumber.js +14 -5
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  7. package/packages/datadog-instrumentations/src/jest.js +70 -36
  8. package/packages/datadog-instrumentations/src/mocha/utils.js +23 -7
  9. package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
  10. package/packages/datadog-instrumentations/src/openai.js +2 -0
  11. package/packages/datadog-instrumentations/src/vitest.js +107 -59
  12. package/packages/datadog-instrumentations/src/vm.js +49 -0
  13. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime.js +295 -0
  14. package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
  15. package/packages/datadog-plugin-cucumber/src/index.js +30 -32
  16. package/packages/datadog-plugin-jest/src/index.js +34 -37
  17. package/packages/datadog-plugin-langchain/src/index.js +12 -80
  18. package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
  19. package/packages/datadog-plugin-mocha/src/index.js +18 -36
  20. package/packages/datadog-plugin-vitest/src/index.js +20 -34
  21. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  22. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
  23. package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
  24. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
  25. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  26. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
  27. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
  28. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
  29. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
  30. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
  31. package/packages/dd-trace/src/config.js +39 -3
  32. package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
  33. package/packages/dd-trace/src/crashtracking/noop.js +3 -0
  34. package/packages/dd-trace/src/datastreams/fnv.js +1 -1
  35. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
  36. package/packages/dd-trace/src/debugger/devtools_client/config.js +1 -0
  37. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
  38. package/packages/dd-trace/src/debugger/devtools_client/index.js +30 -13
  39. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -8
  40. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
  41. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/status.js +12 -10
  43. package/packages/dd-trace/src/debugger/index.js +2 -13
  44. package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
  45. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
  46. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
  47. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
  48. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
  49. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
  50. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
  51. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
  52. package/packages/dd-trace/src/llmobs/tagger.js +11 -3
  53. package/packages/dd-trace/src/llmobs/util.js +7 -1
  54. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
  55. package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -27
  57. package/packages/dd-trace/src/plugins/util/test.js +42 -12
  58. package/packages/dd-trace/src/priority_sampler.js +4 -1
  59. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
  60. package/packages/dd-trace/src/profiling/profiler.js +11 -8
  61. package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
  62. package/packages/dd-trace/src/proxy.js +6 -3
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ const LangChainLLMObsHandler = require('.')
4
+ const LLMObsTagger = require('../../../tagger')
5
+ const { spanHasError } = require('../../../util')
6
+
7
+ class LangChainLLMObsEmbeddingHandler extends LangChainLLMObsHandler {
8
+ setMetaTags ({ span, inputs, results }) {
9
+ const isWorkflow = LLMObsTagger.getSpanKind(span) === 'workflow'
10
+ let embeddingInput, embeddingOutput
11
+
12
+ if (isWorkflow) {
13
+ embeddingInput = this.formatIO(inputs)
14
+ } else {
15
+ const input = Array.isArray(inputs) ? inputs : [inputs]
16
+ embeddingInput = input.map(doc => ({ text: doc }))
17
+ }
18
+
19
+ if (spanHasError(span) || !results) {
20
+ embeddingOutput = ''
21
+ } else {
22
+ let embeddingDimensions, embeddingsCount
23
+ if (typeof results[0] === 'number') {
24
+ embeddingsCount = 1
25
+ embeddingDimensions = results.length
26
+ } else {
27
+ embeddingsCount = results.length
28
+ embeddingDimensions = results[0].length
29
+ }
30
+
31
+ embeddingOutput = `[${embeddingsCount} embedding(s) returned with size ${embeddingDimensions}]`
32
+ }
33
+
34
+ if (isWorkflow) {
35
+ this._tagger.tagTextIO(span, embeddingInput, embeddingOutput)
36
+ } else {
37
+ this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput)
38
+ }
39
+ }
40
+ }
41
+
42
+ module.exports = LangChainLLMObsEmbeddingHandler
@@ -0,0 +1,102 @@
1
+ 'use strict'
2
+
3
+ const ROLE_MAPPINGS = {
4
+ human: 'user',
5
+ ai: 'assistant',
6
+ system: 'system'
7
+ }
8
+
9
+ class LangChainLLMObsHandler {
10
+ constructor (tagger) {
11
+ this._tagger = tagger
12
+ }
13
+
14
+ setMetaTags () {}
15
+
16
+ formatIO (messages) {
17
+ if (messages.constructor.name === 'Object') { // plain JSON
18
+ const formatted = {}
19
+ for (const [key, value] of Object.entries(messages)) {
20
+ formatted[key] = this.formatIO(value)
21
+ }
22
+
23
+ return formatted
24
+ } else if (Array.isArray(messages)) {
25
+ return messages.map(message => this.formatIO(message))
26
+ } else { // either a BaseMesage type or a string
27
+ return this.getContentFromMessage(messages)
28
+ }
29
+ }
30
+
31
+ getContentFromMessage (message) {
32
+ if (typeof message === 'string') {
33
+ return message
34
+ } else {
35
+ try {
36
+ const messageContent = {}
37
+ messageContent.content = message.content || ''
38
+
39
+ const role = this.getRole(message)
40
+ if (role) messageContent.role = role
41
+
42
+ return messageContent
43
+ } catch {
44
+ return JSON.stringify(message)
45
+ }
46
+ }
47
+ }
48
+
49
+ checkTokenUsageChatOrLLMResult (results) {
50
+ const llmOutput = results.llmOutput
51
+ const tokens = {
52
+ inputTokens: 0,
53
+ outputTokens: 0,
54
+ totalTokens: 0
55
+ }
56
+ if (!llmOutput) return tokens
57
+ const tokenUsage = llmOutput.tokenUsage || llmOutput.usageMetadata || llmOutput.usage || {}
58
+ if (!tokenUsage) return tokens
59
+
60
+ tokens.inputTokens = tokenUsage.promptTokens || tokenUsage.inputTokens || 0
61
+ tokens.outputTokens = tokenUsage.completionTokens || tokenUsage.outputTokens || 0
62
+ tokens.totalTokens = tokenUsage.totalTokens || tokens.inputTokens + tokens.outputTokens
63
+
64
+ return tokens
65
+ }
66
+
67
+ checkTokenUsageFromAIMessage (message) {
68
+ let usage = message.usage_metadata || message.additional_kwargs?.usage
69
+ const runId = message.run_id || message.id || ''
70
+ const runIdBase = runId ? runId.split('-').slice(0, -1).join('-') : ''
71
+
72
+ const responseMetadata = message.response_metadata || {}
73
+ usage = usage || responseMetadata.usage || responseMetadata.tokenUsage || {}
74
+
75
+ const inputTokens = usage.promptTokens || usage.inputTokens || usage.prompt_tokens || usage.input_tokens || 0
76
+ const outputTokens =
77
+ usage.completionTokens || usage.outputTokens || usage.completion_tokens || usage.output_tokens || 0
78
+ const totalTokens = usage.totalTokens || inputTokens + outputTokens
79
+
80
+ return {
81
+ tokens: {
82
+ inputTokens,
83
+ outputTokens,
84
+ totalTokens
85
+ },
86
+ runId: runIdBase
87
+ }
88
+ }
89
+
90
+ getRole (message) {
91
+ if (message.role) return ROLE_MAPPINGS[message.role] || message.role
92
+
93
+ const type = (
94
+ (typeof message.getType === 'function' && message.getType()) ||
95
+ (typeof message._getType === 'function' && message._getType())
96
+ )
97
+
98
+ return ROLE_MAPPINGS[type] || type
99
+ }
100
+ }
101
+
102
+ module.exports = LangChainLLMObsHandler
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const LangChainLLMObsHandler = require('.')
4
+ const LLMObsTagger = require('../../../tagger')
5
+ const { spanHasError } = require('../../../util')
6
+
7
+ class LangChainLLMObsLlmHandler extends LangChainLLMObsHandler {
8
+ setMetaTags ({ span, inputs, results }) {
9
+ const isWorkflow = LLMObsTagger.getSpanKind(span) === 'workflow'
10
+ const prompts = Array.isArray(inputs) ? inputs : [inputs]
11
+
12
+ let outputs
13
+ if (spanHasError(span)) {
14
+ outputs = [{ content: '' }]
15
+ } else {
16
+ outputs = results.generations.map(completion => ({ content: completion[0].text }))
17
+
18
+ if (!isWorkflow) {
19
+ const tokens = this.checkTokenUsageChatOrLLMResult(results)
20
+ this._tagger.tagMetrics(span, tokens)
21
+ }
22
+ }
23
+
24
+ if (isWorkflow) {
25
+ this._tagger.tagTextIO(span, prompts, outputs)
26
+ } else {
27
+ this._tagger.tagLLMIO(span, prompts, outputs)
28
+ }
29
+ }
30
+ }
31
+
32
+ module.exports = LangChainLLMObsLlmHandler
@@ -0,0 +1,131 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../../log')
4
+ const LLMObsPlugin = require('../base')
5
+
6
+ const pluginManager = require('../../../../../..')._pluginManager
7
+
8
+ const ANTHROPIC_PROVIDER_NAME = 'anthropic'
9
+ const BEDROCK_PROVIDER_NAME = 'amazon_bedrock'
10
+ const OPENAI_PROVIDER_NAME = 'openai'
11
+
12
+ const SUPPORTED_INTEGRATIONS = ['openai']
13
+ const LLM_SPAN_TYPES = ['llm', 'chat_model', 'embedding']
14
+ const LLM = 'llm'
15
+ const WORKFLOW = 'workflow'
16
+ const EMBEDDING = 'embedding'
17
+
18
+ const ChainHandler = require('./handlers/chain')
19
+ const ChatModelHandler = require('./handlers/chat_model')
20
+ const LlmHandler = require('./handlers/llm')
21
+ const EmbeddingHandler = require('./handlers/embedding')
22
+
23
+ class LangChainLLMObsPlugin extends LLMObsPlugin {
24
+ static get prefix () {
25
+ return 'tracing:apm:langchain:invoke'
26
+ }
27
+
28
+ constructor () {
29
+ super(...arguments)
30
+
31
+ this._handlers = {
32
+ chain: new ChainHandler(this._tagger),
33
+ chat_model: new ChatModelHandler(this._tagger),
34
+ llm: new LlmHandler(this._tagger),
35
+ embedding: new EmbeddingHandler(this._tagger)
36
+ }
37
+ }
38
+
39
+ getLLMObsSpanRegisterOptions (ctx) {
40
+ const span = ctx.currentStore?.span
41
+ const tags = span?.context()._tags || {}
42
+
43
+ const modelProvider = tags['langchain.request.provider'] // could be undefined
44
+ const modelName = tags['langchain.request.model'] // could be undefined
45
+ const kind = this.getKind(ctx.type, modelProvider)
46
+ const name = tags['resource.name']
47
+
48
+ return {
49
+ modelProvider,
50
+ modelName,
51
+ kind,
52
+ name
53
+ }
54
+ }
55
+
56
+ setLLMObsTags (ctx) {
57
+ const span = ctx.currentStore?.span
58
+ const type = ctx.type // langchain operation type (oneof chain,chat_model,llm,embedding)
59
+
60
+ if (!Object.keys(this._handlers).includes(type)) {
61
+ log.warn(`Unsupported LangChain operation type: ${type}`)
62
+ return
63
+ }
64
+
65
+ const provider = span?.context()._tags['langchain.request.provider']
66
+ const integrationName = this.getIntegrationName(type, provider)
67
+ this.setMetadata(span, provider)
68
+
69
+ const inputs = ctx.args?.[0]
70
+ const options = ctx.args?.[1]
71
+ const results = ctx.result
72
+
73
+ this._handlers[type].setMetaTags({ span, inputs, results, options, integrationName })
74
+ }
75
+
76
+ setMetadata (span, provider) {
77
+ if (!provider) return
78
+
79
+ const metadata = {}
80
+
81
+ // these fields won't be set for non model-based operations
82
+ const temperature =
83
+ span?.context()._tags[`langchain.request.${provider}.parameters.temperature`] ||
84
+ span?.context()._tags[`langchain.request.${provider}.parameters.model_kwargs.temperature`]
85
+
86
+ const maxTokens =
87
+ span?.context()._tags[`langchain.request.${provider}.parameters.max_tokens`] ||
88
+ span?.context()._tags[`langchain.request.${provider}.parameters.maxTokens`] ||
89
+ span?.context()._tags[`langchain.request.${provider}.parameters.model_kwargs.max_tokens`]
90
+
91
+ if (temperature) {
92
+ metadata.temperature = parseFloat(temperature)
93
+ }
94
+
95
+ if (maxTokens) {
96
+ metadata.maxTokens = parseInt(maxTokens)
97
+ }
98
+
99
+ this._tagger.tagMetadata(span, metadata)
100
+ }
101
+
102
+ getKind (type, provider) {
103
+ if (LLM_SPAN_TYPES.includes(type)) {
104
+ const llmobsIntegration = this.getIntegrationName(type, provider)
105
+
106
+ if (!this.isLLMIntegrationEnabled(llmobsIntegration)) {
107
+ return type === 'embedding' ? EMBEDDING : LLM
108
+ }
109
+ }
110
+
111
+ return WORKFLOW
112
+ }
113
+
114
+ getIntegrationName (type, provider = 'custom') {
115
+ if (provider.startsWith(BEDROCK_PROVIDER_NAME)) {
116
+ return 'bedrock'
117
+ } else if (provider.startsWith(OPENAI_PROVIDER_NAME)) {
118
+ return 'openai'
119
+ } else if (type === 'chat_model' && provider.startsWith(ANTHROPIC_PROVIDER_NAME)) {
120
+ return 'anthropic'
121
+ }
122
+
123
+ return provider
124
+ }
125
+
126
+ isLLMIntegrationEnabled (integration) {
127
+ return SUPPORTED_INTEGRATIONS.includes(integration) && pluginManager?._pluginsByName[integration]?.llmobs?._enabled
128
+ }
129
+ }
130
+
131
+ module.exports = LangChainLLMObsPlugin
@@ -7,7 +7,7 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
7
7
  return 'tracing:apm:openai:request'
8
8
  }
9
9
 
10
- getLLMObsSPanRegisterOptions (ctx) {
10
+ getLLMObsSpanRegisterOptions (ctx) {
11
11
  const resource = ctx.methodName
12
12
  const methodName = gateResource(normalizeOpenAIResourceName(resource))
13
13
  if (!methodName) return // we will not trace all openai methods for llmobs
@@ -40,6 +40,10 @@ class LLMObsTagger {
40
40
  return registry
41
41
  }
42
42
 
43
+ static getSpanKind (span) {
44
+ return registry.get(span)?.[SPAN_KIND]
45
+ }
46
+
43
47
  registerLLMObsSpan (span, {
44
48
  modelName,
45
49
  modelProvider,
@@ -60,10 +64,10 @@ class LLMObsTagger {
60
64
  if (modelName) this._setTag(span, MODEL_NAME, modelName)
61
65
  if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
62
66
 
63
- sessionId = sessionId || parent?.context()._tags[SESSION_ID]
67
+ sessionId = sessionId || registry.get(parent)?.[SESSION_ID]
64
68
  if (sessionId) this._setTag(span, SESSION_ID, sessionId)
65
69
 
66
- if (!mlApp) mlApp = parent?.context()._tags[ML_APP] || this._config.llmobs.mlApp
70
+ if (!mlApp) mlApp = registry.get(parent)?.[ML_APP] || this._config.llmobs.mlApp
67
71
  this._setTag(span, ML_APP, mlApp)
68
72
 
69
73
  const parentId =
@@ -136,6 +140,10 @@ class LLMObsTagger {
136
140
  this._setTag(span, TAGS, tags)
137
141
  }
138
142
 
143
+ changeKind (span, newKind) {
144
+ this._setTag(span, SPAN_KIND, newKind)
145
+ }
146
+
139
147
  _tagText (span, data, key) {
140
148
  if (data) {
141
149
  if (typeof data === 'string') {
@@ -310,7 +318,7 @@ class LLMObsTagger {
310
318
  _setTag (span, key, value) {
311
319
  if (!this._config.llmobs.enabled) return
312
320
  if (!registry.has(span)) {
313
- this._handleFailure('Span must be an LLMObs generated span.')
321
+ this._handleFailure(`Span "${span._name}" must be an LLMObs generated span.`)
314
322
  return
315
323
  }
316
324
 
@@ -169,8 +169,14 @@ function getFunctionArguments (fn, args = []) {
169
169
  }
170
170
  }
171
171
 
172
+ function spanHasError (span) {
173
+ const tags = span.context()._tags
174
+ return !!(tags.error || tags['error.type'])
175
+ }
176
+
172
177
  module.exports = {
173
178
  encodeUnicode,
174
179
  validateKind,
175
- getFunctionArguments
180
+ getFunctionArguments,
181
+ spanHasError
176
182
  }
@@ -10,10 +10,10 @@ const LLMObsBaseSpanWriter = require('./base')
10
10
  class LLMObsAgentProxySpanWriter extends LLMObsBaseSpanWriter {
11
11
  constructor (config) {
12
12
  super({
13
- intake: config.hostname || 'localhost',
14
- protocol: 'http:',
13
+ intake: config.url?.hostname || config.hostname || 'localhost',
14
+ protocol: config.url?.protocol || 'http:',
15
15
  endpoint: EVP_PROXY_AGENT_ENDPOINT,
16
- port: config.port
16
+ port: config.url?.port || config.port
17
17
  })
18
18
 
19
19
  this._headers[EVP_SUBDOMAIN_HEADER_NAME] = EVP_SUBDOMAIN_HEADER_VALUE
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { storage } = require('../../../datadog-core')
4
- const { trace, ROOT_CONTEXT } = require('@opentelemetry/api')
4
+ const { trace, ROOT_CONTEXT, propagation } = require('@opentelemetry/api')
5
5
  const DataDogSpanContext = require('../opentracing/span_context')
6
6
 
7
7
  const SpanContext = require('./span_context')
@@ -18,17 +18,40 @@ class ContextManager {
18
18
  const context = (activeSpan && activeSpan.context()) || store || ROOT_CONTEXT
19
19
 
20
20
  if (!(context instanceof DataDogSpanContext)) {
21
+ const span = trace.getSpan(context)
22
+ // span instanceof NonRecordingSpan
23
+ if (span && span._spanContext && span._spanContext._ddContext && span._spanContext._ddContext._baggageItems) {
24
+ const baggages = span._spanContext._ddContext._baggageItems
25
+ const entries = {}
26
+ for (const [key, value] of Object.entries(baggages)) {
27
+ entries[key] = { value }
28
+ }
29
+ const otelBaggages = propagation.createBaggage(entries)
30
+ return propagation.setBaggage(context, otelBaggages)
31
+ }
21
32
  return context
22
33
  }
23
34
 
35
+ const baggages = JSON.parse(activeSpan.getAllBaggageItems())
36
+ const entries = {}
37
+ for (const [key, value] of Object.entries(baggages)) {
38
+ entries[key] = { value }
39
+ }
40
+ const otelBaggages = propagation.createBaggage(entries)
41
+
24
42
  if (!context._otelSpanContext) {
25
43
  const newSpanContext = new SpanContext(context)
26
44
  context._otelSpanContext = newSpanContext
27
45
  }
28
46
  if (store && trace.getSpanContext(store) === context._otelSpanContext) {
29
- return store
47
+ return otelBaggages
48
+ ? propagation.setBaggage(store, otelBaggages)
49
+ : store
30
50
  }
31
- return trace.setSpanContext(store || ROOT_CONTEXT, context._otelSpanContext)
51
+ const wrappedContext = trace.setSpanContext(store || ROOT_CONTEXT, context._otelSpanContext)
52
+ return otelBaggages
53
+ ? propagation.setBaggage(wrappedContext, otelBaggages)
54
+ : wrappedContext
32
55
  }
33
56
 
34
57
  with (context, fn, thisArg, ...args) {
@@ -38,9 +61,26 @@ class ContextManager {
38
61
  const cb = thisArg == null ? fn : fn.bind(thisArg)
39
62
  return this._store.run(context, cb, ...args)
40
63
  }
64
+ const baggages = propagation.getBaggage(context)
65
+ let baggageItems = []
66
+ if (baggages) {
67
+ baggageItems = baggages.getAllEntries()
68
+ }
41
69
  if (span && span._ddSpan) {
70
+ // does otel always override datadog?
71
+ span._ddSpan.removeAllBaggageItems()
72
+ for (const baggage of baggageItems) {
73
+ span._ddSpan.setBaggageItem(baggage[0], baggage[1].value)
74
+ }
42
75
  return ddScope.activate(span._ddSpan, run)
43
76
  }
77
+ // span instanceof NonRecordingSpan
78
+ if (span && span._spanContext && span._spanContext._ddContext && span._spanContext._ddContext._baggageItems) {
79
+ span._spanContext._ddContext._baggageItems = {}
80
+ for (const baggage of baggageItems) {
81
+ span._spanContext._ddContext._baggageItems[baggage[0]] = baggage[1].value
82
+ }
83
+ }
44
84
  return run()
45
85
  }
46
86
 
@@ -23,7 +23,11 @@ const {
23
23
  TEST_LEVEL_EVENT_TYPES,
24
24
  TEST_SUITE,
25
25
  getFileAndLineNumberFromError,
26
- getTestSuitePath
26
+ DI_ERROR_DEBUG_INFO_CAPTURED,
27
+ DI_DEBUG_ERROR_PREFIX,
28
+ DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX,
29
+ DI_DEBUG_ERROR_FILE_SUFFIX,
30
+ DI_DEBUG_ERROR_LINE_SUFFIX
27
31
  } = require('./util/test')
28
32
  const Plugin = require('./plugin')
29
33
  const { COMPONENT } = require('../constants')
@@ -180,14 +184,18 @@ module.exports = class CiPlugin extends Plugin {
180
184
  }
181
185
  }
182
186
 
183
- configure (config) {
187
+ configure (config, shouldGetEnvironmentData = true) {
184
188
  super.configure(config)
185
189
 
186
- if (config.isTestDynamicInstrumentationEnabled) {
190
+ if (config.isTestDynamicInstrumentationEnabled && !this.di) {
187
191
  const testVisibilityDynamicInstrumentation = require('../ci-visibility/dynamic-instrumentation')
188
192
  this.di = testVisibilityDynamicInstrumentation
189
193
  }
190
194
 
195
+ if (!shouldGetEnvironmentData) {
196
+ return
197
+ }
198
+
191
199
  this.testEnvironmentMetadata = getTestEnvironmentMetadata(this.constructor.id, this.config)
192
200
 
193
201
  const {
@@ -292,37 +300,59 @@ module.exports = class CiPlugin extends Plugin {
292
300
  return testSpan
293
301
  }
294
302
 
295
- // TODO: If the test finishes and the probe is not hit, we should remove the breakpoint
296
- addDiProbe (err, probe) {
297
- const [file, line] = getFileAndLineNumberFromError(err)
303
+ onDiBreakpointHit ({ snapshot }) {
304
+ if (!this.activeTestSpan || this.activeTestSpan.context()._isFinished) {
305
+ // This is unexpected and is caused by a race condition.
306
+ log.warn('Breakpoint snapshot could not be attached to the active test span')
307
+ return
308
+ }
298
309
 
299
- const relativePath = getTestSuitePath(file, this.repositoryRoot)
310
+ const stackIndex = this.testErrorStackIndex
311
+
312
+ this.activeTestSpan.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true')
313
+ this.activeTestSpan.setTag(
314
+ `${DI_DEBUG_ERROR_PREFIX}.${stackIndex}.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}`,
315
+ snapshot.id
316
+ )
317
+ this.activeTestSpan.setTag(
318
+ `${DI_DEBUG_ERROR_PREFIX}.${stackIndex}.${DI_DEBUG_ERROR_FILE_SUFFIX}`,
319
+ snapshot.probe.location.file
320
+ )
321
+ this.activeTestSpan.setTag(
322
+ `${DI_DEBUG_ERROR_PREFIX}.${stackIndex}.${DI_DEBUG_ERROR_LINE_SUFFIX}`,
323
+ Number(snapshot.probe.location.lines[0])
324
+ )
325
+
326
+ const activeTestSpanContext = this.activeTestSpan.context()
327
+
328
+ this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, {
329
+ debugger: { snapshot },
330
+ dd: {
331
+ trace_id: activeTestSpanContext.toTraceId(),
332
+ span_id: activeTestSpanContext.toSpanId()
333
+ }
334
+ })
335
+ }
300
336
 
301
- const [
302
- snapshotId,
303
- setProbePromise,
304
- hitProbePromise
305
- ] = this.di.addLineProbe({ file: relativePath, line })
337
+ removeDiProbe (probeId) {
338
+ return this.di.removeProbe(probeId)
339
+ }
340
+
341
+ addDiProbe (err) {
342
+ const [file, line, stackIndex] = getFileAndLineNumberFromError(err, this.repositoryRoot)
306
343
 
307
- if (probe) { // not all frameworks may sync with the set probe promise
308
- probe.setProbePromise = setProbePromise
344
+ if (!file || !Number.isInteger(line)) {
345
+ log.warn('Could not add breakpoint for dynamic instrumentation')
346
+ return
309
347
  }
310
348
 
311
- hitProbePromise.then(({ snapshot }) => {
312
- // TODO: handle race conditions for this.retriedTestIds
313
- const { traceId, spanId } = this.retriedTestIds
314
- this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, {
315
- debugger: { snapshot },
316
- dd: {
317
- trace_id: traceId,
318
- span_id: spanId
319
- }
320
- })
321
- })
349
+ const [probeId, setProbePromise] = this.di.addLineProbe({ file, line }, this.onDiBreakpointHit.bind(this))
322
350
 
323
351
  return {
324
- snapshotId,
325
- file: relativePath,
352
+ probeId,
353
+ setProbePromise,
354
+ stackIndex,
355
+ file,
326
356
  line
327
357
  }
328
358
  }
@@ -88,6 +88,7 @@ const TEST_BROWSER_VERSION = 'test.browser.version'
88
88
  // jest worker variables
89
89
  const JEST_WORKER_TRACE_PAYLOAD_CODE = 60
90
90
  const JEST_WORKER_COVERAGE_PAYLOAD_CODE = 61
91
+ const JEST_WORKER_LOGS_PAYLOAD_CODE = 62
91
92
 
92
93
  // cucumber worker variables
93
94
  const CUCUMBER_WORKER_TRACE_PAYLOAD_CODE = 70
@@ -108,10 +109,10 @@ const TEST_LEVEL_EVENT_TYPES = [
108
109
 
109
110
  // Dynamic instrumentation - Test optimization integration tags
110
111
  const DI_ERROR_DEBUG_INFO_CAPTURED = 'error.debug_info_captured'
111
- // TODO: for the moment we'll only use a single snapshot id, so `0` is hardcoded
112
- const DI_DEBUG_ERROR_SNAPSHOT_ID = '_dd.debug.error.0.snapshot_id'
113
- const DI_DEBUG_ERROR_FILE = '_dd.debug.error.0.file'
114
- const DI_DEBUG_ERROR_LINE = '_dd.debug.error.0.line'
112
+ const DI_DEBUG_ERROR_PREFIX = '_dd.debug.error'
113
+ const DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX = 'snapshot_id'
114
+ const DI_DEBUG_ERROR_FILE_SUFFIX = 'file'
115
+ const DI_DEBUG_ERROR_LINE_SUFFIX = 'line'
115
116
 
116
117
  module.exports = {
117
118
  TEST_CODE_OWNERS,
@@ -134,6 +135,7 @@ module.exports = {
134
135
  LIBRARY_VERSION,
135
136
  JEST_WORKER_TRACE_PAYLOAD_CODE,
136
137
  JEST_WORKER_COVERAGE_PAYLOAD_CODE,
138
+ JEST_WORKER_LOGS_PAYLOAD_CODE,
137
139
  CUCUMBER_WORKER_TRACE_PAYLOAD_CODE,
138
140
  MOCHA_WORKER_TRACE_PAYLOAD_CODE,
139
141
  TEST_SOURCE_START,
@@ -191,9 +193,11 @@ module.exports = {
191
193
  getNumFromKnownTests,
192
194
  getFileAndLineNumberFromError,
193
195
  DI_ERROR_DEBUG_INFO_CAPTURED,
194
- DI_DEBUG_ERROR_SNAPSHOT_ID,
195
- DI_DEBUG_ERROR_FILE,
196
- DI_DEBUG_ERROR_LINE
196
+ DI_DEBUG_ERROR_PREFIX,
197
+ DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX,
198
+ DI_DEBUG_ERROR_FILE_SUFFIX,
199
+ DI_DEBUG_ERROR_LINE_SUFFIX,
200
+ getFormattedError
197
201
  }
198
202
 
199
203
  // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
@@ -650,13 +654,30 @@ function getNumFromKnownTests (knownTests) {
650
654
  return totalNumTests
651
655
  }
652
656
 
653
- function getFileAndLineNumberFromError (error) {
657
+ const DEPENDENCY_FOLDERS = [
658
+ 'node_modules',
659
+ 'node:',
660
+ '.pnpm',
661
+ '.yarn',
662
+ '.pnp'
663
+ ]
664
+
665
+ function getFileAndLineNumberFromError (error, repositoryRoot) {
654
666
  // Split the stack trace into individual lines
655
667
  const stackLines = error.stack.split('\n')
656
668
 
657
- // The top frame is usually the second line
658
- const topFrame = stackLines[1]
669
+ // Remove potential messages on top of the stack that are not frames
670
+ const frames = stackLines.filter(line => line.includes('at ') && line.includes(repositoryRoot))
671
+
672
+ const topRelevantFrameIndex = frames.findIndex(line =>
673
+ line.includes(repositoryRoot) && !DEPENDENCY_FOLDERS.some(pattern => line.includes(pattern))
674
+ )
675
+
676
+ if (topRelevantFrameIndex === -1) {
677
+ return []
678
+ }
659
679
 
680
+ const topFrame = frames[topRelevantFrameIndex]
660
681
  // Regular expression to match the file path, line number, and column number
661
682
  const regex = /\s*at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/
662
683
  const match = topFrame.match(regex)
@@ -664,9 +685,18 @@ function getFileAndLineNumberFromError (error) {
664
685
  if (match) {
665
686
  const filePath = match[1]
666
687
  const lineNumber = Number(match[2])
667
- const columnNumber = Number(match[3])
668
688
 
669
- return [filePath, lineNumber, columnNumber]
689
+ return [filePath, lineNumber, topRelevantFrameIndex]
670
690
  }
671
691
  return []
672
692
  }
693
+
694
+ function getFormattedError (error, repositoryRoot) {
695
+ const newError = new Error(error.message)
696
+ if (error.stack) {
697
+ newError.stack = error.stack.split('\n').filter(line => line.includes(repositoryRoot)).join('\n')
698
+ }
699
+ newError.name = error.name
700
+
701
+ return newError
702
+ }