dd-trace 5.70.0 → 5.71.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.
package/index.d.ts CHANGED
@@ -168,6 +168,7 @@ interface Plugins {
168
168
  "aerospike": tracer.plugins.aerospike;
169
169
  "amqp10": tracer.plugins.amqp10;
170
170
  "amqplib": tracer.plugins.amqplib;
171
+ "anthropic": tracer.plugins.anthropic;
171
172
  "apollo": tracer.plugins.apollo;
172
173
  "avsc": tracer.plugins.avsc;
173
174
  "aws-sdk": tracer.plugins.aws_sdk;
@@ -785,6 +786,8 @@ declare namespace tracer {
785
786
 
786
787
  /** Whether to enable request body collection on RASP event
787
788
  * @default false
789
+ *
790
+ * @deprecated Use UI and Remote Configuration to enable extended data collection
788
791
  */
789
792
  bodyCollection?: boolean
790
793
  },
@@ -809,20 +812,28 @@ declare namespace tracer {
809
812
  },
810
813
  /**
811
814
  * Configuration for extended headers collection tied to security events
815
+ *
816
+ * @deprecated Use UI and Remote Configuration to enable extended data collection
812
817
  */
813
818
  extendedHeadersCollection?: {
814
819
  /** Whether to enable extended headers collection
815
820
  * @default false
821
+ *
822
+ * @deprecated Use UI and Remote Configuration to enable extended data collection
816
823
  */
817
824
  enabled: boolean,
818
825
 
819
826
  /** Whether to redact collected headers
820
827
  * @default true
828
+ *
829
+ * @deprecated Use UI and Remote Configuration to enable extended data collection
821
830
  */
822
831
  redaction: boolean,
823
832
 
824
833
  /** Specifies the maximum number of headers collected.
825
834
  * @default 50
835
+ *
836
+ * @deprecated Use UI and Remote Configuration to enable extended data collection
826
837
  */
827
838
  maxHeaders: number,
828
839
  }
@@ -1530,6 +1541,12 @@ declare namespace tracer {
1530
1541
  */
1531
1542
  interface amqplib extends Instrumentation {}
1532
1543
 
1544
+ /**
1545
+ * This plugin automatically instruments the
1546
+ * [anthropic](https://www.npmjs.com/package/@anthropic-ai/sdk) module.
1547
+ */
1548
+ interface anthropic extends Instrumentation {}
1549
+
1533
1550
  /**
1534
1551
  * Currently this plugin automatically instruments
1535
1552
  * [@apollo/gateway](https://github.com/apollographql/federation) for module versions >= v2.3.0.
package/initialize.mjs CHANGED
@@ -36,7 +36,13 @@ ${result.source}`
36
36
  const [NODE_MAJOR, NODE_MINOR] = process.versions.node.split('.').map(Number)
37
37
 
38
38
  const brokenLoaders = NODE_MAJOR === 18 && NODE_MINOR === 0
39
- const iitmExclusions = [/langsmith/, /openai\/_shims/, /openai\/resources\/chat\/completions\/messages/, /openai\/agents-core\/dist\/shims/]
39
+ const iitmExclusions = [
40
+ /langsmith/,
41
+ /openai\/_shims/,
42
+ /openai\/resources\/chat\/completions\/messages/,
43
+ /openai\/agents-core\/dist\/shims/,
44
+ /@anthropic-ai\/sdk\/_shims/
45
+ ]
40
46
 
41
47
  export async function load (url, context, nextLoad) {
42
48
  const iitmExclusionsMatch = iitmExclusions.some((exclusion) => exclusion.test(url))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.70.0",
3
+ "version": "5.71.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -0,0 +1,115 @@
1
+ 'use strict'
2
+
3
+ const { addHook } = require('./helpers/instrument')
4
+ const shimmer = require('../../datadog-shimmer')
5
+ const { channel, tracingChannel } = require('dc-polyfill')
6
+
7
+ const anthropicTracingChannel = tracingChannel('apm:anthropic:request')
8
+ const onStreamedChunkCh = channel('apm:anthropic:request:chunk')
9
+
10
+ function wrapStreamIterator (iterator, ctx) {
11
+ return function () {
12
+ const itr = iterator.apply(this, arguments)
13
+ shimmer.wrap(itr, 'next', next => function () {
14
+ return next.apply(this, arguments)
15
+ .then(res => {
16
+ const { done, value: chunk } = res
17
+ onStreamedChunkCh.publish({ ctx, chunk, done })
18
+
19
+ if (done) {
20
+ finish(ctx)
21
+ }
22
+
23
+ return res
24
+ })
25
+ .catch(error => {
26
+ finish(ctx, null, error)
27
+ throw error
28
+ })
29
+ })
30
+
31
+ return itr
32
+ }
33
+ }
34
+
35
+ function wrapCreate (create) {
36
+ return function () {
37
+ if (!anthropicTracingChannel.start.hasSubscribers) {
38
+ return create.apply(this, arguments)
39
+ }
40
+
41
+ const options = arguments[0]
42
+ const stream = options.stream
43
+
44
+ const ctx = { options, resource: 'create' }
45
+
46
+ return anthropicTracingChannel.start.runStores(ctx, () => {
47
+ let apiPromise
48
+ try {
49
+ apiPromise = create.apply(this, arguments)
50
+ } catch (error) {
51
+ finish(ctx, null, error)
52
+ throw error
53
+ }
54
+
55
+ shimmer.wrap(apiPromise, 'parse', parse => function () {
56
+ return parse.apply(this, arguments)
57
+ .then(response => {
58
+ if (stream) {
59
+ shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx))
60
+ } else {
61
+ finish(ctx, response, null)
62
+ }
63
+
64
+ return response
65
+ }).catch(error => {
66
+ finish(ctx, null, error)
67
+ throw error
68
+ })
69
+ })
70
+
71
+ anthropicTracingChannel.end.publish(ctx)
72
+
73
+ return apiPromise
74
+ })
75
+ }
76
+ }
77
+
78
+ function finish (ctx, result, error) {
79
+ if (error) {
80
+ ctx.error = error
81
+ anthropicTracingChannel.error.publish(ctx)
82
+ }
83
+
84
+ // streamed responses are handled and set separately
85
+ ctx.result ??= result
86
+
87
+ anthropicTracingChannel.asyncEnd.publish(ctx)
88
+ }
89
+
90
+ const extensions = ['js', 'mjs']
91
+ for (const extension of extensions) {
92
+ addHook({
93
+ name: '@anthropic-ai/sdk',
94
+ file: `resources/messages.${extension}`,
95
+ versions: ['>=0.14.0 <0.33.0']
96
+ }, exports => {
97
+ const Messages = exports.Messages
98
+
99
+ shimmer.wrap(Messages.prototype, 'create', wrapCreate)
100
+
101
+ return exports
102
+ })
103
+
104
+ addHook({
105
+ name: '@anthropic-ai/sdk',
106
+ file: `resources/messages/messages.${extension}`,
107
+ versions: ['>=0.33.0']
108
+ }, exports => {
109
+ const Messages = exports.Messages
110
+
111
+ shimmer.wrap(Messages.prototype, 'create', wrapCreate)
112
+
113
+ return exports
114
+ })
115
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
+ '@anthropic-ai/sdk': { esmFirst: true, fn: () => require('../anthropic') },
4
5
  '@apollo/server': () => require('../apollo-server'),
5
6
  '@apollo/gateway': () => require('../apollo'),
6
7
  'apollo-server-core': () => require('../apollo-server-core'),
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
4
+ const AnthropicTracingPlugin = require('./tracing')
5
+ const AnthropicLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/anthropic')
6
+
7
+ class AnthropicPlugin extends CompositePlugin {
8
+ static id = 'anthropic'
9
+ static get plugins () {
10
+ return {
11
+ llmobs: AnthropicLLMObsPlugin,
12
+ tracing: AnthropicTracingPlugin
13
+ }
14
+ }
15
+ }
16
+
17
+ module.exports = AnthropicPlugin
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+
5
+ class AnthropicTracingPlugin extends TracingPlugin {
6
+ static id = 'anthropic'
7
+ static operation = 'request'
8
+ static system = 'anthropic'
9
+ static prefix = 'tracing:apm:anthropic:request'
10
+
11
+ bindStart (ctx) {
12
+ const { resource, options } = ctx
13
+
14
+ this.startSpan('anthropic.request', {
15
+ meta: {
16
+ 'resource.name': `Messages.${resource}`,
17
+ 'anthropic.request.model': options.model
18
+ }
19
+ }, ctx)
20
+
21
+ return ctx.currentStore
22
+ }
23
+
24
+ asyncEnd (ctx) {
25
+ const span = ctx.currentStore?.span
26
+ span?.finish()
27
+ }
28
+ }
29
+
30
+ module.exports = AnthropicTracingPlugin
@@ -38,14 +38,20 @@ const config = {
38
38
 
39
39
  const metricsQueue = new Map()
40
40
 
41
+ const extendedDataCollectionRequest = new WeakMap()
42
+
41
43
  // following header lists are ordered in the same way the spec orders them, it doesn't matter but it's easier to compare
42
44
  const contentHeaderList = [
43
45
  'content-length',
44
- 'content-type',
45
46
  'content-encoding',
46
47
  'content-language'
47
48
  ]
48
49
 
50
+ const responseHeaderList = [
51
+ ...contentHeaderList,
52
+ 'content-type'
53
+ ]
54
+
49
55
  const identificationHeaders = [
50
56
  'x-amzn-trace-id',
51
57
  'cloudfront-viewer-ja3-fingerprint',
@@ -75,15 +81,27 @@ const requestHeadersList = [
75
81
  ...identificationHeaders
76
82
  ]
77
83
 
84
+ const redactedHeadersList = [
85
+ 'authorization',
86
+ 'proxy-authorization',
87
+ 'www-authenticate',
88
+ 'proxy-authenticate',
89
+ 'authentication-info',
90
+ 'proxy-authentication-info',
91
+ 'cookie',
92
+ 'set-cookie'
93
+ ]
94
+
78
95
  // these request headers are always collected - it breaks the expected spec orders
79
96
  const REQUEST_HEADERS_MAP = mapHeaderAndTags(requestHeadersList, REQUEST_HEADER_TAG_PREFIX)
80
97
 
81
98
  const EVENT_HEADERS_MAP = mapHeaderAndTags(eventHeadersList, REQUEST_HEADER_TAG_PREFIX)
82
99
 
83
- const RESPONSE_HEADERS_MAP = mapHeaderAndTags(contentHeaderList, RESPONSE_HEADER_TAG_PREFIX)
100
+ const RESPONSE_HEADERS_MAP = mapHeaderAndTags(responseHeaderList, RESPONSE_HEADER_TAG_PREFIX)
84
101
 
85
102
  const NON_EXTENDED_REQUEST_HEADERS = new Set([...requestHeadersList, ...eventHeadersList])
86
- const NON_EXTENDED_RESPONSE_HEADERS = new Set(contentHeaderList)
103
+ const NON_EXTENDED_RESPONSE_HEADERS = new Set(responseHeaderList)
104
+ const REDACTED_HEADERS = new Set(redactedHeadersList)
87
105
 
88
106
  function init (_config) {
89
107
  config.headersExtendedCollectionEnabled = _config.extendedHeadersCollection.enabled
@@ -132,7 +150,9 @@ function filterExtendedHeaders (headers, excludedHeaderNames, tagPrefix, limit =
132
150
  for (const [headerName, headerValue] of Object.entries(headers)) {
133
151
  if (counter >= limit) break
134
152
  if (!excludedHeaderNames.has(headerName)) {
135
- result[getHeaderTag(tagPrefix, headerName)] = String(headerValue)
153
+ result[getHeaderTag(tagPrefix, headerName)] = REDACTED_HEADERS.has(headerName)
154
+ ? '<redacted>'
155
+ : String(headerValue)
136
156
  counter++
137
157
  }
138
158
  }
@@ -140,7 +160,7 @@ function filterExtendedHeaders (headers, excludedHeaderNames, tagPrefix, limit =
140
160
  return result
141
161
  }
142
162
 
143
- function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedResponseHeaders = {}) {
163
+ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedResponseHeaders = {}, extendedDataCollection) {
144
164
  // Mandatory
145
165
  const mandatoryCollectedHeaders = filterHeaders(req.headers, REQUEST_HEADERS_MAP)
146
166
 
@@ -154,7 +174,8 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
154
174
  const requestEventCollectedHeaders = filterHeaders(req.headers, EVENT_HEADERS_MAP)
155
175
  const responseEventCollectedHeaders = filterHeaders(responseHeaders, RESPONSE_HEADERS_MAP)
156
176
 
157
- if (!config.headersExtendedCollectionEnabled || config.headersRedaction) {
177
+ // TODO headersExtendedCollectionEnabled and headersRedaction properties are deprecated to delete in a major
178
+ if ((!config.headersExtendedCollectionEnabled || config.headersRedaction) && !extendedDataCollection) {
158
179
  // Standard collection
159
180
  return Object.assign(
160
181
  mandatoryCollectedHeaders,
@@ -163,12 +184,15 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
163
184
  )
164
185
  }
165
186
 
187
+ // TODO config.maxHeadersCollected is deprecated to delete in a major
188
+ const maxHeadersCollected = extendedDataCollection?.max_collected_headers ?? config.maxHeadersCollected
189
+
166
190
  // Extended collection
167
- const requestExtendedHeadersAvailableCount =
168
- config.maxHeadersCollected -
169
- Object.keys(mandatoryCollectedHeaders).length -
191
+ const collectedHeadersCount = Object.keys(mandatoryCollectedHeaders).length +
170
192
  Object.keys(requestEventCollectedHeaders).length
171
193
 
194
+ const requestExtendedHeadersAvailableCount = maxHeadersCollected - collectedHeadersCount
195
+
172
196
  const requestEventExtendedCollectedHeaders =
173
197
  filterExtendedHeaders(
174
198
  req.headers,
@@ -178,7 +202,7 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
178
202
  )
179
203
 
180
204
  const responseExtendedHeadersAvailableCount =
181
- config.maxHeadersCollected -
205
+ maxHeadersCollected -
182
206
  Object.keys(responseEventCollectedHeaders).length
183
207
 
184
208
  const responseEventExtendedCollectedHeaders =
@@ -199,15 +223,15 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
199
223
 
200
224
  // Check discarded headers
201
225
  const requestHeadersCount = Object.keys(req.headers).length
202
- if (requestHeadersCount > config.maxHeadersCollected) {
226
+ if (requestHeadersCount > maxHeadersCollected) {
203
227
  headersTags['_dd.appsec.request.header_collection.discarded'] =
204
- requestHeadersCount - config.maxHeadersCollected
228
+ requestHeadersCount - maxHeadersCollected
205
229
  }
206
230
 
207
231
  const responseHeadersCount = Object.keys(responseHeaders).length
208
- if (responseHeadersCount > config.maxHeadersCollected) {
232
+ if (responseHeadersCount > maxHeadersCollected) {
209
233
  headersTags['_dd.appsec.response.header_collection.discarded'] =
210
- responseHeadersCount - config.maxHeadersCollected
234
+ responseHeadersCount - maxHeadersCollected
211
235
  }
212
236
 
213
237
  return headersTags
@@ -307,7 +331,7 @@ function reportTruncationMetrics (rootSpan, metrics) {
307
331
  }
308
332
  }
309
333
 
310
- function reportAttack (attackData) {
334
+ function reportAttack ({ events: attackData, actions }) {
311
335
  const store = storage('legacy').getStore()
312
336
  const req = store?.req
313
337
  const rootSpan = web.root(req)
@@ -338,8 +362,14 @@ function reportAttack (attackData) {
338
362
 
339
363
  rootSpan.addTags(newTags)
340
364
 
365
+ // TODO this should be deleted in a major
341
366
  if (config.raspBodyCollection && isRaspAttack(attackData)) {
342
- reportRequestBody(rootSpan, req.body)
367
+ reportRequestBody(rootSpan, req.body, true)
368
+ }
369
+
370
+ const extendedDataCollection = actions?.extended_data_collection
371
+ if (extendedDataCollection) {
372
+ extendedDataCollectionRequest.set(req, extendedDataCollection)
343
373
  }
344
374
  }
345
375
 
@@ -398,18 +428,29 @@ function truncateRequestBody (target, depth = 0) {
398
428
  }
399
429
  }
400
430
 
401
- function reportRequestBody (rootSpan, requestBody) {
402
- if (!requestBody) return
431
+ function reportRequestBody (rootSpan, requestBody, comesFromRaspAction = false) {
432
+ if (!requestBody || Object.keys(requestBody).length === 0) return
403
433
 
404
434
  if (!rootSpan.meta_struct) {
405
435
  rootSpan.meta_struct = {}
406
436
  }
407
437
 
408
- if (!rootSpan.meta_struct['http.request.body']) {
438
+ if (rootSpan.meta_struct['http.request.body']) {
439
+ // If the rasp.exceed metric exists, set also the same for the new tag
440
+ const currentTags = rootSpan.context()._tags
441
+ const sizeExceedTagValue = currentTags['_dd.appsec.rasp.request_body_size.exceeded']
442
+
443
+ if (sizeExceedTagValue) {
444
+ rootSpan.setTag('_dd.appsec.request_body_size.exceeded', sizeExceedTagValue)
445
+ }
446
+ } else {
409
447
  const { truncated, value } = truncateRequestBody(requestBody)
410
448
  rootSpan.meta_struct['http.request.body'] = value
411
449
  if (truncated) {
412
- rootSpan.setTag('_dd.appsec.rasp.request_body_size.exceeded', 'true')
450
+ const sizeExceedTagKey = comesFromRaspAction
451
+ ? '_dd.appsec.rasp.request_body_size.exceeded' // TODO old metric to delete in a major
452
+ : '_dd.appsec.request_body_size.exceeded'
453
+ rootSpan.setTag(sizeExceedTagKey, 'true')
413
454
  }
414
455
  }
415
456
  }
@@ -496,7 +537,15 @@ function finishRequest (req, res, storedResponseHeaders) {
496
537
 
497
538
  const tags = rootSpan.context()._tags
498
539
 
499
- const newTags = getCollectedHeaders(req, res, shouldCollectEventHeaders(tags), storedResponseHeaders)
540
+ const extendedDataCollection = extendedDataCollectionRequest.get(req)
541
+ const newTags = getCollectedHeaders(
542
+ req, res, shouldCollectEventHeaders(tags), storedResponseHeaders, extendedDataCollection
543
+ )
544
+
545
+ if (extendedDataCollection) {
546
+ // TODO add support for fastify, req.body is not available in fastify
547
+ reportRequestBody(rootSpan, req.body)
548
+ }
500
549
 
501
550
  if (tags['appsec.event'] === 'true' && typeof req.route?.path === 'string') {
502
551
  newTags['http.endpoint'] = req.route.path
@@ -141,7 +141,7 @@ class WAFContextWrapper {
141
141
  metrics.wafTimeout = result.timeout
142
142
 
143
143
  if (ruleTriggered) {
144
- Reporter.reportAttack(result.events)
144
+ Reporter.reportAttack(result)
145
145
  }
146
146
 
147
147
  Reporter.reportAttributes(result.attributes)
@@ -668,6 +668,7 @@ class Config {
668
668
  this._envUnprocessed['appsec.blockedTemplateJson'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON
669
669
  this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED)
670
670
  this._setString(env, 'appsec.eventTracking.mode', DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE)
671
+ // TODO appsec.extendedHeadersCollection are deprecated, to delete in a major
671
672
  this._setBoolean(env, 'appsec.extendedHeadersCollection.enabled', DD_APPSEC_COLLECT_ALL_HEADERS)
672
673
  this._setBoolean(
673
674
  env,
@@ -679,6 +680,7 @@ class Config {
679
680
  this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP)
680
681
  this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP)
681
682
  this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED)
683
+ // TODO Deprecated, to delete in a major
682
684
  this._setBoolean(env, 'appsec.rasp.bodyCollection', DD_APPSEC_RASP_COLLECT_REQUEST_BODY)
683
685
  env['appsec.rateLimit'] = maybeInt(DD_APPSEC_TRACE_RATE_LIMIT)
684
686
  this._envUnprocessed['appsec.rateLimit'] = DD_APPSEC_TRACE_RATE_LIMIT
@@ -36,12 +36,14 @@ module.exports = {
36
36
  'appsec.blockedTemplateJson': undefined,
37
37
  'appsec.enabled': undefined,
38
38
  'appsec.eventTracking.mode': 'identification',
39
+ // TODO appsec.extendedHeadersCollection is deprecated, to delete in a major
39
40
  'appsec.extendedHeadersCollection.enabled': false,
40
41
  'appsec.extendedHeadersCollection.redaction': true,
41
42
  'appsec.extendedHeadersCollection.maxHeaders': 50,
42
43
  'appsec.obfuscatorKeyRegex': defaultWafObfuscatorKeyRegex,
43
44
  'appsec.obfuscatorValueRegex': defaultWafObfuscatorValueRegex,
44
45
  'appsec.rasp.enabled': true,
46
+ // TODO Deprecated, to delete in a major
45
47
  'appsec.rasp.bodyCollection': false,
46
48
  'appsec.rateLimit': 100,
47
49
  'appsec.rules': undefined,
@@ -0,0 +1,282 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('./base')
4
+
5
+ const ALLOWED_METADATA_KEYS = new Set([
6
+ 'max_tokens',
7
+ 'stop_sequences',
8
+ 'temperature',
9
+ 'top_k',
10
+ 'top_p',
11
+ ])
12
+
13
+ class AnthropicLLMObsPlugin extends LLMObsPlugin {
14
+ static integration = 'anthropic' // used for llmobs telemetry
15
+ static id = 'anthropic'
16
+ static prefix = 'tracing:apm:anthropic:request'
17
+
18
+ constructor () {
19
+ super(...arguments)
20
+
21
+ this.addSub('apm:anthropic:request:chunk', ({ ctx, chunk, done }) => {
22
+ ctx.chunks ??= []
23
+ const chunks = ctx.chunks
24
+ if (chunk) chunks.push(chunk)
25
+
26
+ if (!done) return
27
+
28
+ const response = { content: [] }
29
+
30
+ for (const chunk of chunks) {
31
+ switch (chunk.type) {
32
+ case 'message_start': {
33
+ const { message } = chunk
34
+ if (!message) continue
35
+
36
+ const { role, usage } = message
37
+ if (role) response.role = role
38
+ if (usage) response.usage = usage
39
+ break
40
+ }
41
+ case 'content_block_start': {
42
+ const contentBlock = chunk.content_block
43
+ if (!contentBlock) continue
44
+
45
+ const { type } = contentBlock
46
+ if (type === 'text') {
47
+ response.content.push({ type, text: contentBlock.text })
48
+ } else if (type === 'tool_use') {
49
+ response.content.push({ type, name: contentBlock.name, input: '', id: contentBlock.id })
50
+ }
51
+ break
52
+ }
53
+ case 'content_block_delta': {
54
+ const { delta } = chunk
55
+ if (!delta) continue
56
+
57
+ const { text } = delta
58
+ if (text) response.content[response.content.length - 1].text += text
59
+
60
+ const partialJson = delta.partial_json
61
+ if (partialJson && delta.type === 'input_json_delta') {
62
+ response.content[response.content.length - 1].input += partialJson
63
+ }
64
+ break
65
+ }
66
+ case 'content_block_stop': {
67
+ const type = response.content[response.content.length - 1].type
68
+ if (type === 'tool_use') {
69
+ const input = response.content[response.content.length - 1].input ?? '{}'
70
+ response.content[response.content.length - 1].input = JSON.parse(input)
71
+ }
72
+ break
73
+ }
74
+ case 'message_delta': {
75
+ const { delta } = chunk
76
+
77
+ const finishReason = delta?.stop_reason
78
+ if (finishReason) response.finish_reason = finishReason
79
+
80
+ const { usage } = chunk
81
+ if (usage) {
82
+ const responseUsage = response.usage ?? (response.usage = { input_tokens: 0, output_tokens: 0 })
83
+ responseUsage.output_tokens = usage.output_tokens
84
+
85
+ const cacheCreationTokens = usage.cache_creation_input_tokens
86
+ const cacheReadTokens = usage.cache_read_input_tokens
87
+ if (cacheCreationTokens) responseUsage.cache_creation_input_tokens = cacheCreationTokens
88
+ if (cacheReadTokens) responseUsage.cache_read_input_tokens = cacheReadTokens
89
+ }
90
+
91
+ break
92
+ }
93
+ case 'error': {
94
+ const { error } = chunk
95
+ if (!error) continue
96
+
97
+ response.error = {}
98
+ if (error.type) response.error.type = error.type
99
+ if (error.message) response.error.message = error.message
100
+
101
+ break
102
+ }
103
+ }
104
+
105
+ ctx.result = response
106
+ }
107
+ })
108
+ }
109
+
110
+ getLLMObsSpanRegisterOptions (ctx) {
111
+ const { options } = ctx
112
+ const { model } = options
113
+
114
+ return {
115
+ kind: 'llm',
116
+ modelName: model,
117
+ modelProvider: 'anthropic'
118
+ }
119
+ }
120
+
121
+ setLLMObsTags (ctx) {
122
+ const span = ctx.currentStore?.span
123
+ if (!span) return
124
+
125
+ const { options, result } = ctx
126
+
127
+ this.#tagAnthropicInputMessages(span, options)
128
+ this.#tagAnthropicOutputMessages(span, result)
129
+ this.#tagAnthropicMetadata(span, options)
130
+ this.#tagAnthropicUsage(span, result)
131
+ }
132
+
133
+ #tagAnthropicInputMessages (span, options) {
134
+ const { system, messages } = options
135
+ const inputMessages = []
136
+
137
+ if (system) {
138
+ messages.unshift({ content: system, role: 'system' })
139
+ }
140
+
141
+ for (const message of messages) {
142
+ const { content, role } = message
143
+
144
+ if (typeof content === 'string') {
145
+ inputMessages.push({ content, role })
146
+ continue
147
+ }
148
+
149
+ for (const block of content) {
150
+ if (block.type === 'text') {
151
+ inputMessages.push({ content: block.text, role })
152
+ } else if (block.type === 'image') {
153
+ inputMessages.push({ content: '([IMAGE DETECTED])', role })
154
+ } else if (block.type === 'tool_use') {
155
+ const { text, name, id, type } = block
156
+ let input = block.input
157
+ if (typeof input === 'string') {
158
+ input = JSON.parse(input)
159
+ }
160
+
161
+ const toolCall = {
162
+ name,
163
+ arguments: input,
164
+ toolId: id,
165
+ type
166
+ }
167
+
168
+ inputMessages.push({ content: text ?? '', role, toolCalls: [toolCall] })
169
+ } else if (block.type === 'tool_result') {
170
+ const { content } = block
171
+ const formattedContent = this.#formatAnthropicToolResultContent(content)
172
+ const toolResult = {
173
+ result: formattedContent,
174
+ toolId: block.tool_use_id,
175
+ type: 'tool_result'
176
+ }
177
+
178
+ inputMessages.push({ content: '', role, toolResults: [toolResult] })
179
+ } else {
180
+ inputMessages.push({ content: JSON.stringify(block), role })
181
+ }
182
+ }
183
+ }
184
+
185
+ this._tagger.tagLLMIO(span, inputMessages)
186
+ }
187
+
188
+ #tagAnthropicOutputMessages (span, result) {
189
+ if (!result) return
190
+
191
+ const { content, role } = result
192
+
193
+ if (typeof content === 'string') {
194
+ this._tagger.tagLLMIO(span, null, [{ content, role }])
195
+ return
196
+ }
197
+
198
+ const outputMessages = []
199
+ for (const block of content) {
200
+ const { text } = block
201
+ if (typeof text === 'string') {
202
+ outputMessages.push({ content: text, role })
203
+ } else if (block.type === 'tool_use') {
204
+ let input = block.input
205
+ if (typeof input === 'string') {
206
+ input = JSON.parse(input)
207
+ }
208
+
209
+ const toolCall = {
210
+ name: block.name,
211
+ arguments: input,
212
+ toolId: block.id,
213
+ type: block.type
214
+ }
215
+
216
+ outputMessages.push({ content: text ?? '', role, toolCalls: [toolCall] })
217
+ }
218
+ }
219
+
220
+ this._tagger.tagLLMIO(span, null, outputMessages)
221
+ }
222
+
223
+ #tagAnthropicMetadata (span, options) {
224
+ const metadata = {}
225
+ for (const [key, value] of Object.entries(options)) {
226
+ if (ALLOWED_METADATA_KEYS.has(key)) {
227
+ metadata[key] = value
228
+ }
229
+ }
230
+
231
+ this._tagger.tagMetadata(span, metadata)
232
+ }
233
+
234
+ #tagAnthropicUsage (span, result) {
235
+ if (!result) return
236
+
237
+ const { usage } = result
238
+ if (!usage) return
239
+
240
+ const inputTokens = usage.input_tokens
241
+ const outputTokens = usage.output_tokens
242
+ const cacheWriteTokens = usage.cache_creation_input_tokens
243
+ const cacheReadTokens = usage.cache_read_input_tokens
244
+
245
+ const metrics = {}
246
+
247
+ metrics.inputTokens =
248
+ (inputTokens ?? 0) +
249
+ (cacheWriteTokens ?? 0) +
250
+ (cacheReadTokens ?? 0)
251
+
252
+ if (outputTokens) metrics.outputTokens = outputTokens
253
+ const totalTokens = metrics.inputTokens + (outputTokens ?? 0)
254
+ if (totalTokens) metrics.totalTokens = totalTokens
255
+
256
+ if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
257
+ if (cacheReadTokens != null) metrics.cacheReadTokens = cacheReadTokens
258
+
259
+ this._tagger.tagMetrics(span, metrics)
260
+ }
261
+
262
+ // maybe can make into a util file
263
+ #formatAnthropicToolResultContent (content) {
264
+ if (typeof content === 'string') {
265
+ return content
266
+ } else if (Array.isArray(content)) {
267
+ const formattedContent = []
268
+ for (const toolResultBlock of content) {
269
+ if (toolResultBlock.text) {
270
+ formattedContent.push(toolResultBlock.text)
271
+ } else if (toolResultBlock.type === 'image') {
272
+ formattedContent.push('([IMAGE DETECTED])')
273
+ }
274
+ }
275
+
276
+ return formattedContent.join(',')
277
+ }
278
+ return JSON.stringify(content)
279
+ }
280
+ }
281
+
282
+ module.exports = AnthropicLLMObsPlugin
@@ -268,6 +268,32 @@ class LLMObsTagger {
268
268
  return filteredToolCalls
269
269
  }
270
270
 
271
+ #filterToolResults (toolResults) {
272
+ if (!Array.isArray(toolResults)) {
273
+ toolResults = [toolResults]
274
+ }
275
+
276
+ const filteredToolResults = []
277
+ for (const toolResult of toolResults) {
278
+ if (typeof toolResult !== 'object') {
279
+ this.#handleFailure('Tool result must be an object.', 'invalid_io_messages')
280
+ continue
281
+ }
282
+
283
+ const { result, toolId, type } = toolResult
284
+ const toolResultObj = {}
285
+
286
+ const condition1 = this.#tagConditionalString(result, 'Tool result', toolResultObj, 'result')
287
+ const condition2 = this.#tagConditionalString(toolId, 'Tool ID', toolResultObj, 'tool_id')
288
+ const condition3 = this.#tagConditionalString(type, 'Tool type', toolResultObj, 'type')
289
+
290
+ if (condition1 && condition2 && condition3) {
291
+ filteredToolResults.push(toolResultObj)
292
+ }
293
+ }
294
+ return filteredToolResults
295
+ }
296
+
271
297
  #tagMessages (span, data, key) {
272
298
  if (!data) {
273
299
  return
@@ -290,6 +316,7 @@ class LLMObsTagger {
290
316
 
291
317
  const { content = '', role } = message
292
318
  const toolCalls = message.toolCalls
319
+ const toolResults = message.toolResults
293
320
  const toolId = message.toolId
294
321
  const messageObj = { content }
295
322
 
@@ -308,6 +335,14 @@ class LLMObsTagger {
308
335
  }
309
336
  }
310
337
 
338
+ if (toolResults) {
339
+ const filteredToolResults = this.#filterToolResults(toolResults)
340
+
341
+ if (filteredToolResults.length) {
342
+ messageObj.tool_results = filteredToolResults
343
+ }
344
+ }
345
+
311
346
  if (toolId) {
312
347
  if (role === 'tool') {
313
348
  condition = this.#tagConditionalString(toolId, 'Tool ID', messageObj, 'tool_id')
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
+ get '@anthropic-ai/sdk' () { return require('../../../datadog-plugin-anthropic/src') },
4
5
  get '@apollo/gateway' () { return require('../../../datadog-plugin-apollo/src') },
5
6
  get '@aws-sdk/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') },
6
7
  get '@azure/functions' () { return require('../../../datadog-plugin-azure-functions/src') },
@@ -31,6 +31,7 @@ module.exports = {
31
31
  ASM_RASP_CMDI: 1n << 37n,
32
32
  ASM_DD_MULTICONFIG: 1n << 42n,
33
33
  ASM_TRACE_TAGGING_RULES: 1n << 43n,
34
+ ASM_EXTENDED_DATA_COLLECTION: 1n << 44n,
34
35
  /*
35
36
  DO NOT ADD ARBITRARY CAPABILITIES IN YOUR CODE
36
37
  UNLESS THEY ARE ALREADY DEFINED IN THE BACKEND SOURCE OF TRUTH
@@ -96,6 +96,7 @@ function enableWafUpdate (appsecConfig) {
96
96
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true)
97
97
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, true)
98
98
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, true)
99
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, true)
99
100
 
100
101
  if (appsecConfig.rasp?.enabled) {
101
102
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, true)
@@ -134,6 +135,7 @@ function disableWafUpdate () {
134
135
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, false)
135
136
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, false)
136
137
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, false)
138
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, false)
137
139
 
138
140
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, false)
139
141
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false)
@@ -176,6 +176,7 @@
176
176
  "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": ["A"],
177
177
  "DD_TRACE_AEROSPIKE_ENABLED": ["A"],
178
178
  "DD_TRACE_AI_ENABLED": ["A"],
179
+ "DD_TRACE_ANTHROPIC_ENABLED": ["A"],
179
180
  "DD_TRACE_AGENT_PORT": ["A"],
180
181
  "DD_TRACE_AGENT_PROTOCOL_VERSION": ["A"],
181
182
  "DD_TRACE_AGENT_URL": ["A"],
package/register.js CHANGED
@@ -6,5 +6,13 @@ const { register } = require('node:module')
6
6
  const { pathToFileURL } = require('node:url')
7
7
 
8
8
  register('./loader-hook.mjs', pathToFileURL(__filename), {
9
- data: { exclude: [/langsmith/, /openai\/_shims/, /openai\/resources\/chat\/completions\/messages/, /openai\/agents-core\/dist\/shims/] }
9
+ data: {
10
+ exclude: [
11
+ /langsmith/,
12
+ /openai\/_shims/,
13
+ /openai\/resources\/chat\/completions\/messages/,
14
+ /openai\/agents-core\/dist\/shims/,
15
+ /@anthropic-ai\/sdk\/_shims/
16
+ ]
17
+ }
10
18
  })