dd-trace 5.116.0 → 5.118.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 (98) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +3 -3
  45. package/packages/datadog-instrumentations/src/cucumber.js +11 -12
  46. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  47. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  50. package/packages/datadog-instrumentations/src/jest.js +18 -17
  51. package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
  52. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  53. package/packages/datadog-instrumentations/src/playwright.js +3 -24
  54. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
  55. package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
  56. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  57. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  58. package/packages/datadog-plugin-cucumber/src/index.js +2 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  60. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  61. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  62. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  63. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  64. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  65. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  66. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  67. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  68. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  70. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  71. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  72. package/packages/dd-trace/src/exporter.js +16 -9
  73. package/packages/dd-trace/src/llmobs/index.js +9 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  75. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  76. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  77. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  78. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  79. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  80. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  81. package/packages/dd-trace/src/llmobs/util.js +56 -3
  82. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  83. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  84. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  85. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  86. package/packages/dd-trace/src/plugins/index.js +1 -0
  87. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  88. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  89. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  90. package/packages/dd-trace/src/span_processor.js +1 -1
  91. package/packages/dd-trace/src/span_stats.js +22 -4
  92. package/ci/test-optimization-validation/command-suitability.js +0 -471
  93. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  94. package/ci/test-optimization-validation/init-probe.js +0 -246
  95. package/ci/test-optimization-validation/late-initialization.js +0 -63
  96. package/ci/test-optimization-validation/local-command.js +0 -163
  97. package/ci/test-optimization-validation/setup-runner.js +0 -97
  98. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -51,9 +51,18 @@ const {
51
51
  SAMPLING_DECISION_DROPPED,
52
52
  PROPAGATED_SAMPLE_RATE_KEY,
53
53
  PROPAGATED_SAMPLING_DECISION_KEY,
54
+ TRACE_ID,
55
+ PROPAGATED_TRACE_ID_KEY,
54
56
  } = require('./constants/tags')
55
57
  const { storage } = require('./storage')
56
- const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags, validateToolDefinitions } = require('./util')
58
+ const {
59
+ findGenAIAncestorSpanId,
60
+ validateCostTags,
61
+ writeBridgeTags,
62
+ validateToolDefinitions,
63
+ generateLlmObsTraceId,
64
+ normalizeLlmObsTraceId,
65
+ } = require('./util')
57
66
 
58
67
  // global registry of LLMObs spans
59
68
  // maps LLMObs spans to their annotations
@@ -126,12 +135,20 @@ class LLMObsTagger {
126
135
 
127
136
  this._register(span)
128
137
 
138
+ const traceTags = span.context()._trace.tags
139
+
140
+ const llmobsTraceId =
141
+ registry.get(parent)?.[TRACE_ID] ??
142
+ normalizeLlmObsTraceId(traceTags[PROPAGATED_TRACE_ID_KEY]) ??
143
+ generateLlmObsTraceId(span._startTime)
144
+ this._setTag(span, TRACE_ID, llmobsTraceId)
145
+
129
146
  // When the registering span sits below an OTel `gen_ai.*` ancestor, use
130
147
  // that ancestor as the parent_id fallback and suppress the bridge
131
148
  // parent_id tag so the indexer doesn't invert the trace.
132
149
  const genAIAncestorSpanId = findGenAIAncestorSpanId(span)
133
150
 
134
- writeBridgeTags(span, { includeParentId: genAIAncestorSpanId === null })
151
+ writeBridgeTags(span, { includeParentId: genAIAncestorSpanId === null, llmobsTraceId })
135
152
 
136
153
  this._setTag(span, ML_APP, spanMlApp)
137
154
 
@@ -141,7 +158,6 @@ class LLMObsTagger {
141
158
  if (modelName) this.tagModelName(span, modelName)
142
159
  if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
143
160
 
144
- const traceTags = span.context()._trace.tags
145
161
  sessionId = sessionId ||
146
162
  registry.get(parent)?.[SESSION_ID] ||
147
163
  traceTags[SESSION_ID_TRACE_DEFAULT_KEY] ||
@@ -500,6 +516,10 @@ class LLMObsTagger {
500
516
  this._setTag(span, MODEL_NAME, modelName)
501
517
  }
502
518
 
519
+ setName (span, name) {
520
+ this._setTag(span, NAME, name)
521
+ }
522
+
503
523
  #tagText (span, data, key) {
504
524
  if (data) {
505
525
  if (typeof data === 'string') {
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const id = require('../id')
3
4
  const log = require('../log')
4
5
  const {
5
6
  LLMOBS_PARENT_ID_BRIDGE_KEY,
@@ -7,6 +8,10 @@ const {
7
8
  SPAN_KINDS,
8
9
  } = require('./constants/tags')
9
10
 
11
+ const DECIMAL_TRACE_ID_REGEX = /^\d+$/
12
+ const HEX_TRACE_ID_REGEX = /^[0-9a-f]{32}$/i
13
+ const MAX_UINT_64 = (1n << 64n) - 1n
14
+
10
15
  // LLM I/O is overwhelmingly ASCII (English prompts and code). Walk once
11
16
  // looking for the first non-ASCII char; if there is none, hand the input
12
17
  // straight back. Otherwise pick up the slow path from the byte that needed
@@ -312,12 +317,12 @@ function safeJsonParse (value, fallback) {
312
317
  // LLMObs root and hoists the gen_ai ancestors under it, inverting the trace.
313
318
  /**
314
319
  * @param {import('../opentracing/span')} span
315
- * @param {{ includeParentId?: boolean }} [opts]
320
+ * @param {{ includeParentId?: boolean, llmobsTraceId?: string }} [opts]
316
321
  */
317
- function writeBridgeTags (span, { includeParentId = true } = {}) {
322
+ function writeBridgeTags (span, { includeParentId = true, llmobsTraceId } = {}) {
318
323
  const traceTags = span?.context?.()._trace?.tags
319
324
  if (!traceTags || traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY]) return
320
- traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = span.context().toTraceId(true)
325
+ traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = llmobsTraceId ?? span.context().toTraceId(true)
321
326
  if (includeParentId) {
322
327
  traceTags[LLMOBS_PARENT_ID_BRIDGE_KEY] = span.context().toSpanId()
323
328
  }
@@ -362,6 +367,51 @@ function findGenAIAncestorSpanId (span) {
362
367
  return null
363
368
  }
364
369
 
370
+ /**
371
+ * Generate a 128-bit LLMObs trace ID with the span start time encoded in its high bits.
372
+ * @param {number} startTime
373
+ * @returns {string}
374
+ */
375
+ function generateLlmObsTraceId (startTime) {
376
+ const identifier = id()
377
+ const traceIdHigh = Math.floor(startTime / 1000)
378
+ .toString(16)
379
+ .padStart(8, '0')
380
+ .padEnd(16, '0')
381
+
382
+ return identifier.toTraceIdHex(traceIdHigh).padStart(32, '0')
383
+ }
384
+
385
+ /**
386
+ * Convert an internally stored hexadecimal LLMObs trace ID to its distributed wire representation.
387
+ * @param {string | undefined} traceId
388
+ * @returns {string | undefined}
389
+ */
390
+ function llmObsTraceIdToWire (traceId) {
391
+ if (!traceId) return
392
+ if (!HEX_TRACE_ID_REGEX.test(traceId)) return traceId
393
+
394
+ return BigInt(`0x${traceId}`).toString(10)
395
+ }
396
+
397
+ /**
398
+ * Normalize a distributed LLMObs trace ID to the representation expected by LLMObs span events.
399
+ * @param {string | undefined} traceId
400
+ * @returns {string | undefined}
401
+ */
402
+ function normalizeLlmObsTraceId (traceId) {
403
+ if (!traceId) return
404
+
405
+ if (HEX_TRACE_ID_REGEX.test(traceId) && (traceId[0] === '0' || !DECIMAL_TRACE_ID_REGEX.test(traceId))) {
406
+ return traceId
407
+ }
408
+
409
+ if (!DECIMAL_TRACE_ID_REGEX.test(traceId)) return traceId
410
+
411
+ const identifier = BigInt(traceId)
412
+ return identifier > MAX_UINT_64 ? identifier.toString(16).padStart(32, '0') : traceId
413
+ }
414
+
365
415
  // Maps an audio `format` (e.g. "wav", "mp3") to a MIME type. Defaults to `audio/wav` when the
366
416
  // format is missing. Provider-specific overrides (e.g. OpenAI's mp3 -> audio/mpeg) are passed in
367
417
  // via `mimeTypeLookup` so this stays provider-agnostic. A non-string `format` is treated as missing
@@ -396,6 +446,9 @@ module.exports = {
396
446
  audioMimeTypeFromFormat,
397
447
  encodeUnicode,
398
448
  findGenAIAncestorSpanId,
449
+ generateLlmObsTraceId,
450
+ llmObsTraceIdToWire,
451
+ normalizeLlmObsTraceId,
399
452
  formatAudioPart,
400
453
  validateCostTags,
401
454
  validateKind,
@@ -20,7 +20,7 @@ const RETRY_JITTER = 0.2
20
20
  * @property {URL} endpoint
21
21
  * @property {number} pollIntervalMs
22
22
  * @property {number} requestTimeoutMs
23
- * @property {string} apiKey
23
+ * @property {string | undefined} apiKey
24
24
  */
25
25
 
26
26
  /**
@@ -138,7 +138,7 @@ class AgentlessConfigurationSource {
138
138
  #request (signal) {
139
139
  const headers = getClientLibraryHeaders()
140
140
  headers['Accept-Encoding'] = 'gzip'
141
- headers['DD-API-KEY'] = this.#config.apiKey
141
+ if (this.#config.apiKey) headers['DD-API-KEY'] = this.#config.apiKey
142
142
  if (this.#etag) headers['If-None-Match'] = this.#etag
143
143
 
144
144
  /**
@@ -228,10 +228,7 @@ class AgentlessConfigurationSource {
228
228
  this.#failureWarnings.add(category)
229
229
 
230
230
  if (statusCode === 401 || statusCode === 403) {
231
- log.warn(
232
- 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid',
233
- statusCode
234
- )
231
+ log.warn('Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', statusCode)
235
232
  } else if (statusCode) {
236
233
  log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, attempts)
237
234
  } else if (attempts > 1) {
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const { isLoopbackHost } = require('../exporters/common/url')
4
3
  const log = require('../log')
5
4
 
6
5
  const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server'
@@ -27,9 +26,11 @@ function create (config, applyConfiguration) {
27
26
  return
28
27
  }
29
28
 
29
+ const hasCustomEndpoint = Boolean(baseUrl?.trim())
30
+
30
31
  try {
31
- if (!config.DD_API_KEY) {
32
- throw new Error('DD_API_KEY is required for Feature Flagging agentless delivery')
32
+ if (!hasCustomEndpoint && !config.DD_API_KEY) {
33
+ throw new Error('DD_API_KEY is required for the default Datadog Feature Flagging endpoint')
33
34
  }
34
35
 
35
36
  const AgentlessConfigurationSource = require('./agentless_configuration_source')
@@ -37,7 +38,7 @@ function create (config, applyConfiguration) {
37
38
  endpoint: endpoint(config, baseUrl),
38
39
  pollIntervalMs: Math.min(pollIntervalSeconds, MAX_POLL_INTERVAL_SECONDS) * 1000,
39
40
  requestTimeoutMs: requestTimeoutSeconds * 1000,
40
- apiKey: config.DD_API_KEY,
41
+ apiKey: hasCustomEndpoint ? undefined : config.DD_API_KEY,
41
42
  }, applyConfiguration)
42
43
  } catch (error) {
43
44
  log.error('Unable to configure Feature Flagging configuration source', error)
@@ -74,10 +75,6 @@ function endpoint (config, configuredBaseUrl) {
74
75
  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
75
76
  throw new Error('Feature Flagging agentless URL must use HTTP or HTTPS')
76
77
  }
77
- if (url.protocol === 'http:' && !isLoopbackHost(url.hostname)) {
78
- throw new Error('Feature Flagging agentless URL must use HTTPS unless it targets loopback')
79
- }
80
-
81
78
  if (url.pathname === '' || url.pathname === '/') {
82
79
  url.pathname = DEFAULT_AGENTLESS_PATH
83
80
  }
@@ -3,6 +3,7 @@
3
3
  const { LogCollapsingLowestDenseDDSketch } = require('../../../../../vendor/dist/@datadog/sketches-js')
4
4
  const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
5
5
  const { getProtobufTypes } = require('../otlp/protobuf_loader')
6
+ const { GRPC_STATUS_NAMES } = require('../../constants')
6
7
 
7
8
  const NS_PER_S = 1e9
8
9
 
@@ -152,7 +153,10 @@ class OtlpStatsTransformer extends OtlpTransformerBase {
152
153
  if (aggKey.method) raw['http.request.method'] = aggKey.method
153
154
  if (aggKey.endpoint) raw['http.route'] = aggKey.endpoint
154
155
  if (aggKey.rpcStatusCode !== '') {
155
- raw['rpc.response.status_code'] = String(aggKey.rpcStatusCode).toUpperCase()
156
+ const n = Number(aggKey.rpcStatusCode)
157
+ raw['rpc.response.status_code'] = Number.isInteger(n) && n >= 0 && n < GRPC_STATUS_NAMES.length
158
+ ? GRPC_STATUS_NAMES[n]
159
+ : String(aggKey.rpcStatusCode).toUpperCase()
156
160
  }
157
161
 
158
162
  if (!this.#otelSemanticsEnabled) {
@@ -332,8 +332,11 @@ module.exports = class CiPlugin extends Plugin {
332
332
  log.error('Known tests could not be fetched. %s', err.message)
333
333
  this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, err)
334
334
  if (this.libraryConfig) {
335
- this.libraryConfig.isEarlyFlakeDetectionEnabled = false
336
- this.libraryConfig.isKnownTestsEnabled = false
335
+ this.libraryConfig = Object.freeze({
336
+ ...this.libraryConfig,
337
+ isEarlyFlakeDetectionEnabled: false,
338
+ isKnownTestsEnabled: false,
339
+ })
337
340
  }
338
341
  }
339
342
  onDone({ err, knownTests, requestErrorTags: this._getCurrentRequestErrorTags() })
@@ -353,7 +356,10 @@ module.exports = class CiPlugin extends Plugin {
353
356
  log.error('Test management tests could not be fetched. %s', err.message)
354
357
  this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, err)
355
358
  if (this.libraryConfig) {
356
- this.libraryConfig.isTestManagementEnabled = false
359
+ this.libraryConfig = Object.freeze({
360
+ ...this.libraryConfig,
361
+ isTestManagementEnabled: false,
362
+ })
357
363
  }
358
364
  }
359
365
  onDone({ err, testManagementTests, requestErrorTags: this._getCurrentRequestErrorTags() })
@@ -37,6 +37,7 @@ const plugins = {
37
37
  get '@smithy/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') },
38
38
  get '@vitest/runner' () { return require('../../../datadog-plugin-vitest/src') },
39
39
  get '@langchain/langgraph' () { return require('../../../datadog-plugin-langgraph/src') },
40
+ get '@openai/agents' () { return require('../../../datadog-plugin-openai-agents/src') },
40
41
  get aerospike () { return require('../../../datadog-plugin-aerospike/src') },
41
42
  get ai () { return require('../../../datadog-plugin-ai/src') },
42
43
  get amqp10 () { return require('../../../datadog-plugin-amqp10/src') },
@@ -125,7 +125,10 @@ class OutboundPlugin extends TracingPlugin {
125
125
  */
126
126
  tagPeerService (span) {
127
127
  if (this._tracerConfig.spanComputePeerService) {
128
- const peerData = this.getPeerService(span.context().getTags())
128
+ const tags = span.context().getTags()
129
+ if (tags[PEER_SERVICE_SOURCE_KEY] !== undefined) return
130
+
131
+ const peerData = this.getPeerService(tags)
129
132
  if (peerData !== undefined) {
130
133
  span.addTags(this.getPeerServiceRemap(peerData))
131
134
  }
@@ -59,7 +59,7 @@ class NativeSpaceProfiler {
59
59
  }
60
60
 
61
61
  profile (restart) {
62
- const profile = this.#pprof.heap.profile(undefined, this.#mapper, getThreadLabels)
62
+ const profile = this.#pprof.heap.profile(undefined, this.#mapper, getThreadLabels, 'pack')
63
63
  if (!restart) {
64
64
  this.stop()
65
65
  }
@@ -187,6 +187,7 @@ class NativeWallProfiler {
187
187
 
188
188
  this.#pprof.time.start({
189
189
  collectCpuTime: this.#cpuProfilingEnabled,
190
+ columnNumbers: 'pack',
190
191
  durationMillis: this.#flushIntervalMillis,
191
192
  intervalMicros: this.#samplingIntervalMicros,
192
193
  lineNumbers: false,
@@ -62,7 +62,7 @@ class SpanProcessor {
62
62
  active.push(span)
63
63
  } else {
64
64
  const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags)
65
- if (isFirstSpanInChunk && stampApmDisabled) {
65
+ if (stampApmDisabled) {
66
66
  formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0
67
67
  }
68
68
  isFirstSpanInChunk = false
@@ -14,6 +14,8 @@ const {
14
14
  GRPC_STATUS_CODE,
15
15
  } = require('../../../ext/tags')
16
16
  const { ORIGIN_KEY, TOP_LEVEL_KEY, SVC_SRC_KEY, GRPC_STATUS_NAMES } = require('./constants')
17
+
18
+ const GRPC_STATUS_CODE_MAP = Object.fromEntries(GRPC_STATUS_NAMES.map((name, i) => [name, String(i)]))
17
19
  const { version } = require('./pkg')
18
20
  const processTags = require('./process-tags')
19
21
 
@@ -108,10 +110,26 @@ class SpanAggKey {
108
110
  this.srvSrc = span.meta[SVC_SRC_KEY] || ''
109
111
  this.spanKind = span.meta[SPAN_KIND] || ''
110
112
  // dd gRPC plugin sets a numeric code via setTag; OTel/manual sets a string name via meta.
111
- const grpcCode = span.meta[GRPC_STATUS_CODE] ?? span.metrics?.[GRPC_STATUS_CODE]
112
- this.rpcStatusCode = typeof grpcCode === 'number'
113
- ? (GRPC_STATUS_NAMES[grpcCode] ?? String(grpcCode))
114
- : (grpcCode ?? '')
113
+ // Normalize to numeric string to match the Agent's parseGRPCStatusString convention.
114
+ // Also check OTel semantic aliases (rpc.grpc.status_code, rpc.response.status_code) as
115
+ // the OTel bridge stores attributes under their original key without remapping.
116
+ const grpcCode = span.meta[GRPC_STATUS_CODE] ?? span.metrics?.[GRPC_STATUS_CODE] ??
117
+ span.meta['rpc.grpc.status_code'] ?? span.metrics?.['rpc.grpc.status_code'] ??
118
+ span.meta['rpc.response.status_code'] ?? span.metrics?.['rpc.response.status_code']
119
+ if (typeof grpcCode === 'number') {
120
+ this.rpcStatusCode = String(grpcCode)
121
+ } else if (grpcCode) {
122
+ const upper = String(grpcCode).toUpperCase()
123
+ const numeric = GRPC_STATUS_CODE_MAP[upper]
124
+ if (numeric === undefined) {
125
+ const n = Number(grpcCode)
126
+ this.rpcStatusCode = Number.isInteger(n) && n >= 0 ? String(n) : ''
127
+ } else {
128
+ this.rpcStatusCode = numeric
129
+ }
130
+ } else {
131
+ this.rpcStatusCode = ''
132
+ }
115
133
  }
116
134
 
117
135
  toString () {