dd-trace 6.17.0 → 6.19.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 (110) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +55 -11
  4. package/package.json +6 -6
  5. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  6. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  7. package/packages/datadog-instrumentations/src/cucumber.js +50 -9
  8. package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
  9. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
  12. package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +51 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
  18. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +6 -0
  19. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  20. package/packages/datadog-instrumentations/src/jest.js +30 -13
  21. package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
  23. package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
  24. package/packages/datadog-instrumentations/src/playwright.js +73 -6
  25. package/packages/datadog-instrumentations/src/undici.js +7 -3
  26. package/packages/datadog-instrumentations/src/vitest-main.js +95 -21
  27. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  28. package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
  29. package/packages/datadog-instrumentations/src/ws.js +41 -1
  30. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  31. package/packages/datadog-plugin-cucumber/src/index.js +22 -8
  32. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -14
  33. package/packages/datadog-plugin-cypress/src/index.js +3 -1
  34. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  35. package/packages/datadog-plugin-jest/src/index.js +3 -4
  36. package/packages/datadog-plugin-mocha/src/index.js +3 -4
  37. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  38. package/packages/datadog-plugin-playwright/src/index.js +11 -6
  39. package/packages/datadog-plugin-undici/src/index.js +5 -0
  40. package/packages/datadog-plugin-vitest/src/index.js +3 -4
  41. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  42. package/packages/datadog-shimmer/src/shimmer.js +14 -10
  43. package/packages/datadog-turbopack/index.js +4 -1
  44. package/packages/datadog-turbopack/src/loader.js +6 -2
  45. package/packages/dd-trace/src/agent/info.js +5 -4
  46. package/packages/dd-trace/src/aiguard/client.js +38 -19
  47. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  48. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  49. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  50. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  51. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  52. package/packages/dd-trace/src/config/generated-config-types.d.ts +19 -19
  53. package/packages/dd-trace/src/config/index.js +7 -6
  54. package/packages/dd-trace/src/config/supported-configurations.json +16 -5
  55. package/packages/dd-trace/src/constants.js +2 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  57. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  58. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
  59. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  60. package/packages/dd-trace/src/debugger/index.js +1 -1
  61. package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
  62. package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
  63. package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
  64. package/packages/dd-trace/src/evp_proxy/path.js +21 -7
  65. package/packages/dd-trace/src/exporters/common/url.js +33 -2
  66. package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
  67. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  68. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  69. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  70. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  71. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  72. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  73. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  74. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  75. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  76. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  77. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  78. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  79. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
  80. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  81. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  82. package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
  83. package/packages/dd-trace/src/llmobs/prompts/prompt.js +79 -12
  84. package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
  85. package/packages/dd-trace/src/llmobs/tagger.js +11 -40
  86. package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
  87. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
  88. package/packages/dd-trace/src/openfeature/index.js +6 -2
  89. package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
  90. package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
  91. package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
  92. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  93. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  94. package/packages/dd-trace/src/plugin_manager.js +27 -8
  95. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
  96. package/packages/dd-trace/src/plugins/util/test.js +13 -4
  97. package/packages/dd-trace/src/profiler.js +51 -5
  98. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  99. package/packages/dd-trace/src/proxy.js +2 -2
  100. package/packages/dd-trace/src/ritm.js +12 -9
  101. package/packages/dd-trace/src/span_processor.js +6 -1
  102. package/packages/dd-trace/src/telemetry/send-data.js +2 -1
  103. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  104. package/packages/dd-trace/src/tracer.js +5 -4
  105. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  106. package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
  107. package/packages/datadog-instrumentations/src/bullmq.js +0 -11
  108. package/packages/datadog-instrumentations/src/langchain.js +0 -7
  109. package/packages/datadog-instrumentations/src/langgraph.js +0 -7
  110. package/packages/datadog-instrumentations/src/mercurius.js +0 -11
@@ -77,6 +77,7 @@ function promptFromData (data, source) {
77
77
  version: String(version),
78
78
  source,
79
79
  template: template || data.chat_template || [],
80
+ config: data.config,
80
81
  promptUuid: data.prompt_uuid,
81
82
  promptVersionUuid: data.prompt_version_uuid || data.id || data.ID,
82
83
  })
@@ -488,6 +489,9 @@ class PromptManager {
488
489
  if (requireAppKey && !this.config.DD_APP_KEY) {
489
490
  throw new PromptAPIError(0, 'DD_APP_KEY is required for prompt write operations', ERROR_NAMES.AUTH)
490
491
  }
492
+ if (body?.config !== undefined && !isPlainObject(body.config)) {
493
+ throw new PromptAPIError(0, 'config must be a JSON object', ERROR_NAMES.VALIDATION)
494
+ }
491
495
 
492
496
  const headers = {
493
497
  'Content-Type': 'application/json',
@@ -523,7 +527,8 @@ class PromptManager {
523
527
  * Create a text or chat prompt and its first version.
524
528
  * @param {string} promptId
525
529
  * @param {string | Array<{role: string, content: string}>} template
526
- * @param {{title?: string, description?: string, userVersion?: string, envIds?: string[]}} [options]
530
+ * @param {{title?: string, description?: string, userVersion?: string, envIds?: string[],
531
+ * config?: Record<string, unknown>}} [options]
527
532
  * @returns {Promise<object | object[]>}
528
533
  */
529
534
  async createPrompt (promptId, template, options = {}) {
@@ -532,6 +537,7 @@ class PromptManager {
532
537
  if (options.description) body.description = options.description
533
538
  if (options.userVersion) body.user_version = options.userVersion
534
539
  if (options.envIds !== undefined) body.env_ids = options.envIds
540
+ if (options.config !== undefined) body.config = options.config
535
541
  const response = await this.#request('POST', PROMPTS_PATH, body, true)
536
542
  this.#evictPrompt(promptId)
537
543
  return response
@@ -541,7 +547,8 @@ class PromptManager {
541
547
  * Add a text or chat version to an existing prompt.
542
548
  * @param {string} promptId
543
549
  * @param {string | Array<{role: string, content: string}>} template
544
- * @param {{description?: string, userVersion?: string, envIds?: string[]}} [options]
550
+ * @param {{description?: string, userVersion?: string, envIds?: string[],
551
+ * config?: Record<string, unknown>}} [options]
545
552
  * @returns {Promise<object | object[]>}
546
553
  */
547
554
  async createPromptVersion (promptId, template, options = {}) {
@@ -549,6 +556,7 @@ class PromptManager {
549
556
  if (options.description) body.description = options.description
550
557
  if (options.userVersion) body.user_version = options.userVersion
551
558
  if (options.envIds !== undefined) body.env_ids = options.envIds
559
+ if (options.config !== undefined) body.config = options.config
552
560
  const path = `${PROMPTS_PATH}/${encodeURIComponent(promptId)}/versions`
553
561
  const response = await this.#request('POST', path, body, true)
554
562
  this.#evictPrompt(promptId)
@@ -1,6 +1,43 @@
1
1
  'use strict'
2
2
 
3
- const VARIABLE_PATTERN = /(?<!\{)(?:\{\{\s*(\w+)\s*\}\}(?!\})|\{\s*(\w+)\s*\}(?!\}))/g
3
+ /** @typedef {import('../../../../../index').llmobs.FormattedPromptMessage} FormattedPromptMessage */
4
+
5
+ // Match double braces first; preserve surrounding braces such as the closing object in {"age": {age}}.
6
+ const VARIABLE_PATTERN = /\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g
7
+
8
+ function isMessage (value) {
9
+ return typeof value?.role === 'string' &&
10
+ value?.type !== 'placeholder' &&
11
+ (value?.content == null || typeof value.content === 'string') &&
12
+ (typeof value?.content === 'string' ||
13
+ (Array.isArray(value?.tool_calls) && value.tool_calls.length > 0) ||
14
+ (Array.isArray(value?.tool_results) && value.tool_results.length > 0))
15
+ }
16
+
17
+ function isPlaceholder (value) {
18
+ return value?.type === 'placeholder' && typeof value?.name === 'string'
19
+ }
20
+
21
+ function isObject (value) {
22
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
23
+ }
24
+
25
+ function isTool (value) {
26
+ return isObject(value) &&
27
+ (value.id == null || typeof value.id === 'string') &&
28
+ (value.type == null || typeof value.type === 'string') &&
29
+ (value.tool_id == null || typeof value.tool_id === 'string') &&
30
+ (value.name == null || typeof value.name === 'string')
31
+ }
32
+
33
+ function hasValidTools (message) {
34
+ return (message.tool_call_id == null || typeof message.tool_call_id === 'string') &&
35
+ (message.tool_calls == null || (Array.isArray(message.tool_calls) && message.tool_calls.every(call =>
36
+ isTool(call) && (call.function == null ||
37
+ (isObject(call.function) &&
38
+ typeof call.function.name === 'string' && typeof call.function.arguments === 'string'))))) &&
39
+ (message.tool_results == null || (Array.isArray(message.tool_results) && message.tool_results.every(isTool)))
40
+ }
4
41
 
5
42
  function render (template, variables) {
6
43
  return template.replaceAll(VARIABLE_PATTERN, (match, doubleName, singleName) => {
@@ -9,23 +46,40 @@ function render (template, variables) {
9
46
  })
10
47
  }
11
48
 
49
+ function freezeConfig (config) {
50
+ if (config === undefined) return Object.freeze({})
51
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
52
+ throw new TypeError('Invalid prompt config: expected a JSON object')
53
+ }
54
+ const copy = structuredClone(config)
55
+ const freeze = value => {
56
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
57
+ for (const nested of Object.values(value)) freeze(nested)
58
+ return Object.freeze(value)
59
+ }
60
+ return freeze(copy)
61
+ }
12
62
  class ManagedPrompt {
13
63
  /**
14
64
  * @param {object} data
15
65
  * @param {string} data.id
16
66
  * @param {string} data.version
17
67
  * @param {'registry'|'cache'|'fallback'|'ff'|'resolve'} data.source
18
- * @param {string | Array<{role: string, content: string}>} data.template
68
+ * @param {string | Array<{role: string, content: string} | {type: 'placeholder', name: string}>} data.template
69
+ * @param {Record<string, unknown>} [data.config]
19
70
  * @param {string} [data.promptUuid]
20
71
  * @param {string} [data.promptVersionUuid]
21
72
  */
22
- constructor ({ id, version, source, template, promptUuid, promptVersionUuid }) {
73
+ constructor ({ id, version, source, template, config, promptUuid, promptVersionUuid }) {
23
74
  this.id = id
24
75
  this.version = version
25
76
  this.source = source
26
77
  this.template = Array.isArray(template)
27
- ? Object.freeze(template.map(message => Object.freeze({ role: message.role, content: message.content })))
78
+ ? Object.freeze(template.map(item => Object.freeze(isPlaceholder(item)
79
+ ? { type: 'placeholder', name: item.name }
80
+ : { role: item.role, content: item.content })))
28
81
  : template
82
+ this.config = freezeConfig(config)
29
83
  this.promptUuid = promptUuid
30
84
  this.promptVersionUuid = promptVersionUuid
31
85
  Object.freeze(this)
@@ -34,11 +88,21 @@ class ManagedPrompt {
34
88
  /**
35
89
  * Render the prompt without changing its stored template.
36
90
  * @param {Record<string, unknown>} [variables]
37
- * @returns {string | Array<{role: string, content: string}>}
91
+ * @returns {string | FormattedPromptMessage[]}
38
92
  */
39
93
  format (variables = {}) {
40
94
  if (typeof this.template === 'string') return render(this.template, variables)
41
- return this.template.map(message => ({ role: message.role, content: render(message.content, variables) }))
95
+ return this.template.flatMap(item => {
96
+ if (!isPlaceholder(item)) return [{ ...item, content: render(item.content, variables) }]
97
+ if (!Object.hasOwn(variables, item.name)) {
98
+ throw new TypeError(`Missing message placeholder variable '${item.name}'`)
99
+ }
100
+ const messages = variables[item.name]
101
+ if (!Array.isArray(messages) || !messages.every(message => isMessage(message) && hasValidTools(message))) {
102
+ throw new TypeError(`Invalid message placeholder variable '${item.name}': expected an array of messages`)
103
+ }
104
+ return structuredClone(messages)
105
+ })
42
106
  }
43
107
 
44
108
  /**
@@ -52,7 +116,10 @@ class ManagedPrompt {
52
116
  version: this.version,
53
117
  template: typeof this.template === 'string' ? this.template : this.template.map(message => ({ ...message })),
54
118
  }
55
- const entries = Object.entries(variables ?? {})
119
+ const placeholderNames = new Set(Array.isArray(this.template)
120
+ ? this.template.filter(isPlaceholder).map(item => item.name)
121
+ : [])
122
+ const entries = Object.entries(variables ?? {}).filter(([name]) => !placeholderNames.has(name))
56
123
  if (entries.length) {
57
124
  annotation.variables = Object.fromEntries(entries.map(([name, value]) => [name, String(value)]))
58
125
  }
@@ -64,8 +131,8 @@ class ManagedPrompt {
64
131
  /**
65
132
  * Convert a caller fallback to a managed prompt.
66
133
  * @param {string} promptId
67
- * @param {string | object | Array<{role: string, content: string}> |
68
- * (() => string | object | Array<{role: string, content: string}>)} fallback
134
+ * @param {string | object | Array<{role: string, content: string} | {type: 'placeholder', name: string}> |
135
+ * (() => string | object | Array<{role: string, content: string} | {type: 'placeholder', name: string}>)} fallback
69
136
  * @returns {ManagedPrompt}
70
137
  */
71
138
  static fromFallback (promptId, fallback) {
@@ -73,9 +140,8 @@ class ManagedPrompt {
73
140
  const promptLike = value && !Array.isArray(value) && typeof value === 'object'
74
141
  const template = promptLike ? value.template : value
75
142
  const validTemplate = typeof template === 'string' || (
76
- Array.isArray(template) && template.every(message => {
77
- return message && typeof message.role === 'string' && typeof message.content === 'string'
78
- })
143
+ Array.isArray(template) && template.every(item =>
144
+ isPlaceholder(item) || (isMessage(item) && typeof item.content === 'string'))
79
145
  )
80
146
  if (!validTemplate) {
81
147
  throw new TypeError('Invalid prompt fallback: expected a string, chat message array, or object with a template')
@@ -85,6 +151,7 @@ class ManagedPrompt {
85
151
  version: String(promptLike && value.version ? value.version : 'fallback'),
86
152
  source: 'fallback',
87
153
  template,
154
+ config: promptLike ? value.config : undefined,
88
155
  })
89
156
  }
90
157
  }
@@ -38,41 +38,13 @@ const {
38
38
  SAMPLE_RATE,
39
39
  SAMPLING_DECISION,
40
40
  TRACE_ID,
41
- INPUT_TOKENS_METRIC_KEY,
42
- OUTPUT_TOKENS_METRIC_KEY,
43
- TOTAL_TOKENS_METRIC_KEY,
44
- CACHE_READ_INPUT_TOKENS_METRIC_KEY,
45
- CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
46
- REASONING_OUTPUT_TOKENS_METRIC_KEY,
47
- GEN_AI_OPERATION_NAME,
48
- GEN_AI_REQUEST_MODEL,
49
- GEN_AI_PROVIDER_NAME,
50
- GEN_AI_APPLICATION_NAME,
51
- GEN_AI_CONVERSATION_ID,
52
- GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
53
- GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
54
- GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
55
- GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
56
- GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
57
- GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
58
- ARTIFICIAL_GEN_AI_TAGS,
41
+ DEFAULT_MODEL,
59
42
  } = require('./constants/tags')
60
43
  const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
44
+ const { setGenAiApmTags } = require('./gen-ai-tags')
61
45
  const telemetry = require('./telemetry')
62
46
  const LLMObsTagger = require('./tagger')
63
47
 
64
- const DEFAULT_MODEL = 'custom'
65
- const MODEL_BACKED_SPAN_KINDS = new Set(['llm', 'embedding'])
66
-
67
- const GEN_AI_TOKEN_METRIC_KEYS = [
68
- [INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY],
69
- [OUTPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY],
70
- [TOTAL_TOKENS_METRIC_KEY, GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY],
71
- [CACHE_READ_INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY],
72
- [CACHE_WRITE_INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY],
73
- [REASONING_OUTPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY],
74
- ]
75
-
76
48
  class LLMObservabilitySpan {
77
49
  /**
78
50
  * @param {string} kind span kind
@@ -320,46 +292,19 @@ class LLMObsSpanProcessor {
320
292
  }
321
293
 
322
294
  /**
323
- * Writes the scalar `gen_ai.*` attributes onto the APM span, so model, provider, application,
324
- * conversation and token usage are searchable in APM. Message bodies stay off the APM span.
325
- *
326
295
  * @param {import('../opentracing/span')} span
327
296
  */
328
297
  #setGenAiApmTags (span) {
329
298
  const mlObsTags = LLMObsTagger.tagMap.get(span)
330
- const spanContext = span.context()
331
- const spanKind = mlObsTags[SPAN_KIND]
332
-
333
- if (spanKind) spanContext.setTag(GEN_AI_OPERATION_NAME, spanKind)
334
-
335
- const modelName = mlObsTags[MODEL_NAME]
336
- const modelProvider = mlObsTags[MODEL_PROVIDER]
337
- const modelBacked = MODEL_BACKED_SPAN_KINDS.has(spanKind)
338
- if (modelBacked) {
339
- spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName || DEFAULT_MODEL)
340
- spanContext.setTag(GEN_AI_PROVIDER_NAME, (modelProvider || DEFAULT_MODEL).toLowerCase())
341
- } else {
342
- if (modelName) spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName)
343
- if (modelProvider) spanContext.setTag(GEN_AI_PROVIDER_NAME, modelProvider.toLowerCase())
344
- }
345
-
346
- const mlApp = mlObsTags[ML_APP]
347
- if (mlApp) spanContext.setTag(GEN_AI_APPLICATION_NAME, mlApp)
348
-
349
- const sessionId = mlObsTags[SESSION_ID]
350
- if (sessionId) spanContext.setTag(GEN_AI_CONVERSATION_ID, sessionId)
351
-
352
- const metrics = mlObsTags[METRICS]
353
- // Other kinds carry unrelated metrics that would be misleading under a `gen_ai.usage.*` key.
354
- if (modelBacked && metrics) {
355
- for (const [metricKey, genAiKey] of GEN_AI_TOKEN_METRIC_KEYS) {
356
- const value = metrics[metricKey]
357
- if (value != null) spanContext.setTag(genAiKey, value)
358
- }
359
- }
360
299
 
361
- // matches the value dd-trace-py writes
362
- spanContext.setTag(ARTIFICIAL_GEN_AI_TAGS, 'true')
300
+ setGenAiApmTags(span, {
301
+ spanKind: mlObsTags[SPAN_KIND],
302
+ modelName: mlObsTags[MODEL_NAME],
303
+ modelProvider: mlObsTags[MODEL_PROVIDER],
304
+ mlApp: mlObsTags[ML_APP],
305
+ sessionId: mlObsTags[SESSION_ID],
306
+ metrics: mlObsTags[METRICS],
307
+ })
363
308
  }
364
309
 
365
310
  // For now, this only applies to metadata, as we let users annotate this field with any object
@@ -29,14 +29,7 @@ const {
29
29
  PROPAGATED_PARENT_AGENT_ID_KEY,
30
30
  PROPAGATED_PARENT_AGENT_NAME_KEY,
31
31
  ROOT_PARENT_ID,
32
- CACHE_READ_INPUT_TOKENS_METRIC_KEY,
33
- CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
34
- CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY,
35
- CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY,
36
- INPUT_TOKENS_METRIC_KEY,
37
- OUTPUT_TOKENS_METRIC_KEY,
38
- TOTAL_TOKENS_METRIC_KEY,
39
- REASONING_OUTPUT_TOKENS_METRIC_KEY,
32
+ METRIC_KEY_ALIASES,
40
33
  INTEGRATION,
41
34
  DECORATOR,
42
35
  PROPAGATED_ML_APP_KEY,
@@ -307,35 +300,8 @@ class LLMObsTagger {
307
300
  tagMetrics (span, metrics) {
308
301
  const filterdMetrics = {}
309
302
  for (const [key, value] of Object.entries(metrics)) {
310
- let processedKey = key
311
-
312
303
  // processing these specifically for our metrics ingestion
313
- switch (key) {
314
- case 'inputTokens':
315
- processedKey = INPUT_TOKENS_METRIC_KEY
316
- break
317
- case 'outputTokens':
318
- processedKey = OUTPUT_TOKENS_METRIC_KEY
319
- break
320
- case 'totalTokens':
321
- processedKey = TOTAL_TOKENS_METRIC_KEY
322
- break
323
- case 'cacheReadTokens':
324
- processedKey = CACHE_READ_INPUT_TOKENS_METRIC_KEY
325
- break
326
- case 'cacheWriteTokens':
327
- processedKey = CACHE_WRITE_INPUT_TOKENS_METRIC_KEY
328
- break
329
- case 'cacheWrite5mTokens':
330
- processedKey = CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY
331
- break
332
- case 'cacheWrite1hTokens':
333
- processedKey = CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY
334
- break
335
- case 'reasoningOutputTokens':
336
- processedKey = REASONING_OUTPUT_TOKENS_METRIC_KEY
337
- break
338
- }
304
+ const processedKey = METRIC_KEY_ALIASES[key] ?? key
339
305
 
340
306
  if (typeof value === 'number') {
341
307
  filterdMetrics[processedKey] = value
@@ -505,10 +471,13 @@ class LLMObsTagger {
505
471
  }
506
472
 
507
473
  if (Array.isArray(template)) {
508
- for (const message of template) {
509
- if (typeof message !== 'object' || !message.role || !message.content) {
474
+ for (const item of template) {
475
+ const valid = item?.type === 'placeholder'
476
+ ? typeof item.name === 'string'
477
+ : typeof item?.role === 'string' && typeof item?.content === 'string'
478
+ if (!valid) {
510
479
  this.#handleFailure(
511
- 'Prompt chat template must be an array of objects with role and content properties.', 'invalid_prompt'
480
+ 'Prompt chat template must contain messages or message placeholders.', 'invalid_prompt'
512
481
  )
513
482
  return
514
483
  }
@@ -532,7 +501,9 @@ class LLMObsTagger {
532
501
  if (typeof template === 'string') {
533
502
  finalTemplate = template
534
503
  } else if (Array.isArray(template)) {
535
- finalChatTemplate = template.map(message => ({ role: message.role, content: message.content }))
504
+ finalChatTemplate = template.map(item => item.type === 'placeholder'
505
+ ? { type: 'placeholder', name: item.name }
506
+ : { role: item.role, content: item.content })
536
507
  }
537
508
 
538
509
  const validatedPrompt = {}
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { createSiteUrl } = require('../exporters/common/url')
3
4
  const log = require('../log')
4
5
 
5
6
  const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server'
@@ -60,7 +61,9 @@ function endpoint (config, configuredBaseUrl) {
60
61
  const configured = configuredBaseUrl?.trim()
61
62
 
62
63
  if (!configured) {
63
- const url = new URL(`https://ufc-server.ff-cdn.${config.site.toLowerCase()}${DEFAULT_AGENTLESS_PATH}`)
64
+ const url = createSiteUrl(config.site, 'ufc-server.ff-cdn')
65
+ if (url === undefined) throw new Error('Invalid DD_SITE for Feature Flagging agentless configuration')
66
+ url.pathname = DEFAULT_AGENTLESS_PATH
64
67
  if (config.env) url.searchParams.set('dd_env', config.env)
65
68
  return url
66
69
  }
@@ -27,12 +27,12 @@ class FlaggingProvider extends DatadogNodeServerProvider {
27
27
  constructor (tracer, config) {
28
28
  super({
29
29
  exposureChannel: channel(EXPOSURE_CHANNEL),
30
- initializationTimeoutMs: config.experimental.flaggingProvider.initializationTimeoutMs,
30
+ initializationTimeoutMs: config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS,
31
31
  })
32
32
 
33
33
  this.hooks.push(new EvalMetricsHook(config))
34
34
 
35
- if (config.experimental.flaggingProvider.spanEnrichment?.enabled) {
35
+ if (config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED) {
36
36
  this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
37
37
  // @ts-expect-error The upstream constructor always initializes its optional hooks property.
38
38
  this.hooks.push(this.#spanEnrichmentHook)
@@ -42,7 +42,7 @@ class FlaggingProvider extends DatadogNodeServerProvider {
42
42
  }
43
43
 
44
44
  log.debug('%s created with timeout: %dms', this.constructor.name,
45
- config.experimental.flaggingProvider.initializationTimeoutMs)
45
+ config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS)
46
46
 
47
47
  this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
48
48
  this.#configurationSource?.start()
@@ -3,12 +3,13 @@
3
3
  const { channel } = require('dc-polyfill')
4
4
  const log = require('../log')
5
5
  const ExposuresWriter = require('./writers/exposures')
6
- const { setExposureDeliveryStrategy } = require('./writers/util')
6
+ const { setEventDeliveryStrategy } = require('./writers/util')
7
7
 
8
8
  const exposureSubmitCh = channel('ffe:exposure:submit')
9
9
  const flushCh = channel('ffe:writers:flush')
10
10
 
11
11
  let exposuresWriter = null
12
+ let stopEventDeliveryStrategy
12
13
 
13
14
  /**
14
15
  * @private
@@ -42,7 +43,7 @@ function enable (config) {
42
43
  exposureSubmitCh.subscribe(_handleExposureSubmit)
43
44
  flushCh.subscribe(_handleFlush)
44
45
 
45
- setExposureDeliveryStrategy(config, (enabled, route) => {
46
+ stopEventDeliveryStrategy = setEventDeliveryStrategy(config, (enabled, route) => {
46
47
  if (exposuresWriter !== writer) return
47
48
 
48
49
  writer.setEnabled(enabled, route)
@@ -55,6 +56,9 @@ function enable (config) {
55
56
  function disable () {
56
57
  if (!exposuresWriter) return
57
58
 
59
+ stopEventDeliveryStrategy?.()
60
+ stopEventDeliveryStrategy = undefined
61
+
58
62
  if (exposureSubmitCh.hasSubscribers) {
59
63
  exposureSubmitCh.unsubscribe(_handleExposureSubmit)
60
64
  }