dd-trace 5.30.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 (74) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +9 -7
  3. package/package.json +7 -6
  4. package/packages/datadog-core/src/storage.js +11 -2
  5. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  7. package/packages/datadog-instrumentations/src/cucumber.js +14 -5
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  9. package/packages/datadog-instrumentations/src/jest.js +70 -36
  10. package/packages/datadog-instrumentations/src/mocha/utils.js +23 -7
  11. package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
  12. package/packages/datadog-instrumentations/src/openai.js +2 -0
  13. package/packages/datadog-instrumentations/src/vitest.js +107 -59
  14. package/packages/datadog-instrumentations/src/vm.js +49 -0
  15. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime.js +295 -0
  16. package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
  17. package/packages/datadog-plugin-cucumber/src/index.js +30 -32
  18. package/packages/datadog-plugin-jest/src/index.js +34 -37
  19. package/packages/datadog-plugin-langchain/src/index.js +12 -80
  20. package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
  21. package/packages/datadog-plugin-mocha/src/index.js +18 -36
  22. package/packages/datadog-plugin-vitest/src/index.js +20 -34
  23. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  24. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
  25. package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
  26. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
  27. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  28. package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -1
  29. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
  30. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
  31. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
  32. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
  33. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
  34. package/packages/dd-trace/src/config.js +39 -3
  35. package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
  36. package/packages/dd-trace/src/crashtracking/noop.js +3 -0
  37. package/packages/dd-trace/src/datastreams/fnv.js +1 -1
  38. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
  39. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/index.js +32 -14
  42. package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +36 -0
  43. package/packages/dd-trace/src/debugger/devtools_client/send.js +29 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
  45. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/status.js +20 -11
  47. package/packages/dd-trace/src/debugger/index.js +2 -13
  48. package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
  49. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
  50. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
  51. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
  52. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
  53. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
  54. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
  55. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
  56. package/packages/dd-trace/src/llmobs/sdk.js +90 -26
  57. package/packages/dd-trace/src/llmobs/tagger.js +11 -3
  58. package/packages/dd-trace/src/llmobs/util.js +7 -1
  59. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
  60. package/packages/dd-trace/src/log/index.js +8 -9
  61. package/packages/dd-trace/src/noop/proxy.js +2 -2
  62. package/packages/dd-trace/src/noop/span.js +1 -1
  63. package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
  64. package/packages/dd-trace/src/opentracing/span.js +11 -1
  65. package/packages/dd-trace/src/opentracing/span_context.js +12 -0
  66. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -27
  67. package/packages/dd-trace/src/plugins/util/test.js +42 -12
  68. package/packages/dd-trace/src/priority_sampler.js +7 -2
  69. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
  70. package/packages/dd-trace/src/profiling/profiler.js +11 -8
  71. package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
  72. package/packages/dd-trace/src/proxy.js +6 -3
  73. package/packages/dd-trace/src/scope.js +1 -1
  74. package/packages/dd-trace/src/telemetry/index.js +2 -0
@@ -0,0 +1,111 @@
1
+ 'use strict'
2
+
3
+ const LangChainLLMObsHandler = require('.')
4
+ const LLMObsTagger = require('../../../tagger')
5
+ const { spanHasError } = require('../../../util')
6
+
7
+ const LLM = 'llm'
8
+
9
+ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
10
+ setMetaTags ({ span, inputs, results, options, integrationName }) {
11
+ if (integrationName === 'openai' && options?.response_format) {
12
+ // langchain-openai will call a beta client if "response_format" is passed in on the options object
13
+ // we do not trace these calls, so this should be an llm span
14
+ this._tagger.changeKind(span, LLM)
15
+ }
16
+ const spanKind = LLMObsTagger.getSpanKind(span)
17
+ const isWorkflow = spanKind === 'workflow'
18
+
19
+ const inputMessages = []
20
+ if (!Array.isArray(inputs)) inputs = [inputs]
21
+
22
+ for (const messageSet of inputs) {
23
+ for (const message of messageSet) {
24
+ const content = message.content || ''
25
+ const role = this.getRole(message)
26
+ inputMessages.push({ content, role })
27
+ }
28
+ }
29
+
30
+ if (spanHasError(span)) {
31
+ if (isWorkflow) {
32
+ this._tagger.tagTextIO(span, inputMessages, [{ content: '' }])
33
+ } else {
34
+ this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
35
+ }
36
+ return
37
+ }
38
+
39
+ const outputMessages = []
40
+ let inputTokens = 0
41
+ let outputTokens = 0
42
+ let totalTokens = 0
43
+ let tokensSetTopLevel = false
44
+ const tokensPerRunId = {}
45
+
46
+ if (!isWorkflow) {
47
+ const tokens = this.checkTokenUsageChatOrLLMResult(results)
48
+ inputTokens = tokens.inputTokens
49
+ outputTokens = tokens.outputTokens
50
+ totalTokens = tokens.totalTokens
51
+ tokensSetTopLevel = totalTokens > 0
52
+ }
53
+
54
+ for (const messageSet of results.generations) {
55
+ for (const chatCompletion of messageSet) {
56
+ const chatCompletionMessage = chatCompletion.message
57
+ const role = this.getRole(chatCompletionMessage)
58
+ const content = chatCompletionMessage.text || ''
59
+ const toolCalls = this.extractToolCalls(chatCompletionMessage)
60
+ outputMessages.push({ content, role, toolCalls })
61
+
62
+ if (!isWorkflow && !tokensSetTopLevel) {
63
+ const { tokens, runId } = this.checkTokenUsageFromAIMessage(chatCompletionMessage)
64
+ if (!tokensPerRunId[runId]) {
65
+ tokensPerRunId[runId] = tokens
66
+ } else {
67
+ tokensPerRunId[runId].inputTokens += tokens.inputTokens
68
+ tokensPerRunId[runId].outputTokens += tokens.outputTokens
69
+ tokensPerRunId[runId].totalTokens += tokens.totalTokens
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ if (!isWorkflow && !tokensSetTopLevel) {
76
+ inputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.inputTokens, 0)
77
+ outputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.outputTokens, 0)
78
+ totalTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.totalTokens, 0)
79
+ }
80
+
81
+ if (isWorkflow) {
82
+ this._tagger.tagTextIO(span, inputMessages, outputMessages)
83
+ } else {
84
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
85
+ this._tagger.tagMetrics(span, {
86
+ inputTokens,
87
+ outputTokens,
88
+ totalTokens
89
+ })
90
+ }
91
+ }
92
+
93
+ extractToolCalls (message) {
94
+ let toolCalls = message.tool_calls
95
+ if (!toolCalls) return []
96
+
97
+ const toolCallsInfo = []
98
+ if (!Array.isArray(toolCalls)) toolCalls = [toolCalls]
99
+ for (const toolCall of toolCalls) {
100
+ toolCallsInfo.push({
101
+ name: toolCall.name || '',
102
+ arguments: toolCall.args || {},
103
+ tool_id: toolCall.id || ''
104
+ })
105
+ }
106
+
107
+ return toolCallsInfo
108
+ }
109
+ }
110
+
111
+ module.exports = LangChainLLMObsChatModelHandler
@@ -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
@@ -1,12 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { SPAN_KIND, OUTPUT_VALUE } = require('./constants/tags')
3
+ const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags')
4
4
 
5
5
  const {
6
6
  getFunctionArguments,
7
7
  validateKind
8
8
  } = require('./util')
9
- const { isTrue } = require('../util')
9
+ const { isTrue, isError } = require('../util')
10
10
 
11
11
  const { storage } = require('./storage')
12
12
 
@@ -134,29 +134,63 @@ class LLMObs extends NoopLLMObs {
134
134
 
135
135
  function wrapped () {
136
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) })
137
+ const fnArgs = arguments
138
+
139
+ const lastArgId = fnArgs.length - 1
140
+ const cb = fnArgs[lastArgId]
141
+ const hasCallback = typeof cb === 'function'
142
+
143
+ if (hasCallback) {
144
+ const scopeBoundCb = llmobs._bind(cb)
145
+ fnArgs[lastArgId] = function () {
146
+ // it is standard practice to follow the callback signature (err, result)
147
+ // however, we try to parse the arguments to determine if the first argument is an error
148
+ // if it is not, and is not undefined, we will use that for the output value
149
+ const maybeError = arguments[0]
150
+ const maybeResult = arguments[1]
151
+
152
+ llmobs._autoAnnotate(
153
+ span,
154
+ kind,
155
+ getFunctionArguments(fn, fnArgs),
156
+ isError(maybeError) || maybeError == null ? maybeResult : maybeError
157
+ )
158
+
159
+ return scopeBoundCb.apply(this, arguments)
141
160
  }
161
+ }
142
162
 
143
- return fn.apply(this, arguments)
144
- })
163
+ try {
164
+ const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => fn.apply(this, fnArgs))
165
+
166
+ if (result && typeof result.then === 'function') {
167
+ return result.then(
168
+ value => {
169
+ if (!hasCallback) {
170
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), value)
171
+ }
172
+ return value
173
+ },
174
+ err => {
175
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
176
+ throw err
177
+ }
178
+ )
179
+ }
145
180
 
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
- }
181
+ // it is possible to return a value and have a callback
182
+ // however, since the span finishes when the callback is called, it is possible that
183
+ // the callback is called before the function returns (although unlikely)
184
+ // we do not want to throw for "annotating a finished span" in this case
185
+ if (!hasCallback) {
186
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), result)
187
+ }
154
188
 
155
- if (result && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
156
- llmobs.annotate(span, { outputData: result })
189
+ return result
190
+ } catch (e) {
191
+ llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
192
+ throw e
157
193
  }
158
-
159
- return result
160
194
  }
161
195
 
162
196
  return this._tracer.wrap(name, spanOptions, wrapped)
@@ -333,20 +367,34 @@ class LLMObs extends NoopLLMObs {
333
367
  flushCh.publish()
334
368
  }
335
369
 
370
+ _autoAnnotate (span, kind, input, output) {
371
+ const annotations = {}
372
+ if (input && !['llm', 'embedding'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[INPUT_VALUE]) {
373
+ annotations.inputData = input
374
+ }
375
+
376
+ if (output && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
377
+ annotations.outputData = output
378
+ }
379
+
380
+ this.annotate(span, annotations)
381
+ }
382
+
336
383
  _active () {
337
384
  const store = storage.getStore()
338
385
  return store?.span
339
386
  }
340
387
 
341
- _activate (span, { kind, options } = {}, fn) {
388
+ _activate (span, options, fn) {
342
389
  const parent = this._active()
343
390
  if (this.enabled) storage.enterWith({ span })
344
391
 
345
- this._tagger.registerLLMObsSpan(span, {
346
- ...options,
347
- parent,
348
- kind
349
- })
392
+ if (options) {
393
+ this._tagger.registerLLMObsSpan(span, {
394
+ ...options,
395
+ parent
396
+ })
397
+ }
350
398
 
351
399
  try {
352
400
  return fn()
@@ -355,6 +403,22 @@ class LLMObs extends NoopLLMObs {
355
403
  }
356
404
  }
357
405
 
406
+ // bind function to active LLMObs span
407
+ _bind (fn) {
408
+ if (typeof fn !== 'function') return fn
409
+
410
+ const llmobs = this
411
+ const activeSpan = llmobs._active()
412
+
413
+ const bound = function () {
414
+ return llmobs._activate(activeSpan, null, () => {
415
+ return fn.apply(this, arguments)
416
+ })
417
+ }
418
+
419
+ return bound
420
+ }
421
+
358
422
  _extractOptions (options) {
359
423
  const {
360
424
  modelName,
@@ -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
@@ -63,15 +63,14 @@ const log = {
63
63
 
64
64
  Error.captureStackTrace(logRecord, this.trace)
65
65
 
66
- const fn = logRecord.stack.split('\n')[1].replace(/^\s+at ([^\s]+) .+/, '$1')
67
- const params = args.map(a => {
68
- return a && a.hasOwnProperty('toString') && typeof a.toString === 'function'
69
- ? a.toString()
70
- : inspect(a, { depth: 3, breakLength: Infinity, compact: true })
71
- }).join(', ')
72
- const formatted = logRecord.stack.replace('Error: ', `Trace: ${fn}(${params})`)
73
-
74
- traceChannel.publish(Log.parse(formatted))
66
+ const stack = logRecord.stack.split('\n')
67
+ const fn = stack[1].replace(/^\s+at ([^\s]+) .+/, '$1')
68
+ const options = { depth: 2, breakLength: Infinity, compact: true, maxArrayLength: Infinity }
69
+ const params = args.map(a => inspect(a, options)).join(', ')
70
+
71
+ stack[0] = `Trace: ${fn}(${params})`
72
+
73
+ traceChannel.publish(Log.parse(stack.join('\n')))
75
74
  }
76
75
  return this
77
76
  },