dd-trace 5.100.0 → 5.101.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 (64) hide show
  1. package/index.d.ts +14 -0
  2. package/package.json +5 -5
  3. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  4. package/packages/datadog-instrumentations/src/http/client.js +20 -3
  5. package/packages/datadog-instrumentations/src/jest.js +62 -32
  6. package/packages/datadog-instrumentations/src/mocha/common.js +4 -1
  7. package/packages/datadog-instrumentations/src/mocha/main.js +25 -4
  8. package/packages/datadog-instrumentations/src/mocha/worker.js +5 -2
  9. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  10. package/packages/datadog-plugin-bullmq/src/consumer.js +2 -2
  11. package/packages/datadog-plugin-bullmq/src/producer.js +14 -20
  12. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +17 -0
  13. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  14. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  15. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  16. package/packages/dd-trace/src/appsec/reporter.js +4 -1
  17. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  18. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  19. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  20. package/packages/dd-trace/src/config/index.js +1 -55
  21. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  22. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  23. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  24. package/packages/dd-trace/src/datastreams/processor.js +17 -15
  25. package/packages/dd-trace/src/datastreams/size.js +6 -2
  26. package/packages/dd-trace/src/debugger/config.js +5 -2
  27. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  28. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  29. package/packages/dd-trace/src/dogstatsd.js +10 -7
  30. package/packages/dd-trace/src/encode/0.4.js +2 -2
  31. package/packages/dd-trace/src/encode/0.5.js +2 -2
  32. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  33. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  34. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  35. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  36. package/packages/dd-trace/src/git_metadata.js +66 -0
  37. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  38. package/packages/dd-trace/src/id.js +15 -26
  39. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  40. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  41. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  42. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +30 -13
  43. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  44. package/packages/dd-trace/src/llmobs/sdk.js +5 -1
  45. package/packages/dd-trace/src/llmobs/span_processor.js +28 -2
  46. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  47. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  48. package/packages/dd-trace/src/llmobs/util.js +80 -5
  49. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  50. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  51. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -2
  52. package/packages/dd-trace/src/opentelemetry/span-helpers.js +188 -50
  53. package/packages/dd-trace/src/opentelemetry/span.js +42 -80
  54. package/packages/dd-trace/src/opentracing/propagation/text_map.js +65 -27
  55. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +58 -22
  56. package/packages/dd-trace/src/opentracing/span.js +56 -48
  57. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  58. package/packages/dd-trace/src/priority_sampler.js +6 -4
  59. package/packages/dd-trace/src/profiling/config.js +5 -4
  60. package/packages/dd-trace/src/remote_config/index.js +5 -3
  61. package/packages/dd-trace/src/span_format.js +52 -5
  62. package/packages/dd-trace/src/span_processor.js +0 -4
  63. package/packages/dd-trace/src/spanleak.js +0 -1
  64. package/packages/dd-trace/src/util.js +17 -0
@@ -22,6 +22,7 @@ const TYPE_HISTOGRAM = 'h'
22
22
  */
23
23
  class DogStatsDClient {
24
24
  #lookup
25
+ #tagsPrefix
25
26
  constructor (options) {
26
27
  this.#lookup = options.lookup
27
28
  if (options.metricsProxyUrl) {
@@ -36,6 +37,7 @@ class DogStatsDClient {
36
37
  this._family = isIP(this._host)
37
38
  this._port = options.port
38
39
  this._tags = options.tags
40
+ this.#tagsPrefix = this._tags?.length ? `|#${this._tags.join(',')}` : ''
39
41
  this._queue = []
40
42
  this._buffer = ''
41
43
  this._offset = 0
@@ -66,9 +68,9 @@ class DogStatsDClient {
66
68
  flush () {
67
69
  const queue = this._enqueue()
68
70
 
69
- log.debug('Flushing %s metrics via', queue.length, this._httpOptions ? 'HTTP' : 'UDP')
71
+ if (queue.length === 0) return
70
72
 
71
- if (this._queue.length === 0) return
73
+ log.debug('Flushing %s metrics via %s', queue.length, this._httpOptions ? 'HTTP' : 'UDP')
72
74
 
73
75
  this._queue = []
74
76
 
@@ -119,11 +121,12 @@ class DogStatsDClient {
119
121
  _add (stat, value, type, tags) {
120
122
  let message = `${stat}:${value}|${type}`
121
123
 
122
- // Don't manipulate this._tags as it is still used
123
- tags = tags ? [...this._tags, ...tags] : this._tags
124
-
125
- if (tags.length > 0) {
126
- message += `|#${tags.join(',')}`
124
+ if (tags?.length) {
125
+ message += this.#tagsPrefix
126
+ ? `${this.#tagsPrefix},${tags.join(',')}`
127
+ : `|#${tags.join(',')}`
128
+ } else {
129
+ message += this.#tagsPrefix
127
130
  }
128
131
 
129
132
  if (entityId) {
@@ -3,12 +3,12 @@
3
3
  const getConfig = require('../config')
4
4
  const { MsgpackChunk, MsgpackEncoder } = require('../msgpack')
5
5
  const log = require('../log')
6
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
6
+ const { normalizeSpan } = require('./tags-processors')
7
7
 
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
9
9
 
10
10
  function formatSpan (span, config) {
11
- span = normalizeSpan(truncateSpan(span, false))
11
+ span = normalizeSpan(span)
12
12
  if (span.span_events) {
13
13
  // ensure span events are encoded as tags if agent doesn't support native top level span events
14
14
  if (config.DD_TRACE_NATIVE_SPAN_EVENTS) {
@@ -1,13 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
+ const { normalizeSpan } = require('./tags-processors')
4
4
  const { AgentEncoder: BaseEncoder } = require('./0.4')
5
5
 
6
6
  const ARRAY_OF_TWO = 0x92
7
7
  const ARRAY_OF_TWELVE = 0x9C
8
8
 
9
9
  function formatSpan (span) {
10
- span = normalizeSpan(truncateSpan(span, false))
10
+ span = normalizeSpan(span)
11
11
  // ensure span events are encoded as tags
12
12
  if (span.span_events) {
13
13
  span.meta.events = JSON.stringify(span.span_events)
@@ -2,7 +2,7 @@
2
2
 
3
3
  const log = require('../log')
4
4
  const { TOP_LEVEL_KEY } = require('../constants')
5
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
5
+ const { normalizeSpan } = require('./tags-processors')
6
6
 
7
7
  // Soft limit for estimated payload size. Triggers an early flush to stay under intake request size limits.
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
@@ -14,7 +14,7 @@ const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
14
14
  * @returns {object} The formatted span
15
15
  */
16
16
  function formatSpan (span, isFirstSpan) {
17
- span = normalizeSpan(truncateSpan(span, false))
17
+ span = normalizeSpan(span)
18
18
 
19
19
  // Remove _dd.p.tid (the upper 64 bits of a 128-bit trace ID) since trace_id is truncated to lower 64 bits
20
20
  delete span.meta['_dd.p.tid']
@@ -25,35 +25,10 @@ const MAX_SERVICE_LENGTH = 100
25
25
  // MAX_TYPE_LENGTH the maximum length a span type can have
26
26
  const MAX_TYPE_LENGTH = 100
27
27
 
28
- // TODO (bengl) Pretty much everything in this file should happen in
29
- // `format.js`, so that we're not iterating over all the spans and modifying
30
- // them yet again.
31
-
32
- // normally the agent truncates the resource and parses it in certain scenarios (e.g. SQL Queries)
33
- function truncateSpan (span, shouldTruncateResourceName = true) {
34
- if (shouldTruncateResourceName && span.resource && span.resource.length > MAX_RESOURCE_NAME_LENGTH) {
28
+ function truncateSpan (span) {
29
+ if (span.resource && span.resource.length > MAX_RESOURCE_NAME_LENGTH) {
35
30
  span.resource = `${span.resource.slice(0, MAX_RESOURCE_NAME_LENGTH)}...`
36
31
  }
37
- for (let metaKey of Object.keys(span.meta)) {
38
- const val = span.meta[metaKey]
39
- if (metaKey.length > MAX_META_KEY_LENGTH) {
40
- delete span.meta[metaKey]
41
- metaKey = `${metaKey.slice(0, MAX_META_KEY_LENGTH)}...`
42
- span.meta[metaKey] = val
43
- }
44
- if (val && val.length > MAX_META_VALUE_LENGTH) {
45
- span.meta[metaKey] = `${val.slice(0, MAX_META_VALUE_LENGTH)}...`
46
- }
47
- }
48
- for (let metricsKey of Object.keys(span.metrics)) {
49
- const val = span.metrics[metricsKey]
50
- if (metricsKey.length > MAX_METRIC_KEY_LENGTH) {
51
- delete span.metrics[metricsKey]
52
- metricsKey = `${metricsKey.slice(0, MAX_METRIC_KEY_LENGTH)}...`
53
- span.metrics[metricsKey] = val
54
- }
55
- }
56
-
57
32
  return span
58
33
  }
59
34
 
@@ -13,6 +13,12 @@ const log = require('../../log')
13
13
  const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
14
14
  const docker = require('./docker')
15
15
  const { httpAgent, httpsAgent } = require('./agents')
16
+ const {
17
+ getMaxAttempts,
18
+ getRetryDelay,
19
+ isRetriableNetworkError,
20
+ markEndpointReached,
21
+ } = require('./retry')
16
22
 
17
23
  const maxActiveBufferSize = 1024 * 1024 * 64
18
24
 
@@ -92,6 +98,8 @@ function request (data, options, callback) {
92
98
  options.agent = isSecure ? httpsAgent : httpAgent
93
99
 
94
100
  const onResponse = (res, finalize) => {
101
+ markEndpointReached(options)
102
+
95
103
  const chunks = []
96
104
 
97
105
  res.setTimeout(timeout)
@@ -142,7 +150,10 @@ function request (data, options, callback) {
142
150
  })
143
151
  }
144
152
 
145
- const makeRequest = onError => {
153
+ // Retries always run via setTimeout so the AsyncLocalStorage store survives
154
+ // the gap before socket.connect(); ALS.run() does not call ALS.enterWith()
155
+ // outside AsyncContextFrame, so a synchronous re-entry would lose the store.
156
+ const attempt = attemptIndex => {
146
157
  if (!request.writable) {
147
158
  log.debug('Maximum number of active requests reached: payload is discarded.')
148
159
  return callback(null)
@@ -163,9 +174,16 @@ function request (data, options, callback) {
163
174
  req.once('close', finalize)
164
175
  req.once('timeout', finalize)
165
176
 
166
- req.once('error', err => {
177
+ req.once('error', error => {
167
178
  finalize()
168
- onError(err)
179
+ if (attemptIndex < getMaxAttempts(options) && isRetriableNetworkError(error)) {
180
+ // Unref so a pending retry never keeps the host process alive past
181
+ // its natural exit point; long-running apps still retry because the
182
+ // event loop is held open by their own work.
183
+ setTimeout(attempt, getRetryDelay(options, attemptIndex), attemptIndex + 1).unref()
184
+ } else {
185
+ callback(error)
186
+ }
169
187
  })
170
188
 
171
189
  req.setTimeout(timeout, () => {
@@ -185,14 +203,7 @@ function request (data, options, callback) {
185
203
  })
186
204
  }
187
205
 
188
- // The setTimeout is needed to avoid losing the async context in the retry
189
- // request before socket.connect() is called. This is a workaround for the
190
- // issue that the AsyncLocalStorage.run() method does not call the
191
- // AsyncLocalStorage.enterWith() method when not using AsyncContextFrame.
192
- //
193
- // TODO: Test that this doesn't trace itself on retry when the diagnostics
194
- // channel events are available in the agent exporter.
195
- makeRequest(() => setTimeout(() => makeRequest(callback)))
206
+ attempt(1)
196
207
  }
197
208
 
198
209
  function byteLength (data) {
@@ -0,0 +1,104 @@
1
+ 'use strict'
2
+
3
+ const RATE_LIMIT_MAX_WAIT_MS = 30_000
4
+
5
+ const SINGLE_RETRY_BASE_MS = 5000
6
+ const SINGLE_RETRY_JITTER_MS = 2500
7
+
8
+ const STARTUP_GRACE_MS = 30_000
9
+ const STARTUP_BACKOFF_BASE_MS = 1000
10
+ const STARTUP_BACKOFF_MAX_MS = 8000
11
+ const STARTUP_BACKOFF_JITTER_MS = 500
12
+ const STARTUP_MAX_ATTEMPTS = 5
13
+ const POST_STARTUP_MAX_ATTEMPTS = 2
14
+
15
+ // `ECONNREFUSED` and `ENOENT` cover the agent-not-yet-listening cases (TCP and
16
+ // UDS). `EAI_AGAIN` covers transient DNS in agentless intake. `ENOTFOUND` is
17
+ // excluded because it usually means a misconfigured host, not a transient state.
18
+ const RETRIABLE_NETWORK_CODES = new Set([
19
+ 'EAI_AGAIN',
20
+ 'ECONNREFUSED',
21
+ 'ECONNRESET',
22
+ 'ENOENT',
23
+ 'EPIPE',
24
+ 'ETIMEDOUT',
25
+ ])
26
+
27
+ const startedAtMs = Date.now()
28
+ const reachedEndpoints = new Set()
29
+
30
+ /**
31
+ * @typedef {object} EndpointOptions
32
+ * @property {string} [socketPath]
33
+ * @property {string} [hostname]
34
+ * @property {string} [host]
35
+ * @property {string|number} [port]
36
+ */
37
+
38
+ /**
39
+ * @param {Error & { code?: string }} error
40
+ */
41
+ function isRetriableNetworkError (error) {
42
+ return error?.code !== undefined && RETRIABLE_NETWORK_CODES.has(error.code)
43
+ }
44
+
45
+ function singleJitteredDelay () {
46
+ return SINGLE_RETRY_BASE_MS + Math.random() * SINGLE_RETRY_JITTER_MS
47
+ }
48
+
49
+ /**
50
+ * Stable key identifying the destination so the startup-phase gate is scoped
51
+ * per endpoint. UDS path beats host:port because both can coexist on the same
52
+ * options object after `parseUrl` runs.
53
+ *
54
+ * @param {EndpointOptions} options
55
+ */
56
+ function getEndpointKey (options) {
57
+ if (options.socketPath) return options.socketPath
58
+ return `${options.hostname || options.host || ''}:${options.port || ''}`
59
+ }
60
+
61
+ /**
62
+ * @param {EndpointOptions} options
63
+ */
64
+ function inStartupPhase (options) {
65
+ if ((Date.now() - startedAtMs) >= STARTUP_GRACE_MS) return false
66
+ return !reachedEndpoints.has(getEndpointKey(options))
67
+ }
68
+
69
+ /**
70
+ * Wait time before the next attempt when the previous one just failed. Bounded
71
+ * exponential backoff with small jitter inside the startup grace window;
72
+ * single 5–7.5 s jittered retry afterwards.
73
+ *
74
+ * @param {EndpointOptions} options
75
+ * @param {number} previousAttempt 1-based index of the attempt that just failed.
76
+ */
77
+ function getRetryDelay (options, previousAttempt) {
78
+ if (!inStartupPhase(options)) return singleJitteredDelay()
79
+ const exp = Math.min(STARTUP_BACKOFF_MAX_MS, STARTUP_BACKOFF_BASE_MS << (previousAttempt - 1))
80
+ return exp + Math.random() * STARTUP_BACKOFF_JITTER_MS
81
+ }
82
+
83
+ /**
84
+ * @param {EndpointOptions} options
85
+ */
86
+ function getMaxAttempts (options) {
87
+ return inStartupPhase(options) ? STARTUP_MAX_ATTEMPTS : POST_STARTUP_MAX_ATTEMPTS
88
+ }
89
+
90
+ /**
91
+ * @param {EndpointOptions} options
92
+ */
93
+ function markEndpointReached (options) {
94
+ reachedEndpoints.add(getEndpointKey(options))
95
+ }
96
+
97
+ module.exports = {
98
+ RATE_LIMIT_MAX_WAIT_MS,
99
+ getMaxAttempts,
100
+ getRetryDelay,
101
+ isRetriableNetworkError,
102
+ markEndpointReached,
103
+ singleJitteredDelay,
104
+ }
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const log = require('./log')
7
+ const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('./plugins/util/tags')
8
+ const {
9
+ getGitMetadataFromGitProperties,
10
+ getRemoteOriginURL,
11
+ removeUserSensitiveInfo,
12
+ resolveGitHeadSHA,
13
+ } = require('./config/git_properties')
14
+
15
+ /** @type {{ commitSHA: string | undefined, repositoryUrl: string | undefined } | undefined} */
16
+ let cached
17
+
18
+ /**
19
+ * @param {import('./config/config-types').ConfigProperties} config
20
+ */
21
+ function getGitMetadata (config) {
22
+ if (cached) return cached
23
+
24
+ if (!config.DD_TRACE_GIT_METADATA_ENABLED) {
25
+ cached = { commitSHA: undefined, repositoryUrl: undefined }
26
+ return cached
27
+ }
28
+
29
+ let repositoryUrl = removeUserSensitiveInfo(config.DD_GIT_REPOSITORY_URL ?? config.tags[GIT_REPOSITORY_URL])
30
+ let commitSHA = config.DD_GIT_COMMIT_SHA ?? config.tags[GIT_COMMIT_SHA]
31
+
32
+ if (!repositoryUrl || !commitSHA) {
33
+ const propertiesFile = config.DD_GIT_PROPERTIES_FILE
34
+ const gitPropertiesFile = propertiesFile ?? `${process.cwd()}/git.properties`
35
+ try {
36
+ const fromProperties = getGitMetadataFromGitProperties(fs.readFileSync(gitPropertiesFile, 'utf8'))
37
+ commitSHA ??= fromProperties.commitSHA
38
+ repositoryUrl ??= fromProperties.repositoryUrl
39
+ } catch (error) {
40
+ if (propertiesFile) {
41
+ log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', gitPropertiesFile, error)
42
+ }
43
+ }
44
+ }
45
+
46
+ const folderPath = config.DD_GIT_FOLDER_PATH
47
+ const gitFolderPath = folderPath ?? path.join(process.cwd(), '.git')
48
+
49
+ if (!repositoryUrl) {
50
+ const gitConfigPath = path.join(gitFolderPath, 'config')
51
+ try {
52
+ repositoryUrl = getRemoteOriginURL(fs.readFileSync(gitConfigPath, 'utf8'))
53
+ } catch (error) {
54
+ if (folderPath) {
55
+ log.error('Error reading git config: %s', gitConfigPath, error)
56
+ }
57
+ }
58
+ }
59
+
60
+ commitSHA ??= resolveGitHeadSHA(gitFolderPath)
61
+
62
+ cached = { commitSHA, repositoryUrl }
63
+ return cached
64
+ }
65
+
66
+ module.exports = getGitMetadata
@@ -1,17 +1,25 @@
1
1
  'use strict'
2
2
 
3
3
  const { SCI_COMMIT_SHA, SCI_REPOSITORY_URL } = require('./constants')
4
+ const getGitMetadata = require('./git_metadata')
4
5
 
5
6
  class GitMetadataTagger {
7
+ #commitSHA
8
+ #repositoryUrl
9
+ #enabled
10
+
6
11
  constructor (config) {
7
- this._config = config
12
+ this.#enabled = config.DD_TRACE_GIT_METADATA_ENABLED
13
+ const { commitSHA, repositoryUrl } = getGitMetadata(config)
14
+ this.#commitSHA = commitSHA
15
+ this.#repositoryUrl = repositoryUrl
8
16
  }
9
17
 
10
18
  tagGitMetadata (spanContext) {
11
- if (this._config.DD_TRACE_GIT_METADATA_ENABLED) {
12
- // These tags are added only to the local root span
13
- spanContext._trace.tags[SCI_COMMIT_SHA] = this._config.commitSHA
14
- spanContext._trace.tags[SCI_REPOSITORY_URL] = this._config.repositoryUrl
19
+ if (this.#enabled) {
20
+ const tags = spanContext._trace.tags
21
+ tags[SCI_COMMIT_SHA] = this.#commitSHA
22
+ tags[SCI_REPOSITORY_URL] = this.#repositoryUrl
15
23
  }
16
24
  }
17
25
  }
@@ -7,19 +7,19 @@ const UINT_MAX = 4_294_967_296
7
7
  const data = new Uint8Array(8 * 8192)
8
8
  const zeroId = new Uint8Array(8)
9
9
 
10
- const map = Array.prototype.map
11
- const pad = byte => `${byte < 16 ? '0' : ''}${byte.toString(16)}`
12
-
13
10
  let batch = 0
14
11
 
15
12
  // Internal representation of a trace or span ID.
16
13
  class Identifier {
14
+ /** @type {number[] | Uint8Array} */
15
+ #buffer
16
+
17
17
  /**
18
18
  * @param {string} value
19
19
  * @param {number} [radix]
20
20
  */
21
21
  constructor (value, radix = 16) {
22
- this._buffer = radix === 16
22
+ this.#buffer = radix === 16
23
23
  ? createBuffer(value)
24
24
  : fromString(value, radix)
25
25
  }
@@ -30,32 +30,32 @@ class Identifier {
30
30
  */
31
31
  toString (radix = 16) {
32
32
  return radix === 16
33
- ? toHexString(this._buffer)
34
- : toNumberString(this._buffer, radix)
33
+ ? Buffer.from(this.#buffer).toString('hex')
34
+ : toNumberString(this.#buffer, radix)
35
35
  }
36
36
 
37
37
  /**
38
38
  * @returns {bigint}
39
39
  */
40
40
  toBigInt () {
41
- return Buffer.from(this._buffer).readBigUInt64BE(0)
41
+ return Buffer.from(this.#buffer).readBigUInt64BE(0)
42
42
  }
43
43
 
44
44
  /**
45
45
  * @returns {number[] | Uint8Array}
46
46
  */
47
47
  toBuffer () {
48
- return this._buffer
48
+ return this.#buffer
49
49
  }
50
50
 
51
51
  /**
52
52
  * @returns {number[] | Uint8Array}
53
53
  */
54
54
  toArray () {
55
- if (this._buffer.length === 8) {
56
- return this._buffer
55
+ if (this.#buffer.length === 8) {
56
+ return this.#buffer
57
57
  }
58
- return this._buffer.slice(-8)
58
+ return this.#buffer.slice(-8)
59
59
  }
60
60
 
61
61
  /**
@@ -70,12 +70,10 @@ class Identifier {
70
70
  * @returns {boolean}
71
71
  */
72
72
  equals (other) {
73
- const length = this._buffer.length
74
- const otherLength = other._buffer.length
75
-
76
- // Only compare the bytes available in both IDs.
77
- for (let i = length, j = otherLength; i >= 0 && j >= 0; i--, j--) {
78
- if (this._buffer[i] !== other._buffer[j]) return false
73
+ // Big-endian suffix compare: when buffers differ in length, only the
74
+ // rightmost `min(this.length, other.length)` bytes are checked.
75
+ for (let i = this.#buffer.length - 1, j = other.#buffer.length - 1; i >= 0 && j >= 0; i--, j--) {
76
+ if (this.#buffer[i] !== other.#buffer[j]) return false
79
77
  }
80
78
 
81
79
  return true
@@ -174,15 +172,6 @@ function toNumberString (buffer, radix) {
174
172
  return str
175
173
  }
176
174
 
177
- // Convert a buffer to a hexadecimal string.
178
- /**
179
- * @param {number[] | Uint8Array} buffer
180
- * @returns {string}
181
- */
182
- function toHexString (buffer) {
183
- return map.call(buffer, pad).join('')
184
- }
185
-
186
175
  // Simple pseudo-random 64-bit ID generator.
187
176
  /**
188
177
  * @returns {number[] | Uint8Array}
@@ -7,7 +7,9 @@ module.exports = {
7
7
  DECORATOR: '_ml_obs.decorator',
8
8
  INTEGRATION: '_ml_obs.integration',
9
9
  METADATA: '_ml_obs.meta.metadata',
10
+ COST_TAGS: '_ml_obs.meta.metadata._dd.cost_tags',
10
11
  METRICS: '_ml_obs.metrics',
12
+ TOOL_DEFINITIONS: '_ml_obs.meta.tool_definitions',
11
13
  ML_APP: '_ml_obs.meta.ml_app',
12
14
  PROPAGATED_PARENT_ID_KEY: '_dd.p.llmobs_parent_id',
13
15
  PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags')
4
+ const { safeJsonParse } = require('../../util')
4
5
  const LLMObsPlugin = require('../base')
5
6
  const { appendMessage } = require('./util')
6
7
 
@@ -47,6 +48,8 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
47
48
  const { type } = contentBlock
48
49
  if (type === 'text') {
49
50
  response.content.push({ type, text: contentBlock.text })
51
+ } else if (type === 'thinking') {
52
+ response.content.push({ type, thinking: contentBlock.thinking ?? '' })
50
53
  } else if (type === 'tool_use') {
51
54
  response.content.push({ type, name: contentBlock.name, input: '', id: contentBlock.id })
52
55
  }
@@ -56,20 +59,29 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
56
59
  const { delta } = chunk
57
60
  if (!delta) continue
58
61
 
59
- const { text } = delta
60
- if (text) response.content[response.content.length - 1].text += text
61
-
62
- const partialJson = delta.partial_json
63
- if (partialJson && delta.type === 'input_json_delta') {
64
- response.content[response.content.length - 1].input += partialJson
62
+ const lastBlock = response.content[response.content.length - 1]
63
+ if (!lastBlock) continue
64
+
65
+ if (delta.type === 'thinking_delta') {
66
+ const { thinking } = delta
67
+ if (thinking) lastBlock.thinking += thinking
68
+ } else if (delta.type === 'signature_delta') {
69
+ // Signature is for internal verification only; skip it.
70
+ } else if (delta.type === 'input_json_delta') {
71
+ const partialJson = delta.partial_json
72
+ if (partialJson) lastBlock.input += partialJson
73
+ } else {
74
+ const { text } = delta
75
+ if (text) lastBlock.text += text
65
76
  }
66
77
  break
67
78
  }
68
79
  case 'content_block_stop': {
69
- const type = response.content[response.content.length - 1].type
70
- if (type === 'tool_use') {
71
- const input = response.content[response.content.length - 1].input ?? '{}'
72
- response.content[response.content.length - 1].input = JSON.parse(input)
80
+ const lastBlock = response.content[response.content.length - 1]
81
+ if (!lastBlock) break
82
+ if (lastBlock.type === 'tool_use') {
83
+ const input = lastBlock.input ?? '{}'
84
+ lastBlock.input = safeJsonParse(input, {})
73
85
  }
74
86
  break
75
87
  }
@@ -167,18 +179,17 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
167
179
 
168
180
  const outputMessages = []
169
181
  for (const block of content) {
182
+ if (block.type === 'thinking') {
183
+ outputMessages.push({ content: block.thinking ?? '', role: 'reasoning' })
184
+ continue
185
+ }
170
186
  const { text } = block
171
187
  if (typeof text === 'string') {
172
188
  outputMessages.push({ content: text, role })
173
189
  } else if (block.type === 'tool_use') {
174
- let input = block.input
175
- if (typeof input === 'string') {
176
- input = JSON.parse(input)
177
- }
178
-
179
190
  const toolCall = {
180
191
  name: block.name,
181
- arguments: input,
192
+ arguments: safeJsonParse(block.input, {}),
182
193
  toolId: block.id,
183
194
  type: block.type,
184
195
  }
@@ -3,6 +3,7 @@
3
3
  /**
4
4
  * @typedef {{type: 'text', text: string}} TextBlock
5
5
  * @typedef {{type: 'image'}} ImageBlock
6
+ * @typedef {{type: 'thinking', thinking: string, signature?: string}} ThinkingBlock
6
7
  * @typedef {{
7
8
  * type: 'tool_use', text: string, name: string, id: string, input: string | Record<string, unknown>
8
9
  * }} ToolUseBlock
@@ -70,6 +71,8 @@ function appendMessage (messages, { role, content }) {
70
71
  messages.push({ content: block.text, role })
71
72
  } else if (block.type === 'image') {
72
73
  messages.push({ content: '([IMAGE DETECTED])', role })
74
+ } else if (block.type === 'thinking') {
75
+ messages.push({ content: block.thinking ?? '', role: 'reasoning' })
73
76
  } else if (block.type === 'tool_use') {
74
77
  const { text, name, id, type } = block
75
78
  let input = block.input