dd-trace 5.24.0 → 5.26.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 (138) hide show
  1. package/LICENSE-3rdparty.csv +3 -0
  2. package/index.d.ts +345 -8
  3. package/init.js +60 -47
  4. package/package.json +16 -7
  5. package/packages/datadog-code-origin/index.js +4 -4
  6. package/packages/datadog-core/index.js +1 -3
  7. package/packages/datadog-core/src/storage.js +21 -0
  8. package/packages/datadog-core/src/utils/src/parse-tags.js +33 -0
  9. package/packages/datadog-esbuild/index.js +4 -2
  10. package/packages/datadog-instrumentations/src/amqplib.js +65 -5
  11. package/packages/datadog-instrumentations/src/child_process.js +135 -27
  12. package/packages/datadog-instrumentations/src/express.js +1 -1
  13. package/packages/datadog-instrumentations/src/handlebars.js +40 -0
  14. package/packages/datadog-instrumentations/src/helpers/hooks.js +5 -0
  15. package/packages/datadog-instrumentations/src/helpers/register.js +9 -0
  16. package/packages/datadog-instrumentations/src/jest.js +6 -2
  17. package/packages/datadog-instrumentations/src/kafkajs.js +123 -63
  18. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -2
  19. package/packages/datadog-instrumentations/src/multer.js +37 -0
  20. package/packages/datadog-instrumentations/src/openai.js +2 -2
  21. package/packages/datadog-instrumentations/src/pug.js +23 -0
  22. package/packages/datadog-instrumentations/src/router.js +2 -3
  23. package/packages/datadog-instrumentations/src/url.js +84 -0
  24. package/packages/datadog-instrumentations/src/utils/src/extract-package-and-module-path.js +7 -4
  25. package/packages/datadog-plugin-amqplib/src/consumer.js +6 -5
  26. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
  27. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -0
  28. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +10 -7
  29. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +35 -0
  30. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +11 -9
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +59 -45
  32. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  33. package/packages/datadog-plugin-fastify/src/code_origin.js +2 -2
  34. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +10 -2
  35. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +8 -0
  36. package/packages/datadog-plugin-grpc/src/client.js +3 -0
  37. package/packages/datadog-plugin-grpc/src/server.js +5 -1
  38. package/packages/datadog-plugin-http/src/client.js +42 -1
  39. package/packages/datadog-plugin-http2/src/client.js +26 -1
  40. package/packages/datadog-plugin-jest/src/index.js +2 -1
  41. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +6 -3
  42. package/packages/datadog-plugin-kafkajs/src/consumer.js +10 -5
  43. package/packages/datadog-plugin-kafkajs/src/producer.js +10 -4
  44. package/packages/datadog-plugin-mocha/src/index.js +5 -2
  45. package/packages/datadog-plugin-moleculer/src/server.js +2 -2
  46. package/packages/datadog-plugin-openai/src/index.js +9 -1015
  47. package/packages/datadog-plugin-openai/src/tracing.js +1023 -0
  48. package/packages/datadog-plugin-rhea/src/consumer.js +2 -1
  49. package/packages/datadog-plugin-vitest/src/index.js +2 -1
  50. package/packages/dd-trace/src/appsec/addresses.js +2 -0
  51. package/packages/dd-trace/src/appsec/api_security_sampler.js +50 -27
  52. package/packages/dd-trace/src/appsec/channels.js +3 -1
  53. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  54. package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +33 -16
  55. package/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +18 -0
  56. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +55 -7
  57. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -2
  58. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  59. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -2
  60. package/packages/dd-trace/src/appsec/index.js +9 -6
  61. package/packages/dd-trace/src/appsec/rasp/command_injection.js +49 -0
  62. package/packages/dd-trace/src/appsec/rasp/index.js +3 -0
  63. package/packages/dd-trace/src/appsec/rasp/ssrf.js +4 -3
  64. package/packages/dd-trace/src/appsec/rasp/utils.js +3 -2
  65. package/packages/dd-trace/src/appsec/recommended.json +354 -158
  66. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  67. package/packages/dd-trace/src/appsec/remote_config/index.js +2 -7
  68. package/packages/dd-trace/src/appsec/reporter.js +6 -4
  69. package/packages/dd-trace/src/appsec/sdk/track_event.js +5 -3
  70. package/packages/dd-trace/src/appsec/waf/waf_manager.js +4 -0
  71. package/packages/dd-trace/src/azure_metadata.js +120 -0
  72. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +97 -0
  73. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +90 -0
  74. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +19 -1
  75. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +53 -0
  76. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +8 -1
  77. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +43 -0
  78. package/packages/dd-trace/src/config.js +88 -10
  79. package/packages/dd-trace/src/constants.js +8 -1
  80. package/packages/dd-trace/src/crashtracking/crashtracker.js +98 -0
  81. package/packages/dd-trace/src/crashtracking/index.js +15 -0
  82. package/packages/dd-trace/src/crashtracking/noop.js +8 -0
  83. package/packages/dd-trace/src/datastreams/pathway.js +1 -0
  84. package/packages/dd-trace/src/debugger/devtools_client/index.js +9 -13
  85. package/packages/dd-trace/src/debugger/devtools_client/send.js +15 -1
  86. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +57 -23
  87. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +12 -2
  88. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +31 -20
  89. package/packages/dd-trace/src/debugger/devtools_client/snapshot/symbols.js +6 -0
  90. package/packages/dd-trace/src/debugger/devtools_client/state.js +11 -2
  91. package/packages/dd-trace/src/debugger/index.js +10 -3
  92. package/packages/dd-trace/src/llmobs/constants/tags.js +34 -0
  93. package/packages/dd-trace/src/llmobs/constants/text.js +6 -0
  94. package/packages/dd-trace/src/llmobs/constants/writers.js +13 -0
  95. package/packages/dd-trace/src/llmobs/index.js +103 -0
  96. package/packages/dd-trace/src/llmobs/noop.js +82 -0
  97. package/packages/dd-trace/src/llmobs/plugins/base.js +65 -0
  98. package/packages/dd-trace/src/llmobs/plugins/openai.js +205 -0
  99. package/packages/dd-trace/src/llmobs/sdk.js +377 -0
  100. package/packages/dd-trace/src/llmobs/span_processor.js +195 -0
  101. package/packages/dd-trace/src/llmobs/storage.js +7 -0
  102. package/packages/dd-trace/src/llmobs/tagger.js +322 -0
  103. package/packages/dd-trace/src/llmobs/util.js +176 -0
  104. package/packages/dd-trace/src/llmobs/writers/base.js +111 -0
  105. package/packages/dd-trace/src/llmobs/writers/evaluations.js +29 -0
  106. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +23 -0
  107. package/packages/dd-trace/src/llmobs/writers/spans/agentless.js +17 -0
  108. package/packages/dd-trace/src/llmobs/writers/spans/base.js +52 -0
  109. package/packages/dd-trace/src/log/index.js +10 -13
  110. package/packages/dd-trace/src/log/log.js +52 -0
  111. package/packages/dd-trace/src/log/writer.js +50 -19
  112. package/packages/dd-trace/src/noop/proxy.js +3 -0
  113. package/packages/dd-trace/src/noop/span.js +4 -0
  114. package/packages/dd-trace/src/opentelemetry/span.js +16 -1
  115. package/packages/dd-trace/src/opentelemetry/tracer.js +1 -0
  116. package/packages/dd-trace/src/opentracing/propagation/text_map.js +106 -32
  117. package/packages/dd-trace/src/opentracing/span.js +26 -0
  118. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  119. package/packages/dd-trace/src/opentracing/tracer.js +8 -1
  120. package/packages/dd-trace/src/payload-tagging/config/aws.json +71 -3
  121. package/packages/dd-trace/src/plugins/outbound.js +9 -0
  122. package/packages/dd-trace/src/plugins/tracing.js +3 -3
  123. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +121 -0
  124. package/packages/dd-trace/src/plugins/util/ip_extractor.js +0 -1
  125. package/packages/dd-trace/src/plugins/util/web.js +39 -11
  126. package/packages/dd-trace/src/priority_sampler.js +16 -0
  127. package/packages/dd-trace/src/profiling/config.js +3 -1
  128. package/packages/dd-trace/src/profiling/exporters/agent.js +7 -5
  129. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -1
  130. package/packages/dd-trace/src/proxy.js +13 -1
  131. package/packages/dd-trace/src/span_processor.js +5 -0
  132. package/packages/dd-trace/src/telemetry/index.js +11 -1
  133. package/packages/dd-trace/src/telemetry/logs/index.js +16 -11
  134. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -8
  135. package/packages/dd-trace/src/telemetry/metrics.js +6 -1
  136. package/packages/dd-trace/src/util.js +16 -1
  137. package/version.js +4 -2
  138. /package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/{code-injection-sensitive-analyzer.js → tainted-range-based-sensitive-analyzer.js} +0 -0
@@ -0,0 +1,65 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+ const { storage } = require('../storage')
5
+
6
+ const TracingPlugin = require('../../plugins/tracing')
7
+ const LLMObsTagger = require('../tagger')
8
+
9
+ // we make this a `Plugin` so we don't have to worry about `finish` being called
10
+ class LLMObsPlugin extends TracingPlugin {
11
+ constructor (...args) {
12
+ super(...args)
13
+
14
+ this._tagger = new LLMObsTagger(this._tracerConfig, true)
15
+ }
16
+
17
+ getName () {}
18
+
19
+ setLLMObsTags (ctx) {
20
+ throw new Error('setLLMObsTags must be implemented by the subclass')
21
+ }
22
+
23
+ getLLMObsSPanRegisterOptions (ctx) {
24
+ throw new Error('getLLMObsSPanRegisterOptions must be implemented by the subclass')
25
+ }
26
+
27
+ start (ctx) {
28
+ const oldStore = storage.getStore()
29
+ const parent = oldStore?.span
30
+ const span = ctx.currentStore?.span
31
+
32
+ const registerOptions = this.getLLMObsSPanRegisterOptions(ctx)
33
+
34
+ this._tagger.registerLLMObsSpan(span, { parent, ...registerOptions })
35
+ }
36
+
37
+ asyncEnd (ctx) {
38
+ // even though llmobs span events won't be enqueued if llmobs is disabled
39
+ // we should avoid doing any computations here (these listeners aren't disabled)
40
+ const enabled = this._tracerConfig.llmobs.enabled
41
+ if (!enabled) return
42
+
43
+ const span = ctx.currentStore?.span
44
+ if (!span) {
45
+ log.debug(
46
+ `Tried to start an LLMObs span for ${this.constructor.name} without an active APM span.
47
+ Not starting LLMObs span.`
48
+ )
49
+ return
50
+ }
51
+
52
+ this.setLLMObsTags(ctx)
53
+ }
54
+
55
+ configure (config) {
56
+ // we do not want to enable any LLMObs plugins if it is disabled on the tracer
57
+ const llmobsEnabled = this._tracerConfig.llmobs.enabled
58
+ if (llmobsEnabled === false) {
59
+ config = typeof config === 'boolean' ? false : { ...config, enabled: false } // override to false
60
+ }
61
+ super.configure(config)
62
+ }
63
+ }
64
+
65
+ module.exports = LLMObsPlugin
@@ -0,0 +1,205 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('./base')
4
+
5
+ class OpenAiLLMObsPlugin extends LLMObsPlugin {
6
+ static get prefix () {
7
+ return 'tracing:apm:openai:request'
8
+ }
9
+
10
+ getLLMObsSPanRegisterOptions (ctx) {
11
+ const resource = ctx.methodName
12
+ const methodName = gateResource(normalizeOpenAIResourceName(resource))
13
+ if (!methodName) return // we will not trace all openai methods for llmobs
14
+
15
+ const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument
16
+ const operation = getOperation(methodName)
17
+ const kind = operation === 'embedding' ? 'embedding' : 'llm'
18
+ const name = `openai.${methodName}`
19
+
20
+ return {
21
+ modelProvider: 'openai',
22
+ modelName: inputs.model,
23
+ kind,
24
+ name
25
+ }
26
+ }
27
+
28
+ setLLMObsTags (ctx) {
29
+ const span = ctx.currentStore?.span
30
+ const resource = ctx.methodName
31
+ const methodName = gateResource(normalizeOpenAIResourceName(resource))
32
+ if (!methodName) return // we will not trace all openai methods for llmobs
33
+
34
+ const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument
35
+ const response = ctx.result?.data // no result if error
36
+ const error = !!span.context()._tags.error
37
+
38
+ const operation = getOperation(methodName)
39
+
40
+ if (operation === 'completion') {
41
+ this._tagCompletion(span, inputs, response, error)
42
+ } else if (operation === 'chat') {
43
+ this._tagChatCompletion(span, inputs, response, error)
44
+ } else if (operation === 'embedding') {
45
+ this._tagEmbedding(span, inputs, response, error)
46
+ }
47
+
48
+ if (!error) {
49
+ const metrics = this._extractMetrics(response)
50
+ this._tagger.tagMetrics(span, metrics)
51
+ }
52
+ }
53
+
54
+ _extractMetrics (response) {
55
+ const metrics = {}
56
+ const tokenUsage = response.usage
57
+
58
+ if (tokenUsage) {
59
+ const inputTokens = tokenUsage.prompt_tokens
60
+ if (inputTokens) metrics.inputTokens = inputTokens
61
+
62
+ const outputTokens = tokenUsage.completion_tokens
63
+ if (outputTokens) metrics.outputTokens = outputTokens
64
+
65
+ const totalTokens = tokenUsage.total_toksn || (inputTokens + outputTokens)
66
+ if (totalTokens) metrics.totalTokens = totalTokens
67
+ }
68
+
69
+ return metrics
70
+ }
71
+
72
+ _tagEmbedding (span, inputs, response, error) {
73
+ const { model, ...parameters } = inputs
74
+
75
+ const metadata = {
76
+ encoding_format: parameters.encoding_format || 'float'
77
+ }
78
+ if (inputs.dimensions) metadata.dimensions = inputs.dimensions
79
+ this._tagger.tagMetadata(span, metadata)
80
+
81
+ let embeddingInputs = inputs.input
82
+ if (!Array.isArray(embeddingInputs)) embeddingInputs = [embeddingInputs]
83
+ const embeddingInput = embeddingInputs.map(input => ({ text: input }))
84
+
85
+ if (error) {
86
+ this._tagger.tagEmbeddingIO(span, embeddingInput, undefined)
87
+ return
88
+ }
89
+
90
+ const float = Array.isArray(response.data[0].embedding)
91
+ let embeddingOutput
92
+ if (float) {
93
+ const embeddingDim = response.data[0].embedding.length
94
+ embeddingOutput = `[${response.data.length} embedding(s) returned with size ${embeddingDim}]`
95
+ } else {
96
+ embeddingOutput = `[${response.data.length} embedding(s) returned]`
97
+ }
98
+
99
+ this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput)
100
+ }
101
+
102
+ _tagCompletion (span, inputs, response, error) {
103
+ let { prompt, model, ...parameters } = inputs
104
+ if (!Array.isArray(prompt)) prompt = [prompt]
105
+
106
+ const completionInput = prompt.map(p => ({ content: p }))
107
+
108
+ const completionOutput = error ? [{ content: '' }] : response.choices.map(choice => ({ content: choice.text }))
109
+
110
+ this._tagger.tagLLMIO(span, completionInput, completionOutput)
111
+ this._tagger.tagMetadata(span, parameters)
112
+ }
113
+
114
+ _tagChatCompletion (span, inputs, response, error) {
115
+ const { messages, model, ...parameters } = inputs
116
+
117
+ if (error) {
118
+ this._tagger.tagLLMIO(span, messages, [{ content: '' }])
119
+ return
120
+ }
121
+
122
+ const outputMessages = []
123
+ const { choices } = response
124
+ for (const choice of choices) {
125
+ const message = choice.message || choice.delta
126
+ const content = message.content || ''
127
+ const role = message.role
128
+
129
+ if (message.function_call) {
130
+ const functionCallInfo = {
131
+ name: message.function_call.name,
132
+ arguments: JSON.parse(message.function_call.arguments)
133
+ }
134
+ outputMessages.push({ content, role, toolCalls: [functionCallInfo] })
135
+ } else if (message.tool_calls) {
136
+ const toolCallsInfo = []
137
+ for (const toolCall of message.tool_calls) {
138
+ const toolCallInfo = {
139
+ arguments: JSON.parse(toolCall.function.arguments),
140
+ name: toolCall.function.name,
141
+ toolId: toolCall.id,
142
+ type: toolCall.type
143
+ }
144
+ toolCallsInfo.push(toolCallInfo)
145
+ }
146
+ outputMessages.push({ content, role, toolCalls: toolCallsInfo })
147
+ } else {
148
+ outputMessages.push({ content, role })
149
+ }
150
+ }
151
+
152
+ this._tagger.tagLLMIO(span, messages, outputMessages)
153
+
154
+ const metadata = Object.entries(parameters).reduce((obj, [key, value]) => {
155
+ if (!['tools', 'functions'].includes(key)) {
156
+ obj[key] = value
157
+ }
158
+
159
+ return obj
160
+ }, {})
161
+
162
+ this._tagger.tagMetadata(span, metadata)
163
+ }
164
+ }
165
+
166
+ // TODO: this will be moved to the APM integration
167
+ function normalizeOpenAIResourceName (resource) {
168
+ switch (resource) {
169
+ // completions
170
+ case 'completions.create':
171
+ return 'createCompletion'
172
+
173
+ // chat completions
174
+ case 'chat.completions.create':
175
+ return 'createChatCompletion'
176
+
177
+ // embeddings
178
+ case 'embeddings.create':
179
+ return 'createEmbedding'
180
+ default:
181
+ return resource
182
+ }
183
+ }
184
+
185
+ function gateResource (resource) {
186
+ return ['createCompletion', 'createChatCompletion', 'createEmbedding'].includes(resource)
187
+ ? resource
188
+ : undefined
189
+ }
190
+
191
+ function getOperation (resource) {
192
+ switch (resource) {
193
+ case 'createCompletion':
194
+ return 'completion'
195
+ case 'createChatCompletion':
196
+ return 'chat'
197
+ case 'createEmbedding':
198
+ return 'embedding'
199
+ default:
200
+ // should never happen
201
+ return 'unknown'
202
+ }
203
+ }
204
+
205
+ module.exports = OpenAiLLMObsPlugin
@@ -0,0 +1,377 @@
1
+ 'use strict'
2
+
3
+ const { SPAN_KIND, OUTPUT_VALUE } = require('./constants/tags')
4
+
5
+ const {
6
+ getFunctionArguments,
7
+ validateKind
8
+ } = require('./util')
9
+ const { isTrue } = require('../util')
10
+
11
+ const { storage } = require('./storage')
12
+
13
+ const Span = require('../opentracing/span')
14
+
15
+ const tracerVersion = require('../../../../package.json').version
16
+ const logger = require('../log')
17
+
18
+ const LLMObsTagger = require('./tagger')
19
+
20
+ // communicating with writer
21
+ const { channel } = require('dc-polyfill')
22
+ const evalMetricAppendCh = channel('llmobs:eval-metric:append')
23
+ const flushCh = channel('llmobs:writers:flush')
24
+ const NoopLLMObs = require('./noop')
25
+
26
+ class LLMObs extends NoopLLMObs {
27
+ constructor (tracer, llmobsModule, config) {
28
+ super(tracer)
29
+
30
+ this._config = config
31
+ this._llmobsModule = llmobsModule
32
+ this._tagger = new LLMObsTagger(config)
33
+ }
34
+
35
+ get enabled () {
36
+ return this._config.llmobs.enabled
37
+ }
38
+
39
+ enable (options = {}) {
40
+ if (this.enabled) {
41
+ logger.debug('LLMObs is already enabled.')
42
+ return
43
+ }
44
+
45
+ logger.debug('Enabling LLMObs')
46
+
47
+ const { mlApp, agentlessEnabled } = options
48
+
49
+ const { DD_LLMOBS_ENABLED } = process.env
50
+
51
+ const llmobsConfig = {
52
+ mlApp,
53
+ agentlessEnabled
54
+ }
55
+
56
+ const enabled = DD_LLMOBS_ENABLED == null || isTrue(DD_LLMOBS_ENABLED)
57
+ if (!enabled) {
58
+ logger.debug('LLMObs.enable() called when DD_LLMOBS_ENABLED is false. No action taken.')
59
+ return
60
+ }
61
+
62
+ this._config.llmobs.enabled = true
63
+ this._config.configure({ ...this._config, llmobs: llmobsConfig })
64
+
65
+ // configure writers and channel subscribers
66
+ this._llmobsModule.enable(this._config)
67
+ }
68
+
69
+ disable () {
70
+ if (!this.enabled) {
71
+ logger.debug('LLMObs is already disabled.')
72
+ return
73
+ }
74
+
75
+ logger.debug('Disabling LLMObs')
76
+
77
+ this._config.llmobs.enabled = false
78
+
79
+ // disable writers and channel subscribers
80
+ this._llmobsModule.disable()
81
+ }
82
+
83
+ trace (options = {}, fn) {
84
+ if (typeof options === 'function') {
85
+ fn = options
86
+ options = {}
87
+ }
88
+
89
+ const kind = validateKind(options.kind) // will throw if kind is undefined or not an expected kind
90
+
91
+ // name is required for spans generated with `trace`
92
+ // while `kind` is required, this should never throw (as otherwise it would have thrown above)
93
+ const name = options.name || kind
94
+ if (!name) {
95
+ throw new Error('No span name provided for `trace`.')
96
+ }
97
+
98
+ const {
99
+ spanOptions,
100
+ ...llmobsOptions
101
+ } = this._extractOptions(options)
102
+
103
+ if (fn.length > 1) {
104
+ return this._tracer.trace(name, spanOptions, (span, cb) =>
105
+ this._activate(span, { kind, options: llmobsOptions }, () => fn(span, cb))
106
+ )
107
+ }
108
+
109
+ return this._tracer.trace(name, spanOptions, span =>
110
+ this._activate(span, { kind, options: llmobsOptions }, () => fn(span))
111
+ )
112
+ }
113
+
114
+ wrap (options = {}, fn) {
115
+ if (typeof options === 'function') {
116
+ fn = options
117
+ options = {}
118
+ }
119
+
120
+ const kind = validateKind(options.kind) // will throw if kind is undefined or not an expected kind
121
+ let name = options.name || (fn?.name ? fn.name : undefined) || kind
122
+
123
+ if (!name) {
124
+ logger.warn('No span name provided for `wrap`. Defaulting to "unnamed-anonymous-function".')
125
+ name = 'unnamed-anonymous-function'
126
+ }
127
+
128
+ const {
129
+ spanOptions,
130
+ ...llmobsOptions
131
+ } = this._extractOptions(options)
132
+
133
+ const llmobs = this
134
+
135
+ function wrapped () {
136
+ const span = llmobs._tracer.scope().active()
137
+
138
+ const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => {
139
+ if (!['llm', 'embedding'].includes(kind)) {
140
+ llmobs.annotate(span, { inputData: getFunctionArguments(fn, arguments) })
141
+ }
142
+
143
+ return fn.apply(this, arguments)
144
+ })
145
+
146
+ if (result && typeof result.then === 'function') {
147
+ return result.then(value => {
148
+ if (value && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
149
+ llmobs.annotate(span, { outputData: value })
150
+ }
151
+ return value
152
+ })
153
+ }
154
+
155
+ if (result && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
156
+ llmobs.annotate(span, { outputData: result })
157
+ }
158
+
159
+ return result
160
+ }
161
+
162
+ return this._tracer.wrap(name, spanOptions, wrapped)
163
+ }
164
+
165
+ annotate (span, options) {
166
+ if (!this.enabled) return
167
+
168
+ if (!span) {
169
+ span = this._active()
170
+ }
171
+
172
+ if ((span && !options) && !(span instanceof Span)) {
173
+ options = span
174
+ span = this._active()
175
+ }
176
+
177
+ if (!span) {
178
+ throw new Error('No span provided and no active LLMObs-generated span found')
179
+ }
180
+ if (!options) {
181
+ throw new Error('No options provided for annotation.')
182
+ }
183
+
184
+ if (!LLMObsTagger.tagMap.has(span)) {
185
+ throw new Error('Span must be an LLMObs-generated span')
186
+ }
187
+ if (span._duration !== undefined) {
188
+ throw new Error('Cannot annotate a finished span')
189
+ }
190
+
191
+ const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
192
+ if (!spanKind) {
193
+ throw new Error('LLMObs span must have a span kind specified')
194
+ }
195
+
196
+ const { inputData, outputData, metadata, metrics, tags } = options
197
+
198
+ if (inputData || outputData) {
199
+ if (spanKind === 'llm') {
200
+ this._tagger.tagLLMIO(span, inputData, outputData)
201
+ } else if (spanKind === 'embedding') {
202
+ this._tagger.tagEmbeddingIO(span, inputData, outputData)
203
+ } else if (spanKind === 'retrieval') {
204
+ this._tagger.tagRetrievalIO(span, inputData, outputData)
205
+ } else {
206
+ this._tagger.tagTextIO(span, inputData, outputData)
207
+ }
208
+ }
209
+
210
+ if (metadata) {
211
+ this._tagger.tagMetadata(span, metadata)
212
+ }
213
+
214
+ if (metrics) {
215
+ this._tagger.tagMetrics(span, metrics)
216
+ }
217
+
218
+ if (tags) {
219
+ this._tagger.tagSpanTags(span, tags)
220
+ }
221
+ }
222
+
223
+ exportSpan (span) {
224
+ span = span || this._active()
225
+
226
+ if (!span) {
227
+ throw new Error('No span provided and no active LLMObs-generated span found')
228
+ }
229
+
230
+ if (!(span instanceof Span)) {
231
+ throw new Error('Span must be a valid Span object.')
232
+ }
233
+
234
+ if (!LLMObsTagger.tagMap.has(span)) {
235
+ throw new Error('Span must be an LLMObs-generated span')
236
+ }
237
+
238
+ try {
239
+ return {
240
+ traceId: span.context().toTraceId(true),
241
+ spanId: span.context().toSpanId()
242
+ }
243
+ } catch {
244
+ logger.warn('Faild to export span. Span must be a valid Span object.')
245
+ }
246
+ }
247
+
248
+ submitEvaluation (llmobsSpanContext, options = {}) {
249
+ if (!this.enabled) return
250
+
251
+ if (!this._config.apiKey) {
252
+ throw new Error(
253
+ 'DD_API_KEY is required for sending evaluation metrics. Evaluation metric data will not be sent.\n' +
254
+ 'Ensure this configuration is set before running your application.'
255
+ )
256
+ }
257
+
258
+ const { traceId, spanId } = llmobsSpanContext
259
+ if (!traceId || !spanId) {
260
+ throw new Error(
261
+ 'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
262
+ )
263
+ }
264
+
265
+ const mlApp = options.mlApp || this._config.llmobs.mlApp
266
+ if (!mlApp) {
267
+ throw new Error(
268
+ 'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
269
+ )
270
+ }
271
+
272
+ const timestampMs = options.timestampMs || Date.now()
273
+ if (typeof timestampMs !== 'number' || timestampMs < 0) {
274
+ throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
275
+ }
276
+
277
+ const { label, value, tags } = options
278
+ const metricType = options.metricType?.toLowerCase()
279
+ if (!label) {
280
+ throw new Error('label must be the specified name of the evaluation metric')
281
+ }
282
+ if (!metricType || !['categorical', 'score'].includes(metricType)) {
283
+ throw new Error('metricType must be one of "categorical" or "score"')
284
+ }
285
+
286
+ if (metricType === 'categorical' && typeof value !== 'string') {
287
+ throw new Error('value must be a string for a categorical metric.')
288
+ }
289
+ if (metricType === 'score' && typeof value !== 'number') {
290
+ throw new Error('value must be a number for a score metric.')
291
+ }
292
+
293
+ const evaluationTags = {
294
+ 'ddtrace.version': tracerVersion,
295
+ ml_app: mlApp
296
+ }
297
+
298
+ if (tags) {
299
+ for (const key in tags) {
300
+ const tag = tags[key]
301
+ if (typeof tag === 'string') {
302
+ evaluationTags[key] = tag
303
+ } else if (typeof tag.toString === 'function') {
304
+ evaluationTags[key] = tag.toString()
305
+ } else if (tag == null) {
306
+ evaluationTags[key] = Object.prototype.toString.call(tag)
307
+ } else {
308
+ // should be a rare case
309
+ // every object in JS has a toString, otherwise every primitive has its own toString
310
+ // null and undefined are handled above
311
+ throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
312
+ }
313
+ }
314
+ }
315
+
316
+ const payload = {
317
+ span_id: spanId,
318
+ trace_id: traceId,
319
+ label,
320
+ metric_type: metricType,
321
+ ml_app: mlApp,
322
+ [`${metricType}_value`]: value,
323
+ timestamp_ms: timestampMs,
324
+ tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
325
+ }
326
+
327
+ evalMetricAppendCh.publish(payload)
328
+ }
329
+
330
+ flush () {
331
+ if (!this.enabled) return
332
+
333
+ flushCh.publish()
334
+ }
335
+
336
+ _active () {
337
+ const store = storage.getStore()
338
+ return store?.span
339
+ }
340
+
341
+ _activate (span, { kind, options } = {}, fn) {
342
+ const parent = this._active()
343
+ if (this.enabled) storage.enterWith({ span })
344
+
345
+ this._tagger.registerLLMObsSpan(span, {
346
+ ...options,
347
+ parent,
348
+ kind
349
+ })
350
+
351
+ try {
352
+ return fn()
353
+ } finally {
354
+ if (this.enabled) storage.enterWith({ span: parent })
355
+ }
356
+ }
357
+
358
+ _extractOptions (options) {
359
+ const {
360
+ modelName,
361
+ modelProvider,
362
+ sessionId,
363
+ mlApp,
364
+ ...spanOptions
365
+ } = options
366
+
367
+ return {
368
+ mlApp,
369
+ modelName,
370
+ modelProvider,
371
+ sessionId,
372
+ spanOptions
373
+ }
374
+ }
375
+ }
376
+
377
+ module.exports = LLMObs