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.
- package/LICENSE-3rdparty.csv +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +166 -24
- package/package.json +7 -7
- package/packages/datadog-esbuild/index.js +9 -6
- package/packages/datadog-instrumentations/src/ai.js +26 -0
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +40 -4
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/playwright.js +26 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +19 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-openai/src/stream-helpers.js +50 -1
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -4
- package/packages/dd-trace/src/config/remote_config.js +12 -0
- package/packages/dd-trace/src/config/supported-configurations.json +26 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/debugger/constants.js +13 -4
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
- package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
- package/packages/dd-trace/src/debugger/index.js +60 -28
- package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
- package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
- package/packages/dd-trace/src/debugger/redaction.js +139 -0
- package/packages/dd-trace/src/knuth-hash.js +17 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
- package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
- package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
- package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
- package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
- package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +15 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
- package/packages/dd-trace/src/llmobs/sdk.js +19 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
- package/packages/dd-trace/src/llmobs/tagger.js +37 -36
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
- package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
- package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
- package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
- package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
- package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
- package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
- package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
- package/packages/dd-trace/src/opentracing/span.js +14 -9
- package/packages/dd-trace/src/opentracing/tracer.js +10 -3
- package/packages/dd-trace/src/otel-sampling.js +171 -0
- package/packages/dd-trace/src/plugins/index.js +0 -1
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/priority_sampler.js +155 -33
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/sampler.js +2 -5
- package/packages/dd-trace/src/sampling_rule.js +4 -8
- package/packages/dd-trace/src/span_format.js +6 -0
- package/packages/dd-trace/src/span_processor.js +32 -3
- package/packages/dd-trace/src/standalone/index.js +5 -4
- package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
- package/packages/dd-trace/src/telemetry/metrics.js +4 -3
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/postgres.js +0 -7
- 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
|
|
578
|
-
DD_TEST_FAILURE_VIDEOS_ENABLED: boolean
|
|
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
|
|
839
|
-
DD_TEST_FAILURE_VIDEOS_ENABLED: boolean
|
|
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": "
|
|
2079
|
+
"implementation": "B",
|
|
2059
2080
|
"type": "boolean",
|
|
2060
|
-
"default":
|
|
2081
|
+
"default": "true",
|
|
2061
2082
|
"namespace": "testOptimization"
|
|
2062
2083
|
}
|
|
2063
2084
|
],
|
|
2064
2085
|
"DD_TEST_FAILURE_VIDEOS_ENABLED": [
|
|
2065
2086
|
{
|
|
2066
|
-
"implementation": "
|
|
2087
|
+
"implementation": "B",
|
|
2067
2088
|
"type": "boolean",
|
|
2068
|
-
"default":
|
|
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
|
|
14
|
-
// the interval bounds the delay before
|
|
15
|
-
// it, so it is shared rather than duplicated.
|
|
16
|
-
|
|
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 {
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
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 = ${
|
|
104
|
+
return `$dd_sampled = $dd_sampler.makeSampleDecision(${probe.samplingIndex}, ${id}, ${samplingArgs}) || $dd_sampled`
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
//
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
$
|
|
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}, ${
|
|
103
|
-
$dd_sampled
|
|
115
|
+
$dd_sampled = $dd_sampler.conditionError(${probe.samplingIndex}, ${id}, $dd_error) || $dd_sampled
|
|
104
116
|
}
|
|
105
117
|
}`
|
|
106
118
|
}
|