dd-trace 6.18.0 → 6.20.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 (123) 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 +166 -24
  4. package/package.json +7 -7
  5. package/packages/datadog-esbuild/index.js +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +26 -0
  7. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  9. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +40 -4
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
  16. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  17. package/packages/datadog-instrumentations/src/jest.js +3 -10
  18. package/packages/datadog-instrumentations/src/playwright.js +26 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
  20. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  21. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  22. package/packages/datadog-plugin-cucumber/src/index.js +19 -4
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  25. package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
  26. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  27. package/packages/datadog-plugin-undici/src/index.js +5 -0
  28. package/packages/dd-trace/src/aiguard/client.js +38 -19
  29. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  30. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  31. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  32. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  33. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  34. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
  35. package/packages/dd-trace/src/config/remote_config.js +12 -0
  36. package/packages/dd-trace/src/config/supported-configurations.json +26 -5
  37. package/packages/dd-trace/src/constants.js +2 -0
  38. package/packages/dd-trace/src/debugger/constants.js +13 -4
  39. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
  41. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
  43. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
  45. package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
  47. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
  49. package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
  50. package/packages/dd-trace/src/debugger/index.js +60 -28
  51. package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
  52. package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
  53. package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
  54. package/packages/dd-trace/src/debugger/redaction.js +139 -0
  55. package/packages/dd-trace/src/knuth-hash.js +17 -0
  56. package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
  57. package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
  58. package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
  59. package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
  60. package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
  61. package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
  62. package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
  63. package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
  64. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  65. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  66. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  67. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  68. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  69. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  71. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  73. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  74. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  75. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  76. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
  77. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  78. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  79. package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
  80. package/packages/dd-trace/src/llmobs/sdk.js +19 -3
  81. package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
  82. package/packages/dd-trace/src/llmobs/tagger.js +37 -36
  83. package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
  84. package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
  85. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
  86. package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
  87. package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
  88. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
  89. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
  90. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
  91. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
  92. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
  93. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
  94. package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
  95. package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
  96. package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
  97. package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
  98. package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
  99. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  100. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
  101. package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
  102. package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
  103. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
  104. package/packages/dd-trace/src/opentracing/span.js +14 -9
  105. package/packages/dd-trace/src/opentracing/tracer.js +10 -3
  106. package/packages/dd-trace/src/otel-sampling.js +171 -0
  107. package/packages/dd-trace/src/plugins/index.js +0 -1
  108. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  109. package/packages/dd-trace/src/priority_sampler.js +155 -33
  110. package/packages/dd-trace/src/profiler.js +51 -5
  111. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  112. package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
  113. package/packages/dd-trace/src/ritm.js +12 -9
  114. package/packages/dd-trace/src/sampler.js +2 -5
  115. package/packages/dd-trace/src/sampling_rule.js +4 -8
  116. package/packages/dd-trace/src/span_format.js +6 -0
  117. package/packages/dd-trace/src/span_processor.js +32 -3
  118. package/packages/dd-trace/src/standalone/index.js +5 -4
  119. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
  120. package/packages/dd-trace/src/telemetry/metrics.js +4 -3
  121. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  122. package/packages/datadog-instrumentations/src/postgres.js +0 -7
  123. package/packages/datadog-instrumentations/src/supabase.js +0 -15
@@ -431,10 +431,74 @@ function getMessagesOutputMessages (body) {
431
431
  return convertAnthropicMessage({ role, content: body.content })
432
432
  }
433
433
 
434
+ /**
435
+ * Combines Anthropic message stream events into regular output messages.
436
+ *
437
+ * Accumulates exactly the way the SDK does: every `content_block_start` appends a block, and
438
+ * deltas address blocks by position. Keying blocks by `event.index` instead would let a repeated
439
+ * or out-of-range index hide output that the caller still receives.
440
+ *
441
+ * @param {Array<object>} events
442
+ * @returns {Array<object>}
443
+ */
444
+ function getStreamedMessagesOutputMessages (events) {
445
+ let message
446
+ let contentBlocks
447
+ // Keyed by block, not by index: distinct indices can resolve to one block, and a second
448
+ // message must not inherit partial JSON accumulated for the first.
449
+ const inputJson = new Map()
450
+
451
+ for (const event of events) {
452
+ if (!event || typeof event !== 'object') continue
453
+
454
+ if (event.type === 'message_start' && event.message && typeof event.message === 'object') {
455
+ contentBlocks = Array.isArray(event.message.content)
456
+ ? event.message.content.map(block => ({ ...block }))
457
+ : []
458
+ message = { role: event.message.role || 'assistant' }
459
+ continue
460
+ }
461
+
462
+ if (event.type === 'content_block_start' && event.content_block && typeof event.content_block === 'object') {
463
+ message ??= { role: 'assistant' }
464
+ contentBlocks ??= []
465
+ contentBlocks.push({ ...event.content_block })
466
+ continue
467
+ }
468
+
469
+ if (event.type !== 'content_block_delta' || !event.delta || typeof event.delta !== 'object') continue
470
+
471
+ const block = contentBlocks?.at(event.index ?? 0)
472
+ if (!block) continue
473
+
474
+ if (event.delta.type === 'text_delta' && block.type === 'text' && typeof event.delta.text === 'string') {
475
+ block.text = (block.text || '') + event.delta.text
476
+ } else if (event.delta.type === 'input_json_delta' && typeof event.delta.partial_json === 'string') {
477
+ inputJson.set(block, (inputJson.get(block) || '') + event.delta.partial_json)
478
+ }
479
+ }
480
+
481
+ if (!message) return []
482
+
483
+ for (const [block, json] of inputJson) {
484
+ // An empty buffer is what a no-argument tool call accumulates; the SDK keeps `{}` there.
485
+ if (!json) continue
486
+ try {
487
+ block.input = JSON.parse(json)
488
+ } catch {
489
+ block.input = json
490
+ }
491
+ }
492
+
493
+ message.content = contentBlocks
494
+ return getMessagesOutputMessages(message)
495
+ }
496
+
434
497
  module.exports = {
435
498
  convertAnthropicSystem,
436
499
  convertAnthropicBlocksToContent,
437
500
  convertAnthropicMessage,
438
501
  getMessagesInputMessages,
439
502
  getMessagesOutputMessages,
503
+ getStreamedMessagesOutputMessages,
440
504
  }
@@ -416,6 +416,7 @@ export interface GeneratedConfig {
416
416
  dynamicInstrumentation: {
417
417
  DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS: number;
418
418
  DD_DYNAMIC_INSTRUMENTATION_ENABLED: boolean;
419
+ DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS: number;
419
420
  DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE: string | undefined;
420
421
  DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS: string[];
421
422
  DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS: string[];
@@ -434,6 +435,7 @@ export interface GeneratedConfig {
434
435
  DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
435
436
  DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
436
437
  DD_FEATURE_FLAGS_ENABLED: boolean;
438
+ DD_FLAGGING_EVALUATION_COUNTS_ENABLED: boolean;
437
439
  };
438
440
  flushInterval: number;
439
441
  flushMinSpans: number;
@@ -574,8 +576,8 @@ export interface GeneratedConfig {
574
576
  DD_CODE_COVERAGE_FLAGS: string | undefined;
575
577
  DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: number | undefined;
576
578
  DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
577
- DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean | undefined;
578
- DD_TEST_FAILURE_VIDEOS_ENABLED: boolean | undefined;
579
+ DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean;
580
+ DD_TEST_FAILURE_VIDEOS_ENABLED: boolean;
579
581
  DD_TEST_FLEET_CONFIG_PATH: string | undefined;
580
582
  DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
581
583
  DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
@@ -690,6 +692,7 @@ export interface GeneratedEnvVarConfig {
690
692
  DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED: boolean;
691
693
  DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS: number;
692
694
  DD_DYNAMIC_INSTRUMENTATION_ENABLED: boolean;
695
+ DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS: number;
693
696
  DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE: string | undefined;
694
697
  DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS: string[];
695
698
  DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS: string[];
@@ -715,6 +718,7 @@ export interface GeneratedEnvVarConfig {
715
718
  DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
716
719
  DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
717
720
  DD_FEATURE_FLAGS_ENABLED: boolean;
721
+ DD_FLAGGING_EVALUATION_COUNTS_ENABLED: boolean;
718
722
  DD_GIT_BRANCH: string | undefined;
719
723
  DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
720
724
  DD_GIT_COMMIT_AUTHOR_EMAIL: string | undefined;
@@ -835,8 +839,8 @@ export interface GeneratedEnvVarConfig {
835
839
  DD_TELEMETRY_METRICS_ENABLED: boolean;
836
840
  DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: number | undefined;
837
841
  DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
838
- DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean | undefined;
839
- DD_TEST_FAILURE_VIDEOS_ENABLED: boolean | undefined;
842
+ DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean;
843
+ DD_TEST_FAILURE_VIDEOS_ENABLED: boolean;
840
844
  DD_TEST_FLEET_CONFIG_PATH: string | undefined;
841
845
  DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
842
846
  DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
@@ -148,6 +148,18 @@ function enable (rc, config, onConfigUpdated) {
148
148
  // APM_TRACING product (e.g. an org-level and a service-level config) and merges them by priority.
149
149
  rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_MULTICONFIG, true)
150
150
 
151
+ // Keep advertising the legacy per-setting capabilities while the backend and UI still use them
152
+ // to determine which settings are editable. Values are delivered through sdk_config below.
153
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLED, true)
154
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RATE, true)
155
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RULES, true)
156
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true)
157
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true)
158
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_LOGS_INJECTION, true)
159
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION, true)
160
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLE_LIVE_DEBUGGING, true)
161
+ rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLE_CODE_ORIGIN, true)
162
+
151
163
  // This tracer supports receiving the full SDK_CONFIGURATION settings map, env-var-keyed.
152
164
  rc.updateCapabilities(RemoteConfigCapabilities.SDK_CONFIGURATION, true)
153
165
 
@@ -59,7 +59,7 @@
59
59
  "type": "boolean",
60
60
  "namespace": "aiguard",
61
61
  "default": "false",
62
- "description": "Analyze streamed OpenAI and Vercel AI responses with AI Guard After Model evaluation."
62
+ "description": "Analyze streamed Anthropic, OpenAI, and Vercel AI responses with AI Guard After Model evaluation."
63
63
  }
64
64
  ],
65
65
  "DD_AI_GUARD_BLOCK": [
@@ -820,6 +820,18 @@
820
820
  "default": "false"
821
821
  }
822
822
  ],
823
+ "DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS": [
824
+ {
825
+ "implementation": "A",
826
+ "type": "int",
827
+ "namespace": "dynamicInstrumentation",
828
+ "configurationNames": [
829
+ "dynamicInstrumentation.evaluationTimeoutMs"
830
+ ],
831
+ "default": "50",
832
+ "allowed": "\\d+"
833
+ }
834
+ ],
823
835
  "DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE": [
824
836
  {
825
837
  "implementation": "B",
@@ -973,6 +985,15 @@
973
985
  "namespace": "featureFlags"
974
986
  }
975
987
  ],
988
+ "DD_FLAGGING_EVALUATION_COUNTS_ENABLED": [
989
+ {
990
+ "implementation": "A",
991
+ "type": "boolean",
992
+ "default": "true",
993
+ "description": "Enable aggregated Feature Flagging evaluation telemetry.",
994
+ "namespace": "featureFlags"
995
+ }
996
+ ],
976
997
  "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
977
998
  {
978
999
  "implementation": "B",
@@ -2055,17 +2076,17 @@
2055
2076
  ],
2056
2077
  "DD_TEST_FAILURE_SCREENSHOTS_ENABLED": [
2057
2078
  {
2058
- "implementation": "A",
2079
+ "implementation": "B",
2059
2080
  "type": "boolean",
2060
- "default": null,
2081
+ "default": "true",
2061
2082
  "namespace": "testOptimization"
2062
2083
  }
2063
2084
  ],
2064
2085
  "DD_TEST_FAILURE_VIDEOS_ENABLED": [
2065
2086
  {
2066
- "implementation": "A",
2087
+ "implementation": "B",
2067
2088
  "type": "boolean",
2068
- "default": null,
2089
+ "default": "true",
2069
2090
  "namespace": "testOptimization"
2070
2091
  }
2071
2092
  ],
@@ -22,6 +22,8 @@ module.exports = {
22
22
  SPAN_SAMPLING_RULE_RATE: '_dd.span_sampling.rule_rate',
23
23
  SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second',
24
24
  SVC_SRC_KEY: '_dd.svc_src',
25
+ SDK_OTLP_EXPORT_KEY: '_dd.sdk.otlp_export',
26
+ SDK_SEMANTICS_KEY: 'datadog.sdk.semantics',
25
27
  DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent',
26
28
  DATADOG_MINI_AGENT_PATH: '/tmp/datadog/mini_agent_ready',
27
29
  DECISION_MAKER_KEY: '_dd.p.dm',
@@ -1,5 +1,13 @@
1
1
  'use strict'
2
2
 
3
+ const WORKER_ERROR_REASON = Object.freeze({
4
+ UNEXPECTED_PAUSE_REASON: 'unexpected_pause_reason',
5
+ UNSUPPORTED_PROBE_TYPE: 'unsupported_probe_type',
6
+ UNSUPPORTED_INSERTION_POINT: 'unsupported_insertion_point',
7
+ CONFLICTING_CAPTURE_OPTIONS: 'conflicting_capture_options',
8
+ UNKNOWN_REMOTE_CONFIG_ACTION: 'unknown_remote_config_action',
9
+ })
10
+
3
11
  module.exports = {
4
12
  DEFAULT_QUEUE_MAX_BYTES: 10 * 1024 * 1024,
5
13
  DIAGNOSTICS_QUEUE_MAX_BYTES: 1024 * 1024,
@@ -10,10 +18,11 @@ module.exports = {
10
18
  DEBUGGER_INPUT_V1: '/debugger/v1/input',
11
19
  DEBUGGER_INPUT_V2: '/debugger/v2/input',
12
20
 
13
- // Guardrail counters are aggregated in shared memory and only converted into telemetry metrics at this interval, so
14
- // the interval bounds the delay before a guardrail hit becomes visible, not the cost of recording it. Tests wait on
15
- // it, so it is shared rather than duplicated.
16
- GUARDRAIL_METRICS_FLUSH_INTERVAL_MS: 10_000,
21
+ // Guardrail counters and pause durations are aggregated in shared memory and only converted into telemetry metrics
22
+ // at this interval, so the interval bounds the delay before they become visible, not the cost of recording them.
23
+ // Tests wait on it, so it is shared rather than duplicated.
24
+ METRICS_FLUSH_INTERVAL_MS: 10_000,
17
25
 
18
26
  INSPECT_SEGMENT_GLOBAL_PROPERTY: 'debuggerInspectSegment',
27
+ WORKER_ERROR_REASON,
19
28
  }
@@ -4,7 +4,13 @@ const createMutex = require('../../../../../vendor/dist/mutexify/promise')
4
4
  const mutex = createMutex()
5
5
  const { getGeneratedPosition } = require('./source-maps')
6
6
  const session = require('./session')
7
- const { compile, compileSegments, templateRequiresEvaluation } = require('./condition')
7
+ const {
8
+ compile,
9
+ compileSegments,
10
+ getRedactionError,
11
+ getSegmentRedactionErrors,
12
+ templateRequiresEvaluation,
13
+ } = require('./condition')
8
14
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
9
15
  const {
10
16
  compileBreakpointCondition,
@@ -78,6 +84,7 @@ async function addBreakpoint (probe) {
78
84
  probe.templateRequiresEvaluation = templateRequiresEvaluation(probe.segments)
79
85
  if (probe.templateRequiresEvaluation) {
80
86
  probe.template = compileSegments(probe.segments)
87
+ probe.templateRedactionErrors = getSegmentRedactionErrors(probe.segments)
81
88
  }
82
89
  delete probe.segments
83
90
 
@@ -128,6 +135,12 @@ async function addBreakpoint (probe) {
128
135
  if (probe.captureExpressions?.length > 0) {
129
136
  probe.compiledCaptureExpressions = []
130
137
  for (const captureExpr of probe.captureExpressions) {
138
+ const redactionError = getRedactionError(captureExpr.name, captureExpr.expr.json)
139
+ if (redactionError !== undefined) {
140
+ probe.compiledCaptureExpressions.push({ name: captureExpr.name, redactionError })
141
+ continue
142
+ }
143
+
131
144
  let expression
132
145
  try {
133
146
  expression = compile(captureExpr.expr.json)
@@ -3,9 +3,14 @@
3
3
  module.exports = {
4
4
  compile,
5
5
  compileSegments,
6
+ getRedactionError,
7
+ getSegmentRedactionErrors,
6
8
  templateRequiresEvaluation,
7
9
  }
8
10
 
11
+ const { REDACTED_PLACEHOLDER } = require('../redaction')
12
+ const { isRedactedIdentifier } = require('./snapshot/redaction')
13
+
9
14
  const identifierRegex = /^(@[\w$]+|[a-zA-Z_$][\w$]*)$/
10
15
 
11
16
  // The following identifiers have purposefully not been included in this list:
@@ -39,6 +44,16 @@ const reservedWords = new Set([
39
44
 
40
45
  const PRIMITIVE_TYPES = new Set(['string', 'number', 'bigint', 'boolean', 'undefined', 'symbol', 'null'])
41
46
 
47
+ // What a dynamic template segment renders as when it references a redacted identifier and is therefore not evaluated.
48
+ const REDACTED_SEGMENT = JSON.stringify(REDACTED_PLACEHOLDER)
49
+
50
+ /**
51
+ * @typedef {{ str: string, dsl?: undefined, json?: undefined }} StaticTemplateSegment
52
+ * @typedef {{ str?: undefined, dsl: string, json: object }} DynamicTemplateSegment
53
+ * @typedef {StaticTemplateSegment|DynamicTemplateSegment} TemplateSegment
54
+ * @typedef {{ expr: string, message: string }} EvaluationError
55
+ */
56
+
42
57
  function templateRequiresEvaluation (segments) {
43
58
  if (segments === undefined) return false // There should always be segments, but just in case
44
59
  for (const { str } of segments) {
@@ -47,12 +62,29 @@ function templateRequiresEvaluation (segments) {
47
62
  return false
48
63
  }
49
64
 
65
+ /**
66
+ * @param {TemplateSegment[]} segments - Template segments to compile into a JavaScript array expression.
67
+ */
50
68
  function compileSegments (segments) {
51
69
  let result = '['
52
70
  for (let i = 0; i < segments.length; i++) {
53
71
  const { str, dsl, json } = segments[i]
54
- result += str === undefined
55
- ? `(() => {
72
+ result += str === undefined ? compileSegment(dsl, json) : JSON.stringify(str)
73
+ if (i !== segments.length - 1) {
74
+ result += ','
75
+ }
76
+ }
77
+ return `${result}]`
78
+ }
79
+
80
+ /**
81
+ * @param {string} dsl - The original DSL expression.
82
+ * @param {object} json - The JSON expression AST.
83
+ */
84
+ function compileSegment (dsl, json) {
85
+ if (findRedactedIdentifier(json) !== undefined) return REDACTED_SEGMENT
86
+
87
+ return `(() => {
56
88
  try {
57
89
  const result = ${compile(json)}
58
90
  return typeof result === 'string' ? result : $dd_inspectSegment(result)
@@ -60,12 +92,83 @@ function compileSegments (segments) {
60
92
  return { expr: ${JSON.stringify(dsl)}, message: \`\${e.name}: \${e.message}\` }
61
93
  }
62
94
  })()`
63
- : JSON.stringify(str)
64
- if (i !== segments.length - 1) {
65
- result += ','
95
+ }
96
+
97
+ /**
98
+ * Get the evaluation errors for the dynamic template segments that are not evaluated because they reference a redacted
99
+ * identifier. The errors are the same every time the template is evaluated, so they are only computed once.
100
+ *
101
+ * @param {TemplateSegment[]} segments - Template segments to inspect.
102
+ * @returns {EvaluationError[]|undefined} The evaluation errors, if any segment references a redacted identifier.
103
+ */
104
+ function getSegmentRedactionErrors (segments) {
105
+ let errors
106
+ for (const { str, dsl, json } of segments) {
107
+ if (str !== undefined) continue
108
+ const error = getRedactionError(dsl, json)
109
+ if (error !== undefined) {
110
+ errors ??= []
111
+ errors.push(error)
66
112
  }
67
113
  }
68
- return `${result}]`
114
+ return errors
115
+ }
116
+
117
+ /**
118
+ * Get the evaluation error to report instead of evaluating an expression, because it reads a redacted identifier.
119
+ *
120
+ * @param {string} expr - The expression to report the error for.
121
+ * @param {object} json - The JSON expression AST.
122
+ * @returns {EvaluationError|undefined} The evaluation error, if the expression reads a redacted identifier.
123
+ */
124
+ function getRedactionError (expr, json) {
125
+ const identifier = findRedactedIdentifier(json)
126
+ if (identifier === undefined) return
127
+ return { expr, message: `Could not evaluate the expression because '${identifier}' was redacted` }
128
+ }
129
+
130
+ /**
131
+ * Find the first redacted identifier referenced by an expression AST.
132
+ *
133
+ * @param {unknown} node - The JSON expression AST node to inspect.
134
+ * @returns {string|undefined} The redacted identifier, if one is referenced.
135
+ */
136
+ function findRedactedIdentifier (node) {
137
+ if (node === null || typeof node !== 'object') return
138
+
139
+ // Expression nodes are single-key objects: the key is the operator, and the value is that operator's payload.
140
+ const [type, value] = Object.entries(node)[0]
141
+
142
+ // Root references and member/index keys are the only AST positions that name values being read.
143
+ if (type === 'ref') {
144
+ // A non-string `ref` is malformed. Leave it for `compile` to reject with a useful error.
145
+ return typeof value === 'string' && isRedactedIdentifier(value) ? value : undefined
146
+ } else if (type === 'getmember' || type === 'index') {
147
+ const key = findRedactedMemberKey(value[1])
148
+ if (key !== undefined) return key
149
+ }
150
+
151
+ // Other expression types can wrap redacted reads, e.g. substring(user.password, 0, 4), so walk their children.
152
+ if (!Array.isArray(value)) return findRedactedIdentifier(value)
153
+
154
+ let key
155
+ for (const child of value) {
156
+ key = findRedactedIdentifier(child)
157
+ if (key !== undefined) break
158
+ }
159
+ return key
160
+ }
161
+
162
+ /**
163
+ * @param {unknown} key - A member/index key expression.
164
+ * @returns {string|undefined} The redacted identifier, if the key references one.
165
+ */
166
+ function findRedactedMemberKey (key) {
167
+ if (typeof key === 'string') {
168
+ return isRedactedIdentifier(key) ? key : undefined
169
+ }
170
+
171
+ return findRedactedIdentifier(key)
69
172
  }
70
173
 
71
174
  // TODO: Consider storing some of these functions that doesn't require closure access to the current scope on `process`
@@ -27,4 +27,6 @@ function updateConfig (updates) {
27
27
  config.url = new URL(updates.url)
28
28
  config.dynamicInstrumentation.captureTimeoutNs =
29
29
  BigInt(updates.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS) * 1_000_000n
30
+ config.dynamicInstrumentation.evaluationTimeoutNs =
31
+ BigInt(updates.dynamicInstrumentation.DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS) * 1_000_000n
30
32
  }
@@ -1,11 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const { randomUUID } = require('crypto')
4
- const { parentPort, workerData: { probeSamplerBuffer } } = require('worker_threads')
4
+ const { workerData: { probeSamplerBuffer, pauseDurationBuffer } } = require('worker_threads')
5
5
  const { version } = require('../../../../../package.json')
6
6
  const processTags = require('../../process-tags')
7
- const { INSPECT_SEGMENT_GLOBAL_PROPERTY } = require('../constants')
7
+ const { INSPECT_SEGMENT_GLOBAL_PROPERTY, WORKER_ERROR_REASON } = require('../constants')
8
8
  const { EVENT_TYPE, INCOMPLETE_REASON } = require('../guardrail-metrics')
9
+ const { PauseDurationHistogram } = require('../pause-duration-histogram')
9
10
  const {
10
11
  CONDITION_ERROR_FLAG,
11
12
  MAX_SAMPLED_PROBES_PER_PAUSE,
@@ -13,7 +14,7 @@ const {
13
14
  SAMPLED_PROBE_INDEXES_START,
14
15
  SAMPLED_PROBE_OVERFLOW_INDEX,
15
16
  } = require('../probe_sampler_constants')
16
- const { getTakeConditionErrorExpression } = require('./probe_sampler')
17
+ const { getEvaluationTimedOutExpression, getTakeConditionErrorExpression } = require('./probe_sampler')
17
18
  const { breakpointToProbes, samplingIndexToProbe } = require('./state')
18
19
  const { refreshBreakpoints } = require('./breakpoints')
19
20
  const session = require('./session')
@@ -42,6 +43,7 @@ const getDDTagsExpression = `(() => {
42
43
  const threadId = config.parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${config.parentThreadId}`
43
44
  const threadName = config.parentThreadId === 0 ? 'MainThread' : `WorkerThread:${config.parentThreadId}`
44
45
  const sampledProbeIndexes = new Int32Array(probeSamplerBuffer)
46
+ const pauseDurations = new PauseDurationHistogram(pauseDurationBuffer)
45
47
 
46
48
  // WARNING: The code above the line `await session.post('Debugger.resume')` is highly optimized. Please edit with care!
47
49
  session.on('Debugger.paused', async ({ params }) => {
@@ -49,7 +51,9 @@ session.on('Debugger.paused', async ({ params }) => {
49
51
 
50
52
  if (params.reason !== 'other') {
51
53
  // This error should not be caught, and should exit the worker thread, effectively stopping the debugging session
52
- throw new Error(`Unexpected Debugger.paused reason: ${params.reason}`)
54
+ throw Object.assign(new Error(`Unexpected Debugger.paused reason: ${params.reason}`), {
55
+ reason: WORKER_ERROR_REASON.UNEXPECTED_PAUSE_REASON,
56
+ })
53
57
  }
54
58
 
55
59
  let maxReferenceDepth = 0
@@ -58,6 +62,7 @@ session.on('Debugger.paused', async ({ params }) => {
58
62
  let maxLength = 0
59
63
  let numberOfProbesWithSnapshots = 0
60
64
  let probesWithCaptureExpressions = false
65
+ let probesWithTemplates = false
61
66
  const probes = []
62
67
  // Expressions evaluated on the paused frame in one round trip, in the order of `probes`: the evaluated template for
63
68
  // probes whose template requires evaluation, and the recorded error for probes paused to report a condition error
@@ -125,6 +130,7 @@ session.on('Debugger.paused', async ({ params }) => {
125
130
  }
126
131
 
127
132
  if (probe.templateRequiresEvaluation) {
133
+ probesWithTemplates = true
128
134
  frameExpressions += `,${probe.template}`
129
135
  }
130
136
 
@@ -142,6 +148,7 @@ session.on('Debugger.paused', async ({ params }) => {
142
148
  const timestamp = Date.now()
143
149
 
144
150
  let evalResults
151
+ const evaluationStart = process.hrtime.bigint()
145
152
  const { result } = /** @type {EvaluateOnCallFrameResult} */ (
146
153
  await session.post('Debugger.evaluateOnCallFrame', {
147
154
  callFrameId: params.callFrames[0].callFrameId,
@@ -152,6 +159,10 @@ session.on('Debugger.paused', async ({ params }) => {
152
159
  includeCommandLineAPI: true,
153
160
  })
154
161
  )
162
+ // The templates of all probes at the location are evaluated together, so an exceeded budget is attributed to all of
163
+ // them. Evaluation can't be interrupted, so the budget is enforced by reporting and throttling after the fact.
164
+ const templatesTimedOut = probesWithTemplates &&
165
+ process.hrtime.bigint() - evaluationStart > config.dynamicInstrumentation.evaluationTimeoutNs
155
166
  if (result?.subtype === 'error') {
156
167
  log.error('[debugger:devtools_client] Error evaluating code on call frame: %s', result?.description)
157
168
  evalResults = []
@@ -180,7 +191,8 @@ session.on('Debugger.paused', async ({ params }) => {
180
191
  captureExpressionResults.set(probe.id, await evaluateCaptureExpressions(
181
192
  params.callFrames[0],
182
193
  probe.compiledCaptureExpressions,
183
- start + config.dynamicInstrumentation.captureTimeoutNs
194
+ start + config.dynamicInstrumentation.captureTimeoutNs,
195
+ config.dynamicInstrumentation.evaluationTimeoutNs
184
196
  ))
185
197
  }
186
198
  }
@@ -188,6 +200,21 @@ session.on('Debugger.paused', async ({ params }) => {
188
200
  await session.post('Debugger.resume')
189
201
  reportPauseDuration(start)
190
202
 
203
+ // Ideally these throttles would be installed before resuming so a tight loop could not sample the probe again before
204
+ // the request is handled. That would keep the application paused for another inspector request even when the probe
205
+ // does not immediately run again, so we deliberately resume first. If the race occurs, the probe can perform one
206
+ // additional over-budget evaluation before the throttle takes effect, which is preferable to lengthening every
207
+ // timeout pause.
208
+ for (const probe of probes) {
209
+ // Condition errors are already throttled synchronously by the sampler; preserve that state and its classification.
210
+ if (conditionErrorProbes?.has(probe)) continue
211
+ if ((probe.templateRequiresEvaluation && templatesTimedOut) ||
212
+ captureExpressionResults?.get(probe.id)?.timedOut === true) {
213
+ // TODO: Batch all timed-out probes into a single Runtime.evaluate request.
214
+ throttleProbe(probe)
215
+ }
216
+ }
217
+
191
218
  const logger = {
192
219
  // We can safely use `location.file` from the first probe in the array, since all probes hit by `hitBreakpoints`
193
220
  // must exist in the same file since the debugger can only pause the main thread in one location.
@@ -322,6 +349,25 @@ session.on('Debugger.paused', async ({ params }) => {
322
349
  message += `{${result.message}}`
323
350
  }
324
351
  }
352
+ if (probe.templateRedactionErrors !== undefined) {
353
+ if (snapshot.evaluationErrors === undefined) {
354
+ snapshot.evaluationErrors = [...probe.templateRedactionErrors]
355
+ } else {
356
+ snapshot.evaluationErrors.push(...probe.templateRedactionErrors)
357
+ }
358
+ }
359
+ }
360
+ if (templatesTimedOut) {
361
+ const error = {
362
+ expr: '',
363
+ message: 'Template evaluation exceeded its time budget of ' +
364
+ `${config.dynamicInstrumentation.evaluationTimeoutNs / 1_000_000n}ms`,
365
+ }
366
+ if (snapshot.evaluationErrors === undefined) {
367
+ snapshot.evaluationErrors = [error]
368
+ } else {
369
+ snapshot.evaluationErrors.push(error)
370
+ }
325
371
  }
326
372
  } else {
327
373
  message = probe.template
@@ -358,11 +404,24 @@ session.on('Debugger.paused', async ({ params }) => {
358
404
  */
359
405
  function reportPauseDuration (start) {
360
406
  const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000
361
- parentPort.postMessage({ type: 'thread-paused', durationMs })
407
+ pauseDurations.record(durationMs)
362
408
  log.debug('[debugger:devtools_client] Finished processing breakpoints - instrumented thread paused for: ~%d ms',
363
409
  durationMs)
364
410
  }
365
411
 
412
+ /**
413
+ * Throttle a probe in the runtime sampler because its template or capture expressions exceeded the evaluation time
414
+ * budget. The result has already been reported, so this only stops the probe from being evaluated again for a while.
415
+ *
416
+ * @param {{ id: string }} probe - The probe to throttle.
417
+ */
418
+ function throttleProbe (probe) {
419
+ log.debug('[debugger:devtools_client] Evaluation of probe %s exceeded its time budget; throttling probe', probe.id)
420
+ session.post('Runtime.evaluate', { expression: getEvaluationTimedOutExpression(probe.id) }).catch((err) => {
421
+ log.error('[debugger:devtools_client] Error throttling probe %s', probe.id, err)
422
+ })
423
+ }
424
+
366
425
  function processDD (result) {
367
426
  return result?.trace_id === undefined ? undefined : result
368
427
  }
@@ -7,6 +7,7 @@ const SAMPLER_EXPRESSION = `globalThis[Symbol.for(${JSON.stringify(DD_TRACE_SYMB
7
7
 
8
8
  module.exports = {
9
9
  compileBreakpointCondition,
10
+ getEvaluationTimedOutExpression,
10
11
  getRemoveProbeExpression,
11
12
  getTakeConditionErrorExpression,
12
13
  isSnapshotProducingProbe,
@@ -23,6 +24,16 @@ function isSnapshotProducingProbe (probe) {
23
24
  return probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined
24
25
  }
25
26
 
27
+ /**
28
+ * Build the expression that throttles a probe whose evaluation exceeded its time budget in the worker. Called by the
29
+ * devtools worker and evaluated on the debuggee.
30
+ *
31
+ * @param {string} id - The probe id.
32
+ */
33
+ function getEvaluationTimedOutExpression (id) {
34
+ return `${SAMPLER_EXPRESSION}?.evaluationTimedOut(${JSON.stringify(id)})`
35
+ }
36
+
26
37
  /**
27
38
  * Build the expression that removes a probe from runtime sampler state. Called by the devtools worker and evaluated on
28
39
  * the debuggee.
@@ -85,22 +96,23 @@ function compileBreakpointCondition (probes) {
85
96
  * }} probe - The probe to sample.
86
97
  */
87
98
  function compileProbeCondition (probe) {
88
- const sample = `$dd_sampler.makeSampleDecision(${probe.samplingIndex}, ${JSON.stringify(probe.id)}, ` +
89
- `${probe.nsBetweenSampling}n, ${isSnapshotProducingProbe(probe)})`
99
+ const id = JSON.stringify(probe.id)
100
+ const producesSnapshot = isSnapshotProducingProbe(probe)
101
+ const samplingArgs = `${probe.nsBetweenSampling}n, ${producesSnapshot}`
90
102
 
91
103
  if (probe.condition === undefined) {
92
- return `$dd_sampled = ${sample} || $dd_sampled`
104
+ return `$dd_sampled = $dd_sampler.makeSampleDecision(${probe.samplingIndex}, ${id}, ${samplingArgs}) || $dd_sampled`
93
105
  }
94
106
 
95
- // A condition that throws is reported once per throttle window and skipped at probe entry in between
96
- return `if ($dd_sampler.shouldEvaluateCondition(${JSON.stringify(probe.id)})) {
107
+ // The sampler times the condition against the evaluation budget, from `shouldEvaluateCondition` until it either
108
+ // returns or throws. A condition that throws or exceeds its budget is reported once per throttle window and skipped
109
+ // at probe entry in between.
110
+ return `if ($dd_sampler.shouldEvaluateCondition(${id}, ${producesSnapshot})) {
97
111
  try {
98
- if ((${probe.condition}) === true) {
99
- $dd_sampled = ${sample} || $dd_sampled
100
- }
112
+ $dd_sampled = $dd_sampler.conditionEvaluated(${probe.samplingIndex}, ${id}, (${probe.condition}) === true,
113
+ ${samplingArgs}) || $dd_sampled
101
114
  } catch ($dd_error) {
102
- $dd_sampled = $dd_sampler.conditionError(${probe.samplingIndex}, ${JSON.stringify(probe.id)}, $dd_error) ||
103
- $dd_sampled
115
+ $dd_sampled = $dd_sampler.conditionError(${probe.samplingIndex}, ${id}, $dd_error) || $dd_sampled
104
116
  }
105
117
  }`
106
118
  }