From 80f52db7f412687bccf0cb6c620347c6058d94b2 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Tue, 7 Apr 2026 17:19:36 +0200 Subject: [PATCH 1/4] Add missing data for exception replay --- datadog-live-debugger-ffi/src/send_data.rs | 54 +++++++++++++++++++-- datadog-live-debugger/src/debugger_defs.rs | 24 +++++++-- datadog-live-debugger/src/redacted_names.rs | 23 ++++++++- 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/datadog-live-debugger-ffi/src/send_data.rs b/datadog-live-debugger-ffi/src/send_data.rs index 17338091b5..f6a822dc3c 100644 --- a/datadog-live-debugger-ffi/src/send_data.rs +++ b/datadog-live-debugger-ffi/src/send_data.rs @@ -10,11 +10,11 @@ use crate::data::Probe; use datadog_live_debugger::debugger_defs::{ Capture as DebuggerCaptureAlias, Capture, Captures, DebuggerData, DebuggerPayload, Diagnostics, DiagnosticsError, Entry, Fields, ProbeMetadata, ProbeMetadataLocation, ProbeStatus, Snapshot, - SnapshotEvaluationError, SnapshotStackFrame, Value as DebuggerValueAlias, + SnapshotEvaluationError, SnapshotStackFrame, Throwable, Value as DebuggerValueAlias, }; use datadog_live_debugger::sender::generate_new_id; use datadog_live_debugger::{ - add_redacted_name, add_redacted_type, is_redacted_name, is_redacted_type, + add_excluded_name, add_redacted_name, add_redacted_type, is_redacted_name, is_redacted_type, }; use libdd_common_ffi::slice::AsBytes; @@ -126,7 +126,7 @@ pub extern "C" fn ddog_create_exception_snapshot<'a>( }), language: language.to_utf8_lossy(), id: id.to_utf8_lossy(), - exception_capture_id: Some(exception_id.to_utf8_lossy()), + exception_id: Some(exception_id.to_utf8_lossy()), exception_hash: Some(exception_hash.to_utf8_lossy()), frame_index: Some(frame_index), timestamp, @@ -155,6 +155,16 @@ pub extern "C" fn ddog_create_exception_snapshot<'a>( } } +/// Returns a mutable pointer to the last DebuggerPayload in the Vec. +/// Used to push stack frames to exception replay snapshots after creation. +#[no_mangle] +pub extern "C" fn ddog_vec_last_debugger_payload<'a>( + buffer: &'a mut Vec>, +) -> *mut DebuggerPayload<'a> { + #[allow(clippy::unwrap_used)] + buffer.last_mut().unwrap() +} + #[no_mangle] pub extern "C" fn ddog_create_log_probe_snapshot<'a>( probe: &'a Probe, @@ -265,6 +275,12 @@ pub unsafe extern "C" fn ddog_snapshot_add_redacted_name(name: CharSlice) { add_redacted_name(name.as_bytes()) } +#[no_mangle] +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn ddog_snapshot_add_excluded_name(name: CharSlice) { + add_excluded_name(name.as_bytes()) +} + #[no_mangle] pub extern "C" fn ddog_snapshot_redacted_type(name: CharSlice) -> bool { is_redacted_type(name.as_bytes()) @@ -276,6 +292,38 @@ pub unsafe extern "C" fn ddog_snapshot_add_redacted_type(name: CharSlice) { add_redacted_type(name.as_bytes()) } +#[no_mangle] +#[allow(improper_ctypes_definitions)] +pub extern "C" fn ddog_snapshot_set_throwable<'a, 'b: 'a>( + capture: &mut DebuggerCapture<'a>, + throwable_type: CharSlice<'b>, + message: CharSlice<'b>, +) { + capture.0.throwable = Some(Throwable { + r#type: throwable_type.to_utf8_lossy(), + message: message.to_utf8_lossy(), + stacktrace: Vec::new(), + }); +} + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +pub extern "C" fn ddog_snapshot_throwable_add_frame<'a, 'b: 'a>( + capture: &mut DebuggerCapture<'a>, + file: CharSlice<'b>, + function: CharSlice<'b>, + line: i64, +) { + if let Some(throwable) = &mut capture.0.throwable { + throwable.stacktrace.push(SnapshotStackFrame { + file_name: file.to_utf8_lossy(), + function: function.to_utf8_lossy(), + line_number: line, + column_number: None, + }); + } +} + #[no_mangle] #[allow(improper_ctypes_definitions)] // Vec has a fixed size, and we care only about that here pub extern "C" fn ddog_snapshot_add_field<'a, 'b: 'a, 'c: 'a>( diff --git a/datadog-live-debugger/src/debugger_defs.rs b/datadog-live-debugger/src/debugger_defs.rs index e6e8f668bd..be71416b34 100644 --- a/datadog-live-debugger/src/debugger_defs.rs +++ b/datadog-live-debugger/src/debugger_defs.rs @@ -1,7 +1,7 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use std::borrow::Cow; use std::collections::HashMap; @@ -11,7 +11,9 @@ pub struct DebuggerPayload<'a> { pub ddsource: Cow<'static, str>, pub timestamp: u64, pub debugger: DebuggerData<'a>, + #[serde(skip_serializing_if = "Option::is_none")] pub message: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub process_tags: Option>, } @@ -60,10 +62,11 @@ pub struct Snapshot<'a> { pub id: Cow<'a, str>, pub timestamp: u64, #[serde(skip_serializing_if = "Option::is_none")] - pub exception_capture_id: Option>, + pub exception_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub exception_hash: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_option_u32_as_string")] pub frame_index: Option, #[serde(skip_serializing_if = "Option::is_none")] pub captures: Option>, @@ -96,12 +99,20 @@ pub struct Capture<'a> { #[serde(skip_serializing_if = "HashMap::is_empty")] pub locals: Fields<'a>, #[serde(skip_serializing_if = "Option::is_none")] - pub throwable: Option>, + pub throwable: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct Entry<'a>(pub Value<'a>, pub Value<'a>); +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Throwable<'a> { + pub r#type: Cow<'a, str>, + pub message: Cow<'a, str>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub stacktrace: Vec>, +} + #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Value<'a> { @@ -159,3 +170,10 @@ pub struct DiagnosticsError<'a> { pub message: Cow<'a, str>, pub stacktrace: Option>, } + +fn serialize_option_u32_as_string(val: &Option, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&val.unwrap_or_default().to_string()) +} diff --git a/datadog-live-debugger/src/redacted_names.rs b/datadog-live-debugger/src/redacted_names.rs index 8837afff57..1c99e5fed3 100644 --- a/datadog-live-debugger/src/redacted_names.rs +++ b/datadog-live-debugger/src/redacted_names.rs @@ -16,6 +16,7 @@ static REDACTED_NAMES: LazyLock> = LazyLock::new(|| { b"apikey", b"apisecret", b"apisignature", + b"appkey", b"applicationkey", b"auth", b"authorization", @@ -101,6 +102,10 @@ static REDACTED_NAMES: LazyLock> = LazyLock::new(|| { static ADDED_REDACTED_NAMES: LazyLock>> = LazyLock::new(Vec::new); +// Exclusions take precedence over the built-in REDACTED_NAMES set. +static EXCLUDED_NAMES: LazyLock>> = LazyLock::new(HashSet::new); +static EXCLUDED_NAMES_INITIALIZED: AtomicBool = AtomicBool::new(false); + static REDACTED_TYPES: LazyLock> = LazyLock::new(HashSet::new); static ADDED_REDACTED_TYPES: LazyLock>> = LazyLock::new(Vec::new); @@ -139,6 +144,14 @@ pub unsafe fn add_redacted_name>>(name: I) { let redacted_names = &mut (*(&*REDACTED_NAMES as *const HashSet<&'static [u8]>).cast_mut()); redacted_names.insert(&added_names[added_names.len() - 1]); } +/// # Safety +/// May only be called while not running yet - concurrent access to is_excluded_name is forbidden. +pub unsafe fn add_excluded_name>>(name: I) { + assert!(!EXCLUDED_NAMES_INITIALIZED.load(Ordering::Relaxed)); + let excluded_names = &mut (*(&*EXCLUDED_NAMES as *const HashSet>).cast_mut()); + excluded_names.insert(name.into()); +} + /// # Safety /// May only be called while not running yet - concurrent access to is_redacted_type is forbidden. pub unsafe fn add_redacted_type>(name: I) { @@ -182,7 +195,15 @@ pub fn is_redacted_name>(name: I) -> bool { } i += 1; } - REDACTED_NAMES.contains(©[0..copy.len()]) + let normalized = ©[0..copy.len()]; + // Exclusions take precedence: if explicitly excluded, do not redact. + if !EXCLUDED_NAMES.is_empty() { + EXCLUDED_NAMES_INITIALIZED.store(true, Ordering::Relaxed); + if EXCLUDED_NAMES.contains(normalized as &[u8]) { + return false; + } + } + REDACTED_NAMES.contains(normalized) } pub fn is_redacted_type>(name: I) -> bool { From 9f61959693f73911b9b86d283dfab183453afe2e Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Tue, 7 Apr 2026 19:34:19 +0200 Subject: [PATCH 2/4] Add capture_expression to debugger defs Signed-off-by: Bob Weinand --- datadog-live-debugger-ffi/src/data.rs | 45 ++++++++++++++++++++-- datadog-live-debugger-ffi/src/send_data.rs | 13 +++++++ datadog-live-debugger/src/debugger_defs.rs | 3 ++ datadog-live-debugger/src/parse_json.rs | 31 +++++++++++++-- datadog-live-debugger/src/probe_defs.rs | 8 ++++ datadog-remote-config/src/parse.rs | 1 + 6 files changed, 94 insertions(+), 7 deletions(-) diff --git a/datadog-live-debugger-ffi/src/data.rs b/datadog-live-debugger-ffi/src/data.rs index 58f32900d0..e7c9d06570 100644 --- a/datadog-live-debugger-ffi/src/data.rs +++ b/datadog-live-debugger-ffi/src/data.rs @@ -3,8 +3,8 @@ use datadog_live_debugger::debugger_defs::{ProbeMetadata, ProbeMetadataLocation, ProbeStatus}; use datadog_live_debugger::{ - CaptureConfiguration, DslString, EvaluateAt, InBodyLocation, MetricKind, ProbeCondition, - ProbeValue, SpanProbeTarget, + CaptureConfiguration, CaptureExpression as RustCaptureExpression, DslString, EvaluateAt, + InBodyLocation, MetricKind, ProbeCondition, ProbeValue, SpanProbeTarget, }; use libdd_common_ffi::slice::AsBytes; use libdd_common_ffi::{CharSlice, Option}; @@ -57,6 +57,13 @@ impl<'a> From<&'a datadog_live_debugger::MetricProbe> for MetricProbe<'a> { } } +#[repr(C)] +pub struct CaptureExpression<'a> { + pub name: CharSlice<'a>, + pub expr: &'a ProbeValue, + pub capture: &'a CaptureConfiguration, +} + #[repr(C)] pub struct LogProbe<'a> { pub segments: &'a DslString, @@ -64,20 +71,52 @@ pub struct LogProbe<'a> { pub capture: &'a CaptureConfiguration, pub capture_snapshot: bool, pub sampling_snapshots_per_second: u32, + pub capture_expressions: *const CaptureExpression<'a>, + pub capture_expressions_num: usize, } impl<'a> From<&'a datadog_live_debugger::LogProbe> for LogProbe<'a> { fn from(from: &'a datadog_live_debugger::LogProbe) -> Self { - LogProbe { + let exprs: Vec> = from + .capture_expressions + .iter() + .map(|e: &'a RustCaptureExpression| CaptureExpression { + name: e.name.as_str().into(), + expr: &e.expr, + capture: &e.capture, + }) + .collect(); + let new = LogProbe { segments: &from.segments, when: &from.when, capture: &from.capture, capture_snapshot: from.capture_snapshot, sampling_snapshots_per_second: from.sampling_snapshots_per_second, + capture_expressions: exprs.as_ptr(), + capture_expressions_num: exprs.len(), + }; + std::mem::forget(exprs); + new + } +} + +impl Drop for LogProbe<'_> { + fn drop(&mut self) { + if !self.capture_expressions.is_null() { + unsafe { + Vec::from_raw_parts( + self.capture_expressions as *mut CaptureExpression, + self.capture_expressions_num, + self.capture_expressions_num, + ); + } } } } +#[no_mangle] +pub extern "C" fn drop_log_probe_capture_expressions(_: LogProbe) {} + #[repr(C)] pub struct Tag<'a> { pub name: CharSlice<'a>, diff --git a/datadog-live-debugger-ffi/src/send_data.rs b/datadog-live-debugger-ffi/src/send_data.rs index f6a822dc3c..19ee39d407 100644 --- a/datadog-live-debugger-ffi/src/send_data.rs +++ b/datadog-live-debugger-ffi/src/send_data.rs @@ -324,6 +324,19 @@ pub extern "C" fn ddog_snapshot_throwable_add_frame<'a, 'b: 'a>( } } +#[no_mangle] +#[allow(improper_ctypes_definitions)] +pub extern "C" fn ddog_snapshot_add_capture_fields<'a, 'b: 'a, 'c: 'a>( + capture: &mut DebuggerCapture<'a>, + name: CharSlice<'b>, + value: CaptureValue<'c>, +) { + capture + .0 + .capture_expressions + .insert(name.to_utf8_lossy(), value.into()); +} + #[no_mangle] #[allow(improper_ctypes_definitions)] // Vec has a fixed size, and we care only about that here pub extern "C" fn ddog_snapshot_add_field<'a, 'b: 'a, 'c: 'a>( diff --git a/datadog-live-debugger/src/debugger_defs.rs b/datadog-live-debugger/src/debugger_defs.rs index be71416b34..7e28bc66ba 100644 --- a/datadog-live-debugger/src/debugger_defs.rs +++ b/datadog-live-debugger/src/debugger_defs.rs @@ -90,6 +90,7 @@ pub struct Captures<'a> { pub type Fields<'a> = HashMap, Value<'a>>; #[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Capture<'a> { #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(rename = "staticFields")] @@ -100,6 +101,8 @@ pub struct Capture<'a> { pub locals: Fields<'a>, #[serde(skip_serializing_if = "Option::is_none")] pub throwable: Option>, + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub capture_expressions: Fields<'a>, } #[derive(Debug, Serialize, Deserialize)] diff --git a/datadog-live-debugger/src/parse_json.rs b/datadog-live-debugger/src/parse_json.rs index 832aedb914..6099c5dc85 100644 --- a/datadog-live-debugger/src/parse_json.rs +++ b/datadog-live-debugger/src/parse_json.rs @@ -6,9 +6,10 @@ use crate::expr_defs::{ Reference, StringComparison, StringSource, Value, }; use crate::{ - CaptureConfiguration, DslString, EvaluateAt, FilterList, InBodyLocation, LiveDebuggingData, - LogProbe, MetricKind, MetricProbe, Probe, ProbeCondition, ProbeTarget, ProbeType, ProbeValue, - ServiceConfiguration, SpanDecorationProbe, SpanProbe, SpanProbeDecoration, SpanProbeTarget, + CaptureConfiguration, CaptureExpression, DslString, EvaluateAt, FilterList, InBodyLocation, + LiveDebuggingData, LogProbe, MetricKind, MetricProbe, Probe, ProbeCondition, ProbeTarget, + ProbeType, ProbeValue, ServiceConfiguration, SpanDecorationProbe, SpanProbe, + SpanProbeDecoration, SpanProbeTarget, }; use anyhow::Context; use serde::Deserialize; @@ -90,6 +91,17 @@ pub fn parse(json: &str) -> anyhow::Result { .sampling .map(|s| s.snapshots_per_second) .unwrap_or(5000), + capture_expressions: { + let mut exprs = vec![]; + for raw in parsed.capture_expressions.unwrap_or_default() { + exprs.push(CaptureExpression { + name: raw.name, + expr: ProbeValue(err(raw.expr.json.try_into())?), + capture: raw.capture.unwrap_or_default(), + }); + } + exprs + }, }), ContentType::SpanProbe => ProbeType::Span(SpanProbe {}), ContentType::SpanDecorationProbe => { @@ -129,11 +141,13 @@ pub fn parse(json: &str) -> anyhow::Result { _ => unreachable!(), }, }; - // unconditional log probes always capture their entry context + // unconditional snapshot probes always capture their entry context (for arguments). + // Probes with capture_expressions only are evaluated at exit so locals are available. if matches!( probe.probe, ProbeType::Log(LogProbe { when: ProbeCondition(Condition::Always), + capture_snapshot: true, .. }) ) { @@ -176,6 +190,7 @@ struct RawTopLevelItem { deny: Option, sampling: Option, target_span: Option, + capture_expressions: Option>, } #[derive(Deserialize)] @@ -195,6 +210,13 @@ struct ProbeWhere { in_body_location: Option, } +#[derive(Deserialize)] +struct RawCaptureExpression { + name: String, + expr: Expression, + capture: Option, +} + #[derive(Deserialize)] struct Expression { json: RawExpr, @@ -786,6 +808,7 @@ mod tests { }, capture_snapshot, sampling_snapshots_per_second, + .. }), .. }) = parsed diff --git a/datadog-live-debugger/src/probe_defs.rs b/datadog-live-debugger/src/probe_defs.rs index 798ce2e57a..a5dd0561b7 100644 --- a/datadog-live-debugger/src/probe_defs.rs +++ b/datadog-live-debugger/src/probe_defs.rs @@ -73,6 +73,13 @@ pub struct SpanProbeDecoration { pub tags: Vec<(String, DslString)>, } +#[derive(Debug)] +pub struct CaptureExpression { + pub name: String, + pub expr: ProbeValue, + pub capture: CaptureConfiguration, +} + #[derive(Debug)] pub struct LogProbe { pub segments: DslString, @@ -80,6 +87,7 @@ pub struct LogProbe { pub capture: CaptureConfiguration, pub capture_snapshot: bool, pub sampling_snapshots_per_second: u32, + pub capture_expressions: Vec, } #[derive(Debug)] diff --git a/datadog-remote-config/src/parse.rs b/datadog-remote-config/src/parse.rs index 6ba43167ed..48ebf55ce9 100644 --- a/datadog-remote-config/src/parse.rs +++ b/datadog-remote-config/src/parse.rs @@ -13,6 +13,7 @@ use datadog_ffe::rules_based::UniversalFlagConfig; use datadog_live_debugger::LiveDebuggingData; #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum RemoteConfigData { DynamicConfig(DynamicConfigFile), #[cfg(feature = "live-debugger")] From d200781d7f2fbbb4407025ab130ffa826f6a97a1 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Wed, 8 Apr 2026 01:40:01 +0200 Subject: [PATCH 3/4] Allow @key and @value for debugger iteration Signed-off-by: Bob Weinand --- datadog-live-debugger-ffi/src/evaluator.rs | 21 ++--- datadog-live-debugger/src/expr_defs.rs | 6 +- datadog-live-debugger/src/expr_eval.rs | 92 ++++++++++++++++------ datadog-live-debugger/src/parse_json.rs | 13 ++- 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/datadog-live-debugger-ffi/src/evaluator.rs b/datadog-live-debugger-ffi/src/evaluator.rs index a3c690799d..2a0e9b7cbd 100644 --- a/datadog-live-debugger-ffi/src/evaluator.rs +++ b/datadog-live-debugger-ffi/src/evaluator.rs @@ -161,7 +161,7 @@ impl<'e> datadog_live_debugger::Evaluator<'e, c_void> for EvalCtx<'e> { (self.eval.length)(self.context, value) } - fn try_enumerate(&mut self, value: &'e c_void) -> ResultValue> { + fn try_enumerate(&mut self, value: &'e c_void) -> ResultValue> { let collection = (self.eval.try_enumerate)(self.context, value); if collection.count < 0 { Err(if collection.count == EVALUATOR_RESULT_REDACTED as isize { @@ -170,15 +170,18 @@ impl<'e> datadog_live_debugger::Evaluator<'e, c_void> for EvalCtx<'e> { ResultError::Invalid }) } else { - // We need to copy, Vec::from_raw_parts with only free in the allocator would be - // unstable... - let mut vec = Vec::with_capacity(collection.count as usize); + // elements are interleaved key-value pairs: [k0, v0, k1, v1, ...] + // count is the number of pairs (not the number of raw pointers) + let count = collection.count as usize; + let mut vec = Vec::with_capacity(count); unsafe { - vec.extend_from_slice(std::slice::from_raw_parts( - collection.elements as *const &c_void, - collection.count as usize, - )) - }; + let ptrs = collection.elements as *const *const c_void; + for i in 0..count { + let key = &**ptrs.add(i * 2); + let val = &**ptrs.add(i * 2 + 1); + vec.push((key, val)); + } + } (collection.free)(collection); Ok(vec) } diff --git a/datadog-live-debugger/src/expr_defs.rs b/datadog-live-debugger/src/expr_defs.rs index 248e139d89..25599681a9 100644 --- a/datadog-live-debugger/src/expr_defs.rs +++ b/datadog-live-debugger/src/expr_defs.rs @@ -23,7 +23,9 @@ impl Display for CollectionSource { #[derive(Debug)] pub enum Reference { - IteratorVariable, + IteratorVariable, // @it — current iteration value + IteratorKey, // @key — current iteration key + IteratorValue, // @value — current iteration value Base(String), Index(Box<(CollectionSource, Value)>), // i.e. foo[bar] Nested(Box<(Reference, Value)>), // i.e. foo.bar @@ -33,6 +35,8 @@ impl Display for Reference { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Reference::IteratorVariable => f.write_str("@it"), + Reference::IteratorKey => f.write_str("@key"), + Reference::IteratorValue => f.write_str("@value"), Reference::Base(s) => s.fmt(f), Reference::Index(b) => { let (source, index) = &**b; diff --git a/datadog-live-debugger/src/expr_eval.rs b/datadog-live-debugger/src/expr_eval.rs index 22f78ad7ce..9cd65b2fb7 100644 --- a/datadog-live-debugger/src/expr_eval.rs +++ b/datadog-live-debugger/src/expr_eval.rs @@ -84,7 +84,7 @@ pub trait Evaluator<'e, I> { member: IntermediateValue<'e, I>, ) -> ResultValue<&'e I>; fn length(&mut self, value: &'e I) -> usize; - fn try_enumerate(&mut self, value: &'e I) -> ResultValue>; + fn try_enumerate(&mut self, value: &'e I) -> ResultValue>; fn stringify(&mut self, value: &'e I) -> Cow<'e, str>; // generic string representation fn get_string(&mut self, value: &'e I) -> Cow<'e, str>; // log output-formatted string fn convert_index(&mut self, value: &'e I) -> ResultValue; @@ -233,6 +233,8 @@ impl<'e, I> InternalImm<'e, I> { struct Eval<'a, 'e, I, E: Evaluator<'e, I>> { eval: &'a mut E, it: Option<&'e I>, + it_key: Option<&'e I>, + it_value: Option<&'e I>, } impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { @@ -320,7 +322,10 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { }) } - fn reference_collection(&mut self, reference: &'e Reference) -> EvalResult> { + fn reference_collection( + &mut self, + reference: &'e Reference, + ) -> EvalResult> { let immediate = self.reference(reference)?.try_use(self)?; self.eval.try_enumerate(immediate).map_err(|e| match e { ResultError::Invalid => EvErr::str(format!( @@ -337,6 +342,8 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { fn reference(&mut self, reference: &'e Reference) -> EvalResult> { Ok(DefOrUndefRef(match reference { Reference::IteratorVariable => self.it.ok_or(InvalidFetch::NoIterator), + Reference::IteratorKey => self.it_key.ok_or(InvalidFetch::NoIterator), + Reference::IteratorValue => self.it_value.ok_or(InvalidFetch::NoIterator), Reference::Base(ref identifier) => self .eval .fetch_identifier(identifier.as_str()) @@ -351,9 +358,12 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { .map_err(|e| EvErr::str(format!("{} (from {dimension}", e.0)))?; let vec = self.collection_source(source)?; if index < vec.len() { - Ok(vec[index]) + Ok(vec[index].1) } else { - Err(InvalidFetch::IndexEvaluated(source, dimension, vec, index)) + let values: Vec<&'e I> = vec.into_iter().map(|(_, v)| v).collect(); + Err(InvalidFetch::IndexEvaluated( + source, dimension, values, index, + )) } } CollectionSource::Reference(ref reference) => { @@ -387,20 +397,27 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { })) } - fn collection_source(&mut self, collection: &'e CollectionSource) -> EvalResult> { + fn collection_source( + &mut self, + collection: &'e CollectionSource, + ) -> EvalResult> { Ok(match collection { CollectionSource::Reference(ref reference) => self.reference_collection(reference)?, CollectionSource::FilterOperator(ref boxed) => { let (source, condition) = &**boxed; let mut values = vec![]; - let it = self.it; - for item in self.collection_source(source)? { + let (it, it_key, it_value) = (self.it, self.it_key, self.it_value); + for (key, item) in self.collection_source(source)? { self.it = Some(item); + self.it_key = Some(key); + self.it_value = Some(item); if self.condition(condition)? { - values.push(item); + values.push((key, item)); } } self.it = it; + self.it_key = it_key; + self.it_value = it_value; values } }) @@ -459,13 +476,15 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { } Condition::CollectionMatch(match_type, reference, condition) => { let vec = self.reference_collection(reference)?; - let it = self.it; + let (it, it_key, it_value) = (self.it, self.it_key, self.it_value); let mut result; match match_type { CollectionMatch::All => { result = true; - for v in vec { + for (key, v) in vec { self.it = Some(v); + self.it_key = Some(key); + self.it_value = Some(v); if !self.condition(condition)? { result = false; break; @@ -474,8 +493,10 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { } CollectionMatch::Any => { result = false; - for v in vec { + for (key, v) in vec { self.it = Some(v); + self.it_key = Some(key); + self.it_value = Some(v); if self.condition(condition)? { result = true; break; @@ -484,6 +505,8 @@ impl<'e, I, E: Evaluator<'e, I>> Eval<'_, 'e, I, E> { } } self.it = it; + self.it_key = it_key; + self.it_value = it_value; result } Condition::IsDefinedReference(reference) => self.reference(reference)?.0.is_ok(), @@ -512,12 +535,17 @@ pub fn eval_condition<'e, I: 'e, E: Evaluator<'e, I>>( eval: &mut E, condition: &'e ProbeCondition, ) -> Result { - Eval { eval, it: None } - .condition(&condition.0) - .map_err(|e| SnapshotEvaluationError { - expr: condition.to_string(), - message: e.0, - }) + Eval { + eval, + it: None, + it_key: None, + it_value: None, + } + .condition(&condition.0) + .map_err(|e| SnapshotEvaluationError { + expr: condition.to_string(), + message: e.0, + }) } pub fn eval_string<'a, 'e, 'v, I: 'e, E: Evaluator<'e, I>>( @@ -529,7 +557,12 @@ where 'e: 'a, { let mut errors = vec![]; - let mut eval = Eval { eval, it: None }; + let mut eval = Eval { + eval, + it: None, + it_key: None, + it_value: None, + }; let mut map_error = |err: EvErr, expr: &dyn ToString| { errors.push(SnapshotEvaluationError { expr: expr.to_string(), @@ -559,7 +592,7 @@ where CollectionSource::FilterOperator(_) => { eval.collection_source(reference).map(|vec| { let mut strings = vec![]; - for referenced in vec { + for (_, referenced) in vec { strings .push(eval.get_string(IntermediateValue::Referenced(referenced))); } @@ -587,7 +620,12 @@ pub fn eval_value<'e, 'v, I: 'e, E: Evaluator<'e, I>>( where 'v: 'e, { - let mut eval = Eval { eval, it: None }; + let mut eval = Eval { + eval, + it: None, + it_key: None, + it_value: None, + }; eval.value(&value.0) .and_then(|v| v.try_use(&mut eval)) .map_err(|e| SnapshotEvaluationError { @@ -600,7 +638,12 @@ pub fn eval_intermediate_to_string<'e, I, E: Evaluator<'e, I>>( eval: &mut E, value: IntermediateValue<'e, I>, ) -> Cow<'e, str> { - let mut eval = Eval { eval, it: None }; + let mut eval = Eval { + eval, + it: None, + it_key: None, + it_value: None, + }; eval.get_string(value) } @@ -721,10 +764,11 @@ mod tests { } } - fn try_enumerate(&mut self, value: &'e Val) -> ResultValue> { + fn try_enumerate(&mut self, value: &'e Val) -> ResultValue> { match value { - Val::Vec(v) => Ok(v.iter().collect()), - Val::Obj(o) => Ok(o.0.values().collect()), + // key = value (tests don't check @key, only @it/@value) + Val::Vec(v) => Ok(v.iter().map(|item| (item, item)).collect()), + Val::Obj(o) => Ok(o.0.values().map(|v| (v, v)).collect()), _ => Err(ResultError::Invalid), } } diff --git a/datadog-live-debugger/src/parse_json.rs b/datadog-live-debugger/src/parse_json.rs index 6099c5dc85..175805ce95 100644 --- a/datadog-live-debugger/src/parse_json.rs +++ b/datadog-live-debugger/src/parse_json.rs @@ -282,13 +282,12 @@ impl TryInto for RawExpr { fn try_into(self) -> Result { Ok(match self { - RawExpr::Expr(Some(RawExprValue::Ref(identifier))) => { - if identifier == "@it" { - Reference::IteratorVariable - } else { - Reference::Base(identifier) - } - } + RawExpr::Expr(Some(RawExprValue::Ref(identifier))) => match identifier.as_str() { + "@it" => Reference::IteratorVariable, + "@key" => Reference::IteratorKey, + "@value" => Reference::IteratorValue, + _ => Reference::Base(identifier), + }, RawExpr::Expr(Some(RawExprValue::Index([source, index]))) => { Reference::Index(Box::new(((*source).try_into()?, (*index).try_into()?))) } From 5f44d26b4b32bc4e368a5b94bd20b10b87ea97f0 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Wed, 15 Apr 2026 14:18:46 +0200 Subject: [PATCH 4/4] Apply review suggestions Signed-off-by: Bob Weinand --- datadog-live-debugger-ffi/src/data.rs | 4 ++-- datadog-live-debugger-ffi/src/send_data.rs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/datadog-live-debugger-ffi/src/data.rs b/datadog-live-debugger-ffi/src/data.rs index e7c9d06570..883d0f07b9 100644 --- a/datadog-live-debugger-ffi/src/data.rs +++ b/datadog-live-debugger-ffi/src/data.rs @@ -115,7 +115,7 @@ impl Drop for LogProbe<'_> { } #[no_mangle] -pub extern "C" fn drop_log_probe_capture_expressions(_: LogProbe) {} +pub extern "C" fn ddog_drop_log_probe_capture_expressions(_: LogProbe) {} #[repr(C)] pub struct Tag<'a> { @@ -168,7 +168,7 @@ impl<'a> From<&'a datadog_live_debugger::SpanDecorationProbe> for SpanDecoration } #[no_mangle] -extern "C" fn drop_span_decoration_probe(_: SpanDecorationProbe) {} +extern "C" fn ddog_drop_span_decoration_probe(_: SpanDecorationProbe) {} impl Drop for SpanDecorationProbe<'_> { fn drop(&mut self) { diff --git a/datadog-live-debugger-ffi/src/send_data.rs b/datadog-live-debugger-ffi/src/send_data.rs index 19ee39d407..6add5b7129 100644 --- a/datadog-live-debugger-ffi/src/send_data.rs +++ b/datadog-live-debugger-ffi/src/send_data.rs @@ -160,9 +160,8 @@ pub extern "C" fn ddog_create_exception_snapshot<'a>( #[no_mangle] pub extern "C" fn ddog_vec_last_debugger_payload<'a>( buffer: &'a mut Vec>, -) -> *mut DebuggerPayload<'a> { - #[allow(clippy::unwrap_used)] - buffer.last_mut().unwrap() +) -> Option<&'a mut DebuggerPayload<'a>> { + buffer.last_mut() } #[no_mangle]