dd-trace 5.49.0 → 5.50.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 (25) hide show
  1. package/LICENSE-3rdparty.csv +1 -4
  2. package/README.md +5 -15
  3. package/index.d.ts +1 -0
  4. package/package.json +4 -8
  5. package/packages/datadog-core/src/storage.js +4 -3
  6. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +0 -1
  7. package/packages/datadog-shimmer/src/shimmer.js +76 -68
  8. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -17
  9. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  10. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +1 -1
  11. package/packages/dd-trace/src/config.js +24 -32
  12. package/packages/dd-trace/src/datastreams/processor.js +3 -5
  13. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +34 -16
  14. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -1
  15. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  16. package/packages/dd-trace/src/dogstatsd.js +11 -4
  17. package/packages/dd-trace/src/llmobs/index.js +4 -1
  18. package/packages/dd-trace/src/llmobs/sdk.js +146 -112
  19. package/packages/dd-trace/src/llmobs/tagger.js +13 -9
  20. package/packages/dd-trace/src/llmobs/telemetry.js +50 -1
  21. package/packages/dd-trace/src/payload-tagging/jsonpath-plus.js +1 -1
  22. package/packages/dd-trace/src/profiling/config.js +0 -6
  23. package/packages/dd-trace/src/profiling/profilers/wall.js +8 -12
  24. package/packages/dd-trace/src/span_stats.js +2 -2
  25. package/packages/dd-trace/src/debugger/devtools_client/lock.js +0 -8
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const lock = require('mutexify/promise')()
3
4
  const { getGeneratedPosition } = require('./source-maps')
4
- const lock = require('./lock')()
5
5
  const session = require('./session')
6
6
  const { compile: compileCondition, compileSegments, templateRequiresEvaluation } = require('./condition')
7
7
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
@@ -64,14 +64,14 @@ async function addBreakpoint (probe) {
64
64
  const release = await lock()
65
65
 
66
66
  try {
67
- log.debug(
68
- '[debugger:devtools_client] Adding breakpoint at %s:%d:%d (probe: %s, version: %d)',
69
- url, lineNumber, columnNumber, probe.id, probe.version
70
- )
71
-
72
67
  const locationKey = generateLocationKey(scriptId, lineNumber, columnNumber)
73
68
  const breakpoint = locationToBreakpoint.get(locationKey)
74
69
 
70
+ log.debug(
71
+ '[debugger:devtools_client] %s breakpoint at %s:%d:%d (probe: %s, version: %d)',
72
+ breakpoint ? 'Updating' : 'Adding', url, lineNumber, columnNumber, probe.id, probe.version
73
+ )
74
+
75
75
  if (breakpoint) {
76
76
  // A breakpoint already exists at this location, so we need to add the probe to the existing breakpoint
77
77
  await updateBreakpoint(breakpoint, probe)
@@ -82,10 +82,15 @@ async function addBreakpoint (probe) {
82
82
  lineNumber: lineNumber - 1, // Beware! lineNumber is zero-indexed
83
83
  columnNumber
84
84
  }
85
- const result = await session.post('Debugger.setBreakpoint', {
86
- location,
87
- condition: probe.condition
88
- })
85
+ let result
86
+ try {
87
+ result = await session.post('Debugger.setBreakpoint', {
88
+ location,
89
+ condition: probe.condition
90
+ })
91
+ } catch (err) {
92
+ throw new Error(`Error setting breakpoint for probe ${probe.id}`, { cause: err })
93
+ }
89
94
  probeToLocation.set(probe.id, locationKey)
90
95
  locationToBreakpoint.set(locationKey, { id: result.breakpointId, location, locationKey })
91
96
  breakpointToProbes.set(result.breakpointId, new Map([[probe.id, probe]]))
@@ -120,7 +125,11 @@ async function removeBreakpoint ({ id }) {
120
125
  if (breakpointToProbes.size === 0) {
121
126
  await stop() // TODO: Will this actually delete the breakpoint?
122
127
  } else {
123
- await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
128
+ try {
129
+ await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
130
+ } catch (err) {
131
+ throw new Error(`Error removing breakpoint for probe ${id}`, { cause: err })
132
+ }
124
133
  }
125
134
  } else {
126
135
  await updateBreakpoint(breakpoint)
@@ -144,12 +153,21 @@ async function updateBreakpoint (breakpoint, probe) {
144
153
  const condition = compileCompoundCondition(Array.from(probesAtLocation.values()))
145
154
 
146
155
  if (condition || conditionBeforeNewProbe !== condition) {
147
- await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
156
+ try {
157
+ await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
158
+ } catch (err) {
159
+ throw new Error(`Error removing breakpoint for probe ${probe.id}`, { cause: err })
160
+ }
148
161
  breakpointToProbes.delete(breakpoint.id)
149
- const result = await session.post('Debugger.setBreakpoint', {
150
- location: breakpoint.location,
151
- condition
152
- })
162
+ let result
163
+ try {
164
+ result = await session.post('Debugger.setBreakpoint', {
165
+ location: breakpoint.location,
166
+ condition
167
+ })
168
+ } catch (err) {
169
+ throw new Error(`Error setting breakpoint for probe ${probe.id}`, { cause: err })
170
+ }
153
171
  breakpoint.id = result.breakpointId
154
172
  breakpointToProbes.set(result.breakpointId, probesAtLocation)
155
173
  }
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { workerData: { rcPort } } = require('node:worker_threads')
4
- const lock = require('./lock')()
4
+ const lock = require('mutexify/promise')()
5
5
  const { addBreakpoint, removeBreakpoint } = require('./breakpoints')
6
6
  const { ackReceived, ackInstalled, ackError } = require('./status')
7
7
  const log = require('../../log')
@@ -26,7 +26,7 @@ const ddtags = [
26
26
  ['host_name', hostname],
27
27
  [GIT_COMMIT_SHA, config.commitSHA],
28
28
  [GIT_REPOSITORY_URL, config.repositoryUrl]
29
- ].map((pair) => pair.join(':')).join(',')
29
+ ].filter(([, value]) => value !== undefined).map((pair) => pair.join(':')).join(',')
30
30
 
31
31
  const path = `/debugger/v1/input?${stringify({ ddtags })}`
32
32
 
@@ -308,12 +308,16 @@ class MetricsAggregationClient {
308
308
  }
309
309
 
310
310
  for (const [tag, next] of node.nodes) {
311
- this._captureNode(next, name, tags.concat(tag), fn)
311
+ tags.push(tag)
312
+ this._captureNode(next, name, tags, fn)
313
+ tags.pop()
312
314
  }
313
315
  }
314
316
 
315
- _ensureTree (tree, name, tags, value) {
316
- tags = tags ? [].concat(tags) : []
317
+ _ensureTree (tree, name, tags = [], value) {
318
+ if (!Array.isArray(tags)) {
319
+ tags = [tags]
320
+ }
317
321
 
318
322
  let node = this._ensureNode(tree, name, value)
319
323
 
@@ -331,7 +335,10 @@ class MetricsAggregationClient {
331
335
 
332
336
  if (!node) {
333
337
  node = { nodes: new Map(), touched: false, value }
334
- container.set(key, node)
338
+
339
+ if (typeof key === 'string') {
340
+ container.set(key, node)
341
+ }
335
342
  }
336
343
 
337
344
  return node
@@ -4,9 +4,9 @@ const log = require('../log')
4
4
  const { PROPAGATED_PARENT_ID_KEY } = require('./constants/tags')
5
5
  const { storage } = require('./storage')
6
6
 
7
+ const telemetry = require('./telemetry')
7
8
  const LLMObsSpanProcessor = require('./span_processor')
8
9
 
9
- const telemetry = require('./telemetry')
10
10
  const { channel } = require('dc-polyfill')
11
11
  const spanProcessCh = channel('dd-trace:span:process')
12
12
  const evalMetricAppendCh = channel('llmobs:eval-metric:append')
@@ -94,12 +94,15 @@ function handleLLMObsParentIdInjection ({ carrier }) {
94
94
  }
95
95
 
96
96
  function handleFlush () {
97
+ let err = ''
97
98
  try {
98
99
  spanWriter.flush()
99
100
  evalWriter.flush()
100
101
  } catch (e) {
102
+ err = 'writer_flush_error'
101
103
  log.warn(`Failed to flush LLMObs spans and evaluation metrics: ${e.message}`)
102
104
  }
105
+ telemetry.recordUserFlush(err)
103
106
  }
104
107
 
105
108
  function handleSpanProcess (data) {
@@ -201,7 +201,7 @@ class LLMObs extends NoopLLMObs {
201
201
  return this._tracer.wrap(name, spanOptions, wrapped)
202
202
  }
203
203
 
204
- annotate (span, options) {
204
+ annotate (span, options, autoinstrumented = false) {
205
205
  if (!this.enabled) return
206
206
 
207
207
  if (!span) {
@@ -213,150 +213,184 @@ class LLMObs extends NoopLLMObs {
213
213
  span = this._active()
214
214
  }
215
215
 
216
- if (!span) {
217
- throw new Error('No span provided and no active LLMObs-generated span found')
218
- }
219
- if (!options) {
220
- throw new Error('No options provided for annotation.')
221
- }
216
+ let err = ''
222
217
 
223
- if (!LLMObsTagger.tagMap.has(span)) {
224
- throw new Error('Span must be an LLMObs-generated span')
225
- }
226
- if (span._duration !== undefined) {
227
- throw new Error('Cannot annotate a finished span')
228
- }
218
+ try {
219
+ if (!span) {
220
+ err = 'invalid_span_no_active_spans'
221
+ throw new Error('No span provided and no active LLMObs-generated span found')
222
+ }
223
+ if (!options) {
224
+ err = 'invalid_options'
225
+ throw new Error('No options provided for annotation.')
226
+ }
229
227
 
230
- const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
231
- if (!spanKind) {
232
- throw new Error('LLMObs span must have a span kind specified')
233
- }
228
+ if (!LLMObsTagger.tagMap.has(span)) {
229
+ err = 'invalid_span_type'
230
+ throw new Error('Span must be an LLMObs-generated span')
231
+ }
232
+ if (span._duration !== undefined) {
233
+ err = 'invalid_finished_span'
234
+ throw new Error('Cannot annotate a finished span')
235
+ }
234
236
 
235
- const { inputData, outputData, metadata, metrics, tags } = options
236
-
237
- if (inputData || outputData) {
238
- if (spanKind === 'llm') {
239
- this._tagger.tagLLMIO(span, inputData, outputData)
240
- } else if (spanKind === 'embedding') {
241
- this._tagger.tagEmbeddingIO(span, inputData, outputData)
242
- } else if (spanKind === 'retrieval') {
243
- this._tagger.tagRetrievalIO(span, inputData, outputData)
244
- } else {
245
- this._tagger.tagTextIO(span, inputData, outputData)
237
+ const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
238
+ if (!spanKind) {
239
+ err = 'invalid_no_span_kind'
240
+ throw new Error('LLMObs span must have a span kind specified')
246
241
  }
247
- }
248
242
 
249
- if (metadata) {
250
- this._tagger.tagMetadata(span, metadata)
251
- }
243
+ const { inputData, outputData, metadata, metrics, tags } = options
252
244
 
253
- if (metrics) {
254
- this._tagger.tagMetrics(span, metrics)
255
- }
245
+ if (inputData || outputData) {
246
+ if (spanKind === 'llm') {
247
+ this._tagger.tagLLMIO(span, inputData, outputData)
248
+ } else if (spanKind === 'embedding') {
249
+ this._tagger.tagEmbeddingIO(span, inputData, outputData)
250
+ } else if (spanKind === 'retrieval') {
251
+ this._tagger.tagRetrievalIO(span, inputData, outputData)
252
+ } else {
253
+ this._tagger.tagTextIO(span, inputData, outputData)
254
+ }
255
+ }
256
256
 
257
- if (tags) {
258
- this._tagger.tagSpanTags(span, tags)
257
+ if (metadata) {
258
+ this._tagger.tagMetadata(span, metadata)
259
+ }
260
+ if (metrics) {
261
+ this._tagger.tagMetrics(span, metrics)
262
+ }
263
+ if (tags) {
264
+ this._tagger.tagSpanTags(span, tags)
265
+ }
266
+ } catch (e) {
267
+ if (e.ddErrorTag) {
268
+ err = e.ddErrorTag
269
+ }
270
+ throw e
271
+ } finally {
272
+ if (autoinstrumented === false) {
273
+ telemetry.recordLLMObsAnnotate(span, err)
274
+ }
259
275
  }
260
276
  }
261
277
 
262
278
  exportSpan (span) {
263
279
  span = span || this._active()
264
-
265
- if (!span) {
266
- throw new Error('No span provided and no active LLMObs-generated span found')
267
- }
268
-
269
- if (!(span instanceof Span)) {
270
- throw new Error('Span must be a valid Span object.')
271
- }
272
-
273
- if (!LLMObsTagger.tagMap.has(span)) {
274
- throw new Error('Span must be an LLMObs-generated span')
280
+ let err = ''
281
+ try {
282
+ if (!span) {
283
+ err = 'no_active_span'
284
+ throw new Error('No span provided and no active LLMObs-generated span found')
285
+ }
286
+ if (!(span instanceof Span)) {
287
+ err = 'invalid_span'
288
+ throw new Error('Span must be a valid Span object.')
289
+ }
290
+ if (!LLMObsTagger.tagMap.has(span)) {
291
+ err = 'invalid_span'
292
+ throw new Error('Span must be an LLMObs-generated span')
293
+ }
294
+ } catch (e) {
295
+ telemetry.recordExportSpan(span, err)
296
+ throw e
275
297
  }
276
-
277
298
  try {
278
299
  return {
279
300
  traceId: span.context().toTraceId(true),
280
301
  spanId: span.context().toSpanId()
281
302
  }
282
303
  } catch {
283
- logger.warn('Faild to export span. Span must be a valid Span object.')
304
+ err = 'invalid_span'
305
+ logger.warn('Failed to export span. Span must be a valid Span object.')
306
+ } finally {
307
+ telemetry.recordExportSpan(span, err)
284
308
  }
285
309
  }
286
310
 
287
311
  submitEvaluation (llmobsSpanContext, options = {}) {
288
312
  if (!this.enabled) return
289
313
 
314
+ let err = ''
290
315
  const { traceId, spanId } = llmobsSpanContext
291
- if (!traceId || !spanId) {
292
- throw new Error(
293
- 'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
294
- )
295
- }
296
-
297
- const mlApp = options.mlApp || this._config.llmobs.mlApp
298
- if (!mlApp) {
299
- throw new Error(
300
- 'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
301
- )
302
- }
303
-
304
- const timestampMs = options.timestampMs || Date.now()
305
- if (typeof timestampMs !== 'number' || timestampMs < 0) {
306
- throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
307
- }
316
+ try {
317
+ if (!traceId || !spanId) {
318
+ err = 'invalid_span'
319
+ throw new Error(
320
+ 'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
321
+ )
322
+ }
323
+ const mlApp = options.mlApp || this._config.llmobs.mlApp
324
+ if (!mlApp) {
325
+ err = 'missing_ml_app'
326
+ throw new Error(
327
+ 'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
328
+ )
329
+ }
308
330
 
309
- const { label, value, tags } = options
310
- const metricType = options.metricType?.toLowerCase()
311
- if (!label) {
312
- throw new Error('label must be the specified name of the evaluation metric')
313
- }
314
- if (!metricType || !['categorical', 'score'].includes(metricType)) {
315
- throw new Error('metricType must be one of "categorical" or "score"')
316
- }
331
+ const timestampMs = options.timestampMs || Date.now()
332
+ if (typeof timestampMs !== 'number' || timestampMs < 0) {
333
+ err = 'invalid_timestamp'
334
+ throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
335
+ }
317
336
 
318
- if (metricType === 'categorical' && typeof value !== 'string') {
319
- throw new Error('value must be a string for a categorical metric.')
320
- }
321
- if (metricType === 'score' && typeof value !== 'number') {
322
- throw new Error('value must be a number for a score metric.')
323
- }
337
+ const { label, value, tags } = options
338
+ const metricType = options.metricType?.toLowerCase()
339
+ if (!label) {
340
+ err = 'invalid_metric_label'
341
+ throw new Error('label must be the specified name of the evaluation metric')
342
+ }
343
+ if (!metricType || !['categorical', 'score'].includes(metricType)) {
344
+ err = 'invalid_metric_type'
345
+ throw new Error('metricType must be one of "categorical" or "score"')
346
+ }
347
+ if (metricType === 'categorical' && typeof value !== 'string') {
348
+ err = 'invalid_metric_value'
349
+ throw new Error('value must be a string for a categorical metric.')
350
+ }
351
+ if (metricType === 'score' && typeof value !== 'number') {
352
+ err = 'invalid_metric_value'
353
+ throw new Error('value must be a number for a score metric.')
354
+ }
324
355
 
325
- const evaluationTags = {
326
- 'ddtrace.version': tracerVersion,
327
- ml_app: mlApp
328
- }
356
+ const evaluationTags = {
357
+ 'ddtrace.version': tracerVersion,
358
+ ml_app: mlApp
359
+ }
329
360
 
330
- if (tags) {
331
- for (const key in tags) {
332
- const tag = tags[key]
333
- if (typeof tag === 'string') {
334
- evaluationTags[key] = tag
335
- } else if (typeof tag.toString === 'function') {
336
- evaluationTags[key] = tag.toString()
337
- } else if (tag == null) {
338
- evaluationTags[key] = Object.prototype.toString.call(tag)
339
- } else {
340
- // should be a rare case
341
- // every object in JS has a toString, otherwise every primitive has its own toString
342
- // null and undefined are handled above
343
- throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
361
+ if (tags) {
362
+ for (const key in tags) {
363
+ const tag = tags[key]
364
+ if (typeof tag === 'string') {
365
+ evaluationTags[key] = tag
366
+ } else if (typeof tag.toString === 'function') {
367
+ evaluationTags[key] = tag.toString()
368
+ } else if (tag == null) {
369
+ evaluationTags[key] = Object.prototype.toString.call(tag)
370
+ } else {
371
+ // should be a rare case
372
+ // every object in JS has a toString, otherwise every primitive has its own toString
373
+ // null and undefined are handled above
374
+ err = 'invalid_tags'
375
+ throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
376
+ }
344
377
  }
345
378
  }
346
- }
347
379
 
348
- const payload = {
349
- span_id: spanId,
350
- trace_id: traceId,
351
- label,
352
- metric_type: metricType,
353
- ml_app: mlApp,
354
- [`${metricType}_value`]: value,
355
- timestamp_ms: timestampMs,
356
- tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
380
+ const payload = {
381
+ span_id: spanId,
382
+ trace_id: traceId,
383
+ label,
384
+ metric_type: metricType,
385
+ ml_app: mlApp,
386
+ [`${metricType}_value`]: value,
387
+ timestamp_ms: timestampMs,
388
+ tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
389
+ }
390
+ evalMetricAppendCh.publish(payload)
391
+ } finally {
392
+ telemetry.recordSubmitEvaluation(options, err)
357
393
  }
358
-
359
- evalMetricAppendCh.publish(payload)
360
394
  }
361
395
 
362
396
  flush () {
@@ -375,7 +409,7 @@ class LLMObs extends NoopLLMObs {
375
409
  annotations.outputData = output
376
410
  }
377
411
 
378
- this.annotate(span, annotations)
412
+ this.annotate(span, annotations, true)
379
413
  }
380
414
 
381
415
  _active () {
@@ -135,7 +135,7 @@ class LLMObsTagger {
135
135
  if (typeof value === 'number') {
136
136
  filterdMetrics[processedKey] = value
137
137
  } else {
138
- this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`)
138
+ this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`, 'invalid_metrics')
139
139
  }
140
140
  }
141
141
 
@@ -169,7 +169,7 @@ class LLMObsTagger {
169
169
  this._setTag(span, key, JSON.stringify(data))
170
170
  } catch {
171
171
  const type = key === INPUT_VALUE ? 'input' : 'output'
172
- this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`)
172
+ this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`, 'invalid_io_text')
173
173
  }
174
174
  }
175
175
  }
@@ -187,7 +187,7 @@ class LLMObsTagger {
187
187
  }
188
188
 
189
189
  if (document == null || typeof document !== 'object') {
190
- this._handleFailure('Documents must be a string, object, or list of objects.')
190
+ this._handleFailure('Documents must be a string, object, or list of objects.', 'invalid_embedding_io')
191
191
  return undefined
192
192
  }
193
193
 
@@ -195,7 +195,7 @@ class LLMObsTagger {
195
195
  let validDocument = true
196
196
 
197
197
  if (typeof text !== 'string') {
198
- this._handleFailure('Document text must be a string.')
198
+ this._handleFailure('Document text must be a string.', 'invalid_embedding_io')
199
199
  validDocument = false
200
200
  }
201
201
 
@@ -226,7 +226,7 @@ class LLMObsTagger {
226
226
  }
227
227
 
228
228
  if (message == null || typeof message !== 'object') {
229
- this._handleFailure('Messages must be a string, object, or list of objects')
229
+ this._handleFailure('Messages must be a string, object, or list of objects', 'invalid_io_messages')
230
230
  return undefined
231
231
  }
232
232
 
@@ -237,7 +237,7 @@ class LLMObsTagger {
237
237
  const messageObj = { content }
238
238
 
239
239
  if (typeof content !== 'string') {
240
- this._handleFailure('Message content must be a string.')
240
+ this._handleFailure('Message content must be a string.', 'invalid_io_messages')
241
241
  validMessage = false
242
242
  }
243
243
 
@@ -250,7 +250,7 @@ class LLMObsTagger {
250
250
 
251
251
  const filteredToolCalls = toolCalls.map(toolCall => {
252
252
  if (typeof toolCall !== 'object') {
253
- this._handleFailure('Tool call must be an object.')
253
+ this._handleFailure('Tool call must be an object.', 'invalid_io_messages')
254
254
  return undefined
255
255
  }
256
256
 
@@ -313,11 +313,15 @@ class LLMObsTagger {
313
313
 
314
314
  // any public-facing LLMObs APIs using this tagger should not soft fail
315
315
  // auto-instrumentation should soft fail
316
- _handleFailure (msg) {
316
+ _handleFailure (msg, errorTag) {
317
317
  if (this.softFail) {
318
318
  log.warn(msg)
319
319
  } else {
320
- throw new Error(msg)
320
+ const err = new Error(msg)
321
+ if (errorTag) {
322
+ Object.defineProperty(err, 'ddErrorTag', { get () { return errorTag } })
323
+ }
324
+ throw err
321
325
  }
322
326
  }
323
327
 
@@ -109,11 +109,60 @@ function recordDroppedPayload (numEvents, eventType, error) {
109
109
  llmobsMetrics.count(metricName, tags).inc(numEvents)
110
110
  }
111
111
 
112
+ function recordLLMObsAnnotate (span, err, value = 1) {
113
+ const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
114
+ const spanKind = mlObsTags[SPAN_KIND] || 'N/A'
115
+ const isRootSpan = mlObsTags[PARENT_ID_KEY] === ROOT_PARENT_ID
116
+
117
+ const tags = {
118
+ error: Number(!!err),
119
+ span_kind: spanKind,
120
+ is_root_span: Number(isRootSpan)
121
+ }
122
+ if (err) tags.error_type = err
123
+ llmobsMetrics.count('annotations', tags).inc(value)
124
+ }
125
+
126
+ function recordUserFlush (err, value = 1) {
127
+ const tags = { error: Number(!!err) }
128
+ if (err) tags.error_type = err
129
+ llmobsMetrics.count('user_flush', tags).inc(value)
130
+ }
131
+
132
+ function recordExportSpan (span, err, value = 1) {
133
+ const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
134
+ const spanKind = mlObsTags[SPAN_KIND] || 'N/A'
135
+ const isRootSpan = mlObsTags[PARENT_ID_KEY] === ROOT_PARENT_ID
136
+
137
+ const tags = {
138
+ error: Number(!!err),
139
+ span_kind: spanKind,
140
+ is_root_span: Number(isRootSpan)
141
+ }
142
+ if (err) tags.error_type = err
143
+ llmobsMetrics.count('spans_exported', tags).inc(value)
144
+ }
145
+
146
+ function recordSubmitEvaluation (options, err, value = 1) {
147
+ const tags = {
148
+ error: Number(!!err),
149
+ custom_joining_key: 0
150
+ }
151
+ const metricType = options?.metricType?.toLowerCase()
152
+ if (metricType !== 'categorical' && metricType !== 'score') tags.metric_type = 'other'
153
+ if (err) tags.error_type = err
154
+ llmobsMetrics.count('evals_submitted', tags).inc(value)
155
+ }
156
+
112
157
  module.exports = {
113
158
  recordLLMObsEnabled,
114
159
  incrementLLMObsSpanStartCount,
115
160
  incrementLLMObsSpanFinishedCount,
116
161
  recordLLMObsRawSpanSize,
117
162
  recordLLMObsSpanSize,
118
- recordDroppedPayload
163
+ recordDroppedPayload,
164
+ recordLLMObsAnnotate,
165
+ recordUserFlush,
166
+ recordExportSpan,
167
+ recordSubmitEvaluation
119
168
  }
@@ -1694,7 +1694,7 @@ JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
1694
1694
  * @param {string} parentPropName
1695
1695
  * @param {JSONPathCallback} callback
1696
1696
  * @param {boolean} hasArrExpr
1697
- * @param {boolean} literalPriority
1697
+ * @param {boolean} [literalPriority]
1698
1698
  * @returns {ReturnObject|ReturnObject[]}
1699
1699
  */
1700
1700
  JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback, hasArrExpr, literalPriority) {
@@ -15,7 +15,6 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
15
15
  const { tagger } = require('./tagger')
16
16
  const { isFalse, isTrue } = require('../util')
17
17
  const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata')
18
- const satisfies = require('semifies')
19
18
 
20
19
  class Config {
21
20
  constructor (options = {}) {
@@ -23,7 +22,6 @@ class Config {
23
22
  DD_AGENT_HOST,
24
23
  DD_ENV,
25
24
  DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
26
- DD_PROFILING_ASYNC_ID_ENABLED,
27
25
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
28
26
  DD_PROFILING_CPU_ENABLED,
29
27
  DD_PROFILING_DEBUG_SOURCE_MAPS,
@@ -181,10 +179,6 @@ class Config {
181
179
  this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled,
182
180
  DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true))
183
181
 
184
- // Async ID gathering only works reliably on Node >= 22.10.0
185
- this.asyncIdEnabled = isTrue(coalesce(options.asyncIdEnabled,
186
- DD_PROFILING_ASYNC_ID_ENABLED, this.timelineEnabled && satisfies(process.versions.node, '>=22.10.0')))
187
-
188
182
  this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
189
183
  DD_PROFILING_CODEHOTSPOTS_ENABLED,
190
184
  DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, samplingContextsAvailable))