Add JSON field path extraction for params/metrics (ticket 015)

RegexToMetricPrimitiveMatcher and the standalone param declaration
(DeclareParamRequest/DataValue::ParamDeclaration) gain an optional
json_field_path, an alternative to capture_group that reads a
dot-separated field path (e.g. "foo.bar") out of lines that parse as
JSON objects. It's additive and serde-defaulted, so existing
regex-based params/metrics are unaffected. Params.tsx now offers a
"JSON field" extraction option when the message match regex looks
like a JSON object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 19:38:39 +01:00
co-authored by Claude Sonnet 5
parent e6e5bc8f50
commit b3e0272544
8 changed files with 287 additions and 54 deletions
+14
View File
@@ -60,6 +60,10 @@ pub struct ParamDeclarationRecord {
pub name: String,
pub message_match_regex: String,
pub capture_group: String,
/// See `crate::data_value::RegexToMetricPrimitiveMatcher::json_field_path`; `#[serde(default)]`
/// so profiles saved before this field existed still load as plain regex-capture-group params.
#[serde(default)]
pub json_field_path: Option<String>,
pub kind: ProfileMetricPrimitiveKind,
}
@@ -69,6 +73,7 @@ impl From<&DeclareParamRequest> for ParamDeclarationRecord {
name: request.name.clone(),
message_match_regex: request.message_match_regex.clone(),
capture_group: request.capture_group.clone(),
json_field_path: request.json_field_path.clone(),
kind: request.kind.into(),
}
}
@@ -80,6 +85,7 @@ impl From<&ParamDeclarationRecord> for DeclareParamRequest {
name: record.name.clone(),
message_match_regex: record.message_match_regex.clone(),
capture_group: record.capture_group.clone(),
json_field_path: record.json_field_path.clone(),
kind: record.kind.into(),
}
}
@@ -90,6 +96,10 @@ impl From<&ParamDeclarationRecord> for DeclareParamRequest {
pub struct MetricParamRecord {
pub param_name: String,
pub capture_group: String,
/// See `crate::data_value::RegexToMetricPrimitiveMatcher::json_field_path`; `#[serde(default)]`
/// so profiles saved before this field existed still load as plain regex-capture-group matchers.
#[serde(default)]
pub json_field_path: Option<String>,
pub kind: ProfileMetricPrimitiveKind,
/// See `ParamRole`; `#[serde(default)]` so profiles saved before this field existed still load, defaulting to `Label`.
#[serde(default)]
@@ -121,6 +131,7 @@ impl From<&DeclareMetricRequest> for MetricDeclarationRecord {
.map(|matcher| MetricParamRecord {
param_name: matcher.param_name.clone(),
capture_group: matcher.capture_group.clone(),
json_field_path: matcher.json_field_path.clone(),
kind: matcher.kind.into(),
role: matcher.role,
})
@@ -144,6 +155,7 @@ impl From<&MetricDeclarationRecord> for DeclareMetricRequest {
.map(|param| RegexToMetricPrimitiveMatcher {
param_name: param.param_name.clone(),
capture_group: param.capture_group.clone(),
json_field_path: param.json_field_path.clone(),
kind: param.kind.into(),
role: param.role,
})
@@ -626,6 +638,7 @@ mod tests {
name: name.to_string(),
message_match_regex: pattern.to_string(),
capture_group: capture_group.to_string(),
json_field_path: None,
kind: MetricPrimitiveKind::String,
}
}
@@ -639,6 +652,7 @@ mod tests {
params: vec![RegexToMetricPrimitiveMatcher {
param_name: param_name.to_string(),
capture_group: capture_group.to_string(),
json_field_path: None,
kind: MetricPrimitiveKind::Number,
role: ParamRole::Label,
}],