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,195 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ SPAN_KIND,
5
+ MODEL_NAME,
6
+ MODEL_PROVIDER,
7
+ METADATA,
8
+ INPUT_MESSAGES,
9
+ INPUT_VALUE,
10
+ OUTPUT_MESSAGES,
11
+ INPUT_DOCUMENTS,
12
+ OUTPUT_DOCUMENTS,
13
+ OUTPUT_VALUE,
14
+ METRICS,
15
+ ML_APP,
16
+ TAGS,
17
+ PARENT_ID_KEY,
18
+ SESSION_ID,
19
+ NAME
20
+ } = require('./constants/tags')
21
+ const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
22
+
23
+ const {
24
+ ERROR_MESSAGE,
25
+ ERROR_TYPE,
26
+ ERROR_STACK
27
+ } = require('../constants')
28
+
29
+ const LLMObsTagger = require('./tagger')
30
+
31
+ const tracerVersion = require('../../../../package.json').version
32
+ const logger = require('../log')
33
+
34
+ class LLMObsSpanProcessor {
35
+ constructor (config) {
36
+ this._config = config
37
+ }
38
+
39
+ setWriter (writer) {
40
+ this._writer = writer
41
+ }
42
+
43
+ // TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
44
+ process ({ span }) {
45
+ if (!this._config.llmobs.enabled) return
46
+ // if the span is not in our private tagger map, it is not an llmobs span
47
+ if (!LLMObsTagger.tagMap.has(span)) return
48
+
49
+ try {
50
+ const formattedEvent = this.format(span)
51
+ this._writer.append(formattedEvent)
52
+ } catch (e) {
53
+ // this should be a rare case
54
+ // we protect against unserializable properties in the format function, and in
55
+ // safeguards in the tagger
56
+ logger.warn(`
57
+ Failed to append span to LLM Observability writer, likely due to an unserializable property.
58
+ Span won't be sent to LLM Observability: ${e.message}
59
+ `)
60
+ }
61
+ }
62
+
63
+ format (span) {
64
+ const spanTags = span.context()._tags
65
+ const mlObsTags = LLMObsTagger.tagMap.get(span)
66
+
67
+ const spanKind = mlObsTags[SPAN_KIND]
68
+
69
+ const meta = { 'span.kind': spanKind, input: {}, output: {} }
70
+ const input = {}
71
+ const output = {}
72
+
73
+ if (['llm', 'embedding'].includes(spanKind)) {
74
+ meta.model_name = mlObsTags[MODEL_NAME] || 'custom'
75
+ meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
76
+ }
77
+ if (mlObsTags[METADATA]) {
78
+ this._addObject(mlObsTags[METADATA], meta.metadata = {})
79
+ }
80
+ if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
81
+ input.messages = mlObsTags[INPUT_MESSAGES]
82
+ }
83
+ if (mlObsTags[INPUT_VALUE]) {
84
+ input.value = mlObsTags[INPUT_VALUE]
85
+ }
86
+ if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
87
+ output.messages = mlObsTags[OUTPUT_MESSAGES]
88
+ }
89
+ if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
90
+ input.documents = mlObsTags[INPUT_DOCUMENTS]
91
+ }
92
+ if (mlObsTags[OUTPUT_VALUE]) {
93
+ output.value = mlObsTags[OUTPUT_VALUE]
94
+ }
95
+ if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
96
+ output.documents = mlObsTags[OUTPUT_DOCUMENTS]
97
+ }
98
+
99
+ const error = spanTags.error || spanTags[ERROR_TYPE]
100
+ if (error) {
101
+ meta[ERROR_MESSAGE] = spanTags[ERROR_MESSAGE] || error.message || error.code
102
+ meta[ERROR_TYPE] = spanTags[ERROR_TYPE] || error.name
103
+ meta[ERROR_STACK] = spanTags[ERROR_STACK] || error.stack
104
+ }
105
+
106
+ if (input) meta.input = input
107
+ if (output) meta.output = output
108
+
109
+ const metrics = mlObsTags[METRICS] || {}
110
+
111
+ const mlApp = mlObsTags[ML_APP]
112
+ const sessionId = mlObsTags[SESSION_ID]
113
+ const parentId = mlObsTags[PARENT_ID_KEY]
114
+
115
+ const name = mlObsTags[NAME] || span._name
116
+
117
+ const llmObsSpanEvent = {
118
+ trace_id: span.context().toTraceId(true),
119
+ span_id: span.context().toSpanId(),
120
+ parent_id: parentId,
121
+ name,
122
+ tags: this._processTags(span, mlApp, sessionId, error),
123
+ start_ns: Math.round(span._startTime * 1e6),
124
+ duration: Math.round(span._duration * 1e6),
125
+ status: error ? 'error' : 'ok',
126
+ meta,
127
+ metrics,
128
+ _dd: {
129
+ span_id: span.context().toSpanId(),
130
+ trace_id: span.context().toTraceId(true)
131
+ }
132
+ }
133
+
134
+ if (sessionId) llmObsSpanEvent.session_id = sessionId
135
+
136
+ return llmObsSpanEvent
137
+ }
138
+
139
+ // For now, this only applies to metadata, as we let users annotate this field with any object
140
+ // However, we want to protect against circular references or BigInts (unserializable)
141
+ // This function can be reused for other fields if needed
142
+ // Messages, Documents, and Metrics are safeguarded in `llmobs/tagger.js`
143
+ _addObject (obj, carrier) {
144
+ const seenObjects = new WeakSet()
145
+ seenObjects.add(obj) // capture root object
146
+
147
+ const isCircular = value => {
148
+ if (typeof value !== 'object') return false
149
+ if (seenObjects.has(value)) return true
150
+ seenObjects.add(value)
151
+ return false
152
+ }
153
+
154
+ const add = (obj, carrier) => {
155
+ for (const key in obj) {
156
+ const value = obj[key]
157
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
158
+ if (typeof value === 'bigint' || isCircular(value)) {
159
+ // mark as unserializable instead of dropping
160
+ logger.warn(`Unserializable property found in metadata: ${key}`)
161
+ carrier[key] = UNSERIALIZABLE_VALUE_TEXT
162
+ continue
163
+ }
164
+ if (typeof value === 'object') {
165
+ add(value, carrier[key] = {})
166
+ } else {
167
+ carrier[key] = value
168
+ }
169
+ }
170
+ }
171
+
172
+ add(obj, carrier)
173
+ }
174
+
175
+ _processTags (span, mlApp, sessionId, error) {
176
+ let tags = {
177
+ version: this._config.version,
178
+ env: this._config.env,
179
+ service: this._config.service,
180
+ source: 'integration',
181
+ ml_app: mlApp,
182
+ 'ddtrace.version': tracerVersion,
183
+ error: Number(!!error) || 0,
184
+ language: 'javascript'
185
+ }
186
+ const errType = span.context()._tags[ERROR_TYPE] || error?.name
187
+ if (errType) tags.error_type = errType
188
+ if (sessionId) tags.session_id = sessionId
189
+ const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
190
+ if (existingTags) tags = { ...tags, ...existingTags }
191
+ return Object.entries(tags).map(([key, value]) => `${key}:${value ?? ''}`)
192
+ }
193
+ }
194
+
195
+ module.exports = LLMObsSpanProcessor
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ // TODO: remove this and use namespaced storage once available
4
+ const { AsyncLocalStorage } = require('async_hooks')
5
+ const storage = new AsyncLocalStorage()
6
+
7
+ module.exports = { storage }
@@ -0,0 +1,322 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+ const {
5
+ MODEL_NAME,
6
+ MODEL_PROVIDER,
7
+ SESSION_ID,
8
+ ML_APP,
9
+ SPAN_KIND,
10
+ INPUT_VALUE,
11
+ OUTPUT_DOCUMENTS,
12
+ INPUT_DOCUMENTS,
13
+ OUTPUT_VALUE,
14
+ METADATA,
15
+ METRICS,
16
+ PARENT_ID_KEY,
17
+ INPUT_MESSAGES,
18
+ OUTPUT_MESSAGES,
19
+ TAGS,
20
+ NAME,
21
+ PROPAGATED_PARENT_ID_KEY,
22
+ ROOT_PARENT_ID,
23
+ INPUT_TOKENS_METRIC_KEY,
24
+ OUTPUT_TOKENS_METRIC_KEY,
25
+ TOTAL_TOKENS_METRIC_KEY
26
+ } = require('./constants/tags')
27
+
28
+ // global registry of LLMObs spans
29
+ // maps LLMObs spans to their annotations
30
+ const registry = new WeakMap()
31
+
32
+ class LLMObsTagger {
33
+ constructor (config, softFail = false) {
34
+ this._config = config
35
+
36
+ this.softFail = softFail
37
+ }
38
+
39
+ static get tagMap () {
40
+ return registry
41
+ }
42
+
43
+ registerLLMObsSpan (span, {
44
+ modelName,
45
+ modelProvider,
46
+ sessionId,
47
+ mlApp,
48
+ parent,
49
+ kind,
50
+ name
51
+ } = {}) {
52
+ if (!this._config.llmobs.enabled) return
53
+ if (!kind) return // do not register it in the map if it doesn't have an llmobs span kind
54
+
55
+ this._register(span)
56
+
57
+ if (name) this._setTag(span, NAME, name)
58
+
59
+ this._setTag(span, SPAN_KIND, kind)
60
+ if (modelName) this._setTag(span, MODEL_NAME, modelName)
61
+ if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
62
+
63
+ sessionId = sessionId || parent?.context()._tags[SESSION_ID]
64
+ if (sessionId) this._setTag(span, SESSION_ID, sessionId)
65
+
66
+ if (!mlApp) mlApp = parent?.context()._tags[ML_APP] || this._config.llmobs.mlApp
67
+ this._setTag(span, ML_APP, mlApp)
68
+
69
+ const parentId =
70
+ parent?.context().toSpanId() ||
71
+ span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY] ||
72
+ ROOT_PARENT_ID
73
+ this._setTag(span, PARENT_ID_KEY, parentId)
74
+ }
75
+
76
+ // TODO: similarly for the following `tag` methods,
77
+ // how can we transition from a span weakmap to core API functionality
78
+ tagLLMIO (span, inputData, outputData) {
79
+ this._tagMessages(span, inputData, INPUT_MESSAGES)
80
+ this._tagMessages(span, outputData, OUTPUT_MESSAGES)
81
+ }
82
+
83
+ tagEmbeddingIO (span, inputData, outputData) {
84
+ this._tagDocuments(span, inputData, INPUT_DOCUMENTS)
85
+ this._tagText(span, outputData, OUTPUT_VALUE)
86
+ }
87
+
88
+ tagRetrievalIO (span, inputData, outputData) {
89
+ this._tagText(span, inputData, INPUT_VALUE)
90
+ this._tagDocuments(span, outputData, OUTPUT_DOCUMENTS)
91
+ }
92
+
93
+ tagTextIO (span, inputData, outputData) {
94
+ this._tagText(span, inputData, INPUT_VALUE)
95
+ this._tagText(span, outputData, OUTPUT_VALUE)
96
+ }
97
+
98
+ tagMetadata (span, metadata) {
99
+ this._setTag(span, METADATA, metadata)
100
+ }
101
+
102
+ tagMetrics (span, metrics) {
103
+ const filterdMetrics = {}
104
+ for (const [key, value] of Object.entries(metrics)) {
105
+ let processedKey = key
106
+
107
+ // processing these specifically for our metrics ingestion
108
+ switch (key) {
109
+ case 'inputTokens':
110
+ processedKey = INPUT_TOKENS_METRIC_KEY
111
+ break
112
+ case 'outputTokens':
113
+ processedKey = OUTPUT_TOKENS_METRIC_KEY
114
+ break
115
+ case 'totalTokens':
116
+ processedKey = TOTAL_TOKENS_METRIC_KEY
117
+ break
118
+ }
119
+
120
+ if (typeof value === 'number') {
121
+ filterdMetrics[processedKey] = value
122
+ } else {
123
+ this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`)
124
+ }
125
+ }
126
+
127
+ this._setTag(span, METRICS, filterdMetrics)
128
+ }
129
+
130
+ tagSpanTags (span, tags) {
131
+ // new tags will be merged with existing tags
132
+ const currentTags = registry.get(span)?.[TAGS]
133
+ if (currentTags) {
134
+ Object.assign(tags, currentTags)
135
+ }
136
+ this._setTag(span, TAGS, tags)
137
+ }
138
+
139
+ _tagText (span, data, key) {
140
+ if (data) {
141
+ if (typeof data === 'string') {
142
+ this._setTag(span, key, data)
143
+ } else {
144
+ try {
145
+ this._setTag(span, key, JSON.stringify(data))
146
+ } catch {
147
+ const type = key === INPUT_VALUE ? 'input' : 'output'
148
+ this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`)
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ _tagDocuments (span, data, key) {
155
+ if (data) {
156
+ if (!Array.isArray(data)) {
157
+ data = [data]
158
+ }
159
+
160
+ const documents = data.map(document => {
161
+ if (typeof document === 'string') {
162
+ return { text: document }
163
+ }
164
+
165
+ if (document == null || typeof document !== 'object') {
166
+ this._handleFailure('Documents must be a string, object, or list of objects.')
167
+ return undefined
168
+ }
169
+
170
+ const { text, name, id, score } = document
171
+ let validDocument = true
172
+
173
+ if (typeof text !== 'string') {
174
+ this._handleFailure('Document text must be a string.')
175
+ validDocument = false
176
+ }
177
+
178
+ const documentObj = { text }
179
+
180
+ validDocument = this._tagConditionalString(name, 'Document name', documentObj, 'name') && validDocument
181
+ validDocument = this._tagConditionalString(id, 'Document ID', documentObj, 'id') && validDocument
182
+ validDocument = this._tagConditionalNumber(score, 'Document score', documentObj, 'score') && validDocument
183
+
184
+ return validDocument ? documentObj : undefined
185
+ }).filter(doc => !!doc)
186
+
187
+ if (documents.length) {
188
+ this._setTag(span, key, documents)
189
+ }
190
+ }
191
+ }
192
+
193
+ _tagMessages (span, data, key) {
194
+ if (data) {
195
+ if (!Array.isArray(data)) {
196
+ data = [data]
197
+ }
198
+
199
+ const messages = data.map(message => {
200
+ if (typeof message === 'string') {
201
+ return { content: message }
202
+ }
203
+
204
+ if (message == null || typeof message !== 'object') {
205
+ this._handleFailure('Messages must be a string, object, or list of objects')
206
+ return undefined
207
+ }
208
+
209
+ let validMessage = true
210
+
211
+ const { content = '', role } = message
212
+ let toolCalls = message.toolCalls
213
+ const messageObj = { content }
214
+
215
+ if (typeof content !== 'string') {
216
+ this._handleFailure('Message content must be a string.')
217
+ validMessage = false
218
+ }
219
+
220
+ validMessage = this._tagConditionalString(role, 'Message role', messageObj, 'role') && validMessage
221
+
222
+ if (toolCalls) {
223
+ if (!Array.isArray(toolCalls)) {
224
+ toolCalls = [toolCalls]
225
+ }
226
+
227
+ const filteredToolCalls = toolCalls.map(toolCall => {
228
+ if (typeof toolCall !== 'object') {
229
+ this._handleFailure('Tool call must be an object.')
230
+ return undefined
231
+ }
232
+
233
+ let validTool = true
234
+
235
+ const { name, arguments: args, toolId, type } = toolCall
236
+ const toolCallObj = {}
237
+
238
+ validTool = this._tagConditionalString(name, 'Tool name', toolCallObj, 'name') && validTool
239
+ validTool = this._tagConditionalObject(args, 'Tool arguments', toolCallObj, 'arguments') && validTool
240
+ validTool = this._tagConditionalString(toolId, 'Tool ID', toolCallObj, 'tool_id') && validTool
241
+ validTool = this._tagConditionalString(type, 'Tool type', toolCallObj, 'type') && validTool
242
+
243
+ return validTool ? toolCallObj : undefined
244
+ }).filter(toolCall => !!toolCall)
245
+
246
+ if (filteredToolCalls.length) {
247
+ messageObj.tool_calls = filteredToolCalls
248
+ }
249
+ }
250
+
251
+ return validMessage ? messageObj : undefined
252
+ }).filter(msg => !!msg)
253
+
254
+ if (messages.length) {
255
+ this._setTag(span, key, messages)
256
+ }
257
+ }
258
+ }
259
+
260
+ _tagConditionalString (data, type, carrier, key) {
261
+ if (!data) return true
262
+ if (typeof data !== 'string') {
263
+ this._handleFailure(`"${type}" must be a string.`)
264
+ return false
265
+ }
266
+ carrier[key] = data
267
+ return true
268
+ }
269
+
270
+ _tagConditionalNumber (data, type, carrier, key) {
271
+ if (!data) return true
272
+ if (typeof data !== 'number') {
273
+ this._handleFailure(`"${type}" must be a number.`)
274
+ return false
275
+ }
276
+ carrier[key] = data
277
+ return true
278
+ }
279
+
280
+ _tagConditionalObject (data, type, carrier, key) {
281
+ if (!data) return true
282
+ if (typeof data !== 'object') {
283
+ this._handleFailure(`"${type}" must be an object.`)
284
+ return false
285
+ }
286
+ carrier[key] = data
287
+ return true
288
+ }
289
+
290
+ // any public-facing LLMObs APIs using this tagger should not soft fail
291
+ // auto-instrumentation should soft fail
292
+ _handleFailure (msg) {
293
+ if (this.softFail) {
294
+ log.warn(msg)
295
+ } else {
296
+ throw new Error(msg)
297
+ }
298
+ }
299
+
300
+ _register (span) {
301
+ if (!this._config.llmobs.enabled) return
302
+ if (registry.has(span)) {
303
+ this._handleFailure(`LLMObs Span "${span._name}" already registered.`)
304
+ return
305
+ }
306
+
307
+ registry.set(span, {})
308
+ }
309
+
310
+ _setTag (span, key, value) {
311
+ if (!this._config.llmobs.enabled) return
312
+ if (!registry.has(span)) {
313
+ this._handleFailure('Span must be an LLMObs generated span.')
314
+ return
315
+ }
316
+
317
+ const tagsCarrier = registry.get(span)
318
+ Object.assign(tagsCarrier, { [key]: value })
319
+ }
320
+ }
321
+
322
+ module.exports = LLMObsTagger
@@ -0,0 +1,176 @@
1
+ 'use strict'
2
+
3
+ const { SPAN_KINDS } = require('./constants/tags')
4
+
5
+ function encodeUnicode (str) {
6
+ if (!str) return str
7
+ return str.split('').map(char => {
8
+ const code = char.charCodeAt(0)
9
+ if (code > 127) {
10
+ return `\\u${code.toString(16).padStart(4, '0')}`
11
+ }
12
+ return char
13
+ }).join('')
14
+ }
15
+
16
+ function validateKind (kind) {
17
+ if (!SPAN_KINDS.includes(kind)) {
18
+ throw new Error(`
19
+ Invalid span kind specified: "${kind}"
20
+ Must be one of: ${SPAN_KINDS.join(', ')}
21
+ `)
22
+ }
23
+
24
+ return kind
25
+ }
26
+
27
+ // extracts the argument names from a function string
28
+ function parseArgumentNames (str) {
29
+ const result = []
30
+ let current = ''
31
+ let closerCount = 0
32
+ let recording = true
33
+ let inSingleLineComment = false
34
+ let inMultiLineComment = false
35
+
36
+ for (let i = 0; i < str.length; i++) {
37
+ const char = str[i]
38
+ const nextChar = str[i + 1]
39
+
40
+ // Handle single-line comments
41
+ if (!inMultiLineComment && char === '/' && nextChar === '/') {
42
+ inSingleLineComment = true
43
+ i++ // Skip the next character
44
+ continue
45
+ }
46
+
47
+ // Handle multi-line comments
48
+ if (!inSingleLineComment && char === '/' && nextChar === '*') {
49
+ inMultiLineComment = true
50
+ i++ // Skip the next character
51
+ continue
52
+ }
53
+
54
+ // End of single-line comment
55
+ if (inSingleLineComment && char === '\n') {
56
+ inSingleLineComment = false
57
+ continue
58
+ }
59
+
60
+ // End of multi-line comment
61
+ if (inMultiLineComment && char === '*' && nextChar === '/') {
62
+ inMultiLineComment = false
63
+ i++ // Skip the next character
64
+ continue
65
+ }
66
+
67
+ // Skip characters inside comments
68
+ if (inSingleLineComment || inMultiLineComment) {
69
+ continue
70
+ }
71
+
72
+ if (['{', '[', '('].includes(char)) {
73
+ closerCount++
74
+ } else if (['}', ']', ')'].includes(char)) {
75
+ closerCount--
76
+ } else if (char === '=' && nextChar !== '>' && closerCount === 0) {
77
+ recording = false
78
+ // record the variable name early, and stop counting characters until we reach the next comma
79
+ result.push(current.trim())
80
+ current = ''
81
+ continue
82
+ } else if (char === ',' && closerCount === 0) {
83
+ if (recording) {
84
+ result.push(current.trim())
85
+ current = ''
86
+ }
87
+
88
+ recording = true
89
+ continue
90
+ }
91
+
92
+ if (recording) {
93
+ current += char
94
+ }
95
+ }
96
+
97
+ if (current && recording) {
98
+ result.push(current.trim())
99
+ }
100
+
101
+ return result
102
+ }
103
+
104
+ // finds the bounds of the arguments in a function string
105
+ function findArgumentsBounds (str) {
106
+ let start = -1
107
+ let end = -1
108
+ let closerCount = 0
109
+
110
+ for (let i = 0; i < str.length; i++) {
111
+ const char = str[i]
112
+
113
+ if (char === '(') {
114
+ if (closerCount === 0) {
115
+ start = i
116
+ }
117
+
118
+ closerCount++
119
+ } else if (char === ')') {
120
+ closerCount--
121
+
122
+ if (closerCount === 0) {
123
+ end = i
124
+ break
125
+ }
126
+ }
127
+ }
128
+
129
+ return { start, end }
130
+ }
131
+
132
+ const memo = new WeakMap()
133
+ function getFunctionArguments (fn, args = []) {
134
+ if (!fn) return
135
+ if (!args.length) return
136
+ if (args.length === 1) return args[0]
137
+
138
+ try {
139
+ let names
140
+ if (memo.has(fn)) {
141
+ names = memo.get(fn)
142
+ } else {
143
+ const fnString = fn.toString()
144
+ const { start, end } = findArgumentsBounds(fnString)
145
+ names = parseArgumentNames(fnString.slice(start + 1, end))
146
+ memo.set(fn, names)
147
+ }
148
+
149
+ const argsObject = {}
150
+
151
+ for (const argIdx in args) {
152
+ const name = names[argIdx]
153
+ const arg = args[argIdx]
154
+
155
+ const spread = name?.startsWith('...')
156
+
157
+ // this can only be the last argument
158
+ if (spread) {
159
+ argsObject[name.slice(3)] = args.slice(argIdx)
160
+ break
161
+ }
162
+
163
+ argsObject[name] = arg
164
+ }
165
+
166
+ return argsObject
167
+ } catch {
168
+ return args
169
+ }
170
+ }
171
+
172
+ module.exports = {
173
+ encodeUnicode,
174
+ validateKind,
175
+ getFunctionArguments
176
+ }