Skip to content

Commit 6080886

Browse files
authored
feat(protocol): Track transaction name changes (#1466)
Introduces `propagations` and `changes` that provides more insight into how a transaction name was changed during the lifetime of a transaction span. Since the transaction name is propagated to downstream SDKs as part of the Dynamic Sampling Context (DSC), this is a relevant quality metric for sampling traces by transaction name. All traces where propagations occur before the final transaction name is set may not be sampled consistently if there are rules matching on the transaction name.
1 parent 6802bd1 commit 6080886

File tree

6 files changed

+325
-17
lines changed

6 files changed

+325
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- Add InvalidReplayEvent outcome. ([#1455](https://github.com/getsentry/relay/pull/1455))
2727
- Add replay and replay-recording rate limiter. ([#1456](https://github.com/getsentry/relay/pull/1456))
2828
- Support profiles tagged for many transactions. ([#1444](https://github.com/getsentry/relay/pull/1444)), ([#1463](https://github.com/getsentry/relay/pull/1463)), ([#1464](https://github.com/getsentry/relay/pull/1464))
29+
- Track metrics for changes to the transaction name and DSC propagations. ([#1466](https://github.com/getsentry/relay/pull/1466))
2930

3031
**Features**:
3132

relay-general/src/protocol/event.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,26 @@ impl Event {
560560
.map(|m| m.contains_key(module_name))
561561
.unwrap_or(false)
562562
}
563+
564+
pub fn sdk_name(&self) -> &str {
565+
if let Some(client_sdk) = self.client_sdk.value() {
566+
if let Some(name) = client_sdk.name.as_str() {
567+
return name;
568+
}
569+
}
570+
571+
"unknown"
572+
}
573+
574+
pub fn sdk_version(&self) -> &str {
575+
if let Some(client_sdk) = self.client_sdk.value() {
576+
if let Some(version) = client_sdk.version.as_str() {
577+
return version;
578+
}
579+
}
580+
581+
"unknown"
582+
}
563583
}
564584

565585
#[cfg(test)]

relay-general/src/protocol/transaction.rs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::fmt;
22
use std::str::FromStr;
33

44
use crate::processor::ProcessValue;
5+
use crate::protocol::Timestamp;
56
use crate::types::{Annotated, Empty, ErrorKind, FromValue, IntoValue, SkipSerialization, Value};
67

78
/// Describes how the name of the transaction was determined.
@@ -28,6 +29,21 @@ pub enum TransactionSource {
2829
Other(String),
2930
}
3031

32+
impl TransactionSource {
33+
pub fn as_str(&self) -> &str {
34+
match self {
35+
Self::Custom => "custom",
36+
Self::Url => "url",
37+
Self::Route => "route",
38+
Self::View => "view",
39+
Self::Component => "component",
40+
Self::Task => "task",
41+
Self::Unknown => "unknown",
42+
Self::Other(ref s) => s,
43+
}
44+
}
45+
}
46+
3147
impl FromStr for TransactionSource {
3248
type Err = std::convert::Infallible;
3349

@@ -47,16 +63,7 @@ impl FromStr for TransactionSource {
4763

4864
impl fmt::Display for TransactionSource {
4965
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50-
match self {
51-
Self::Custom => write!(f, "custom"),
52-
Self::Url => write!(f, "url"),
53-
Self::Route => write!(f, "route"),
54-
Self::View => write!(f, "view"),
55-
Self::Component => write!(f, "component"),
56-
Self::Task => write!(f, "task"),
57-
Self::Unknown => write!(f, "unknown"),
58-
Self::Other(s) => write!(f, "{}", s),
59-
}
66+
write!(f, "{}", self.as_str())
6067
}
6168
}
6269

@@ -108,6 +115,21 @@ impl IntoValue for TransactionSource {
108115

109116
impl ProcessValue for TransactionSource {}
110117

118+
#[derive(Clone, Debug, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
119+
#[cfg_attr(feature = "jsonschema", derive(JsonSchema))]
120+
pub struct TransactionNameChange {
121+
/// Describes how the previous transaction name was determined.
122+
pub source: Annotated<TransactionSource>,
123+
124+
/// The number of propagations from the start of the transaction to this change.
125+
pub propagations: Annotated<u64>,
126+
127+
/// Timestamp when the transaction name was changed.
128+
///
129+
/// This adheres to the event timestamp specification.
130+
pub timestamp: Annotated<Timestamp>,
131+
}
132+
111133
/// Additional information about the name of the transaction.
112134
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
113135
#[cfg_attr(feature = "jsonschema", derive(JsonSchema))]
@@ -123,10 +145,20 @@ pub struct TransactionInfo {
123145
/// This value will only be set if the transaction name was modified during event processing.
124146
#[metastructure(max_chars = "culprit", trim_whitespace = "true")]
125147
pub original: Annotated<String>,
148+
149+
/// A list of changes prior to the final transaction name.
150+
///
151+
/// This list must be empty if the transaction name is set at the beginning of the transaction
152+
/// and never changed. There is no placeholder entry for the initial transaction name.
153+
pub changes: Annotated<Vec<Annotated<TransactionNameChange>>>,
154+
155+
/// The total number of propagations during the transaction.
156+
pub propagations: Annotated<u64>,
126157
}
127158

128159
#[cfg(test)]
129160
mod tests {
161+
use chrono::{TimeZone, Utc};
130162
use similar_asserts::assert_eq;
131163

132164
use super::*;
@@ -143,13 +175,27 @@ mod tests {
143175
#[test]
144176
fn test_transaction_info_roundtrip() {
145177
let json = r#"{
146-
"source": "url",
147-
"original": "/auth/login/john123/"
178+
"source": "route",
179+
"original": "/auth/login/john123/",
180+
"changes": [
181+
{
182+
"source": "url",
183+
"propagations": 1,
184+
"timestamp": 946684800.0
185+
}
186+
],
187+
"propagations": 2
148188
}"#;
149189

150190
let info = Annotated::new(TransactionInfo {
151-
source: Annotated::new(TransactionSource::Url),
191+
source: Annotated::new(TransactionSource::Route),
152192
original: Annotated::new("/auth/login/john123/".to_owned()),
193+
changes: Annotated::new(vec![Annotated::new(TransactionNameChange {
194+
source: Annotated::new(TransactionSource::Url),
195+
propagations: Annotated::new(1),
196+
timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()),
197+
})]),
198+
propagations: Annotated::new(2),
153199
});
154200

155201
assert_eq!(info, Annotated::from_json(json).unwrap());

relay-general/tests/snapshots/test_fixtures__event_schema.snap

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
source: relay-general/tests/test_fixtures.rs
3-
assertion_line: 110
4-
expression: event_json_schema()
3+
expression: "relay_general::protocol::event_json_schema()"
54
---
65
{
76
"$schema": "http://json-schema.org/draft-07/schema#",
@@ -2983,6 +2982,24 @@ expression: event_json_schema()
29832982
{
29842983
"type": "object",
29852984
"properties": {
2985+
"changes": {
2986+
"description": " A list of changes prior to the final transaction name.\n\n This list must be empty if the transaction name is set at the beginning of the transaction\n and never changed. There is no placeholder entry for the initial transaction name.",
2987+
"default": null,
2988+
"type": [
2989+
"array",
2990+
"null"
2991+
],
2992+
"items": {
2993+
"anyOf": [
2994+
{
2995+
"$ref": "#/definitions/TransactionNameChange"
2996+
},
2997+
{
2998+
"type": "null"
2999+
}
3000+
]
3001+
}
3002+
},
29863003
"original": {
29873004
"description": " The unmodified transaction name as obtained by the source.\n\n This value will only be set if the transaction name was modified during event processing.",
29883005
"default": null,
@@ -2991,6 +3008,16 @@ expression: event_json_schema()
29913008
"null"
29923009
]
29933010
},
3011+
"propagations": {
3012+
"description": " The total number of propagations during the transaction.",
3013+
"default": null,
3014+
"type": [
3015+
"integer",
3016+
"null"
3017+
],
3018+
"format": "uint64",
3019+
"minimum": 0.0
3020+
},
29943021
"source": {
29953022
"description": " Describes how the name of the transaction was determined.\n\n This will be used by the server to decide whether or not to scrub identifiers from the\n transaction name, or replace the entire name with a placeholder.",
29963023
"default": null,
@@ -3008,6 +3035,50 @@ expression: event_json_schema()
30083035
}
30093036
]
30103037
},
3038+
"TransactionNameChange": {
3039+
"anyOf": [
3040+
{
3041+
"type": "object",
3042+
"properties": {
3043+
"propagations": {
3044+
"description": " The number of propagations from the start of the transaction to this change.",
3045+
"default": null,
3046+
"type": [
3047+
"integer",
3048+
"null"
3049+
],
3050+
"format": "uint64",
3051+
"minimum": 0.0
3052+
},
3053+
"source": {
3054+
"description": " Describes how the previous transaction name was determined.",
3055+
"default": null,
3056+
"anyOf": [
3057+
{
3058+
"$ref": "#/definitions/TransactionSource"
3059+
},
3060+
{
3061+
"type": "null"
3062+
}
3063+
]
3064+
},
3065+
"timestamp": {
3066+
"description": " Timestamp when the transaction name was changed.\n\n This adheres to the event timestamp specification.",
3067+
"default": null,
3068+
"anyOf": [
3069+
{
3070+
"$ref": "#/definitions/Timestamp"
3071+
},
3072+
{
3073+
"type": "null"
3074+
}
3075+
]
3076+
}
3077+
},
3078+
"additionalProperties": false
3079+
}
3080+
]
3081+
},
30113082
"TransactionSource": {
30123083
"description": "Describes how the name of the transaction was determined.",
30133084
"type": "string",

relay-server/src/actors/processor.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use relay_log::LogError;
3232
use relay_metrics::{Bucket, Metric};
3333
use relay_quotas::{DataCategory, RateLimits, ReasonCode};
3434
use relay_redis::RedisPool;
35-
use relay_sampling::RuleId;
35+
use relay_sampling::{DynamicSamplingContext, RuleId};
3636
use relay_statsd::metric;
3737

3838
use crate::actors::envelopes::{EnvelopeManager, SendEnvelope, SendEnvelopeError, SubmitEnvelope};
@@ -45,7 +45,7 @@ use crate::envelope::{AttachmentType, ContentType, Envelope, Item, ItemType};
4545
use crate::metrics_extraction::sessions::{extract_session_metrics, SessionMetricsConfig};
4646
use crate::metrics_extraction::transactions::extract_transaction_metrics;
4747
use crate::service::{ServerError, REGISTRY};
48-
use crate::statsd::{RelayCounters, RelayTimers};
48+
use crate::statsd::{RelayCounters, RelayHistograms, RelayTimers};
4949
use crate::utils::{
5050
self, ChunkedFormDataAggregator, EnvelopeContext, ErrorBoundary, FormDataIter, SamplingResult,
5151
};
@@ -344,6 +344,94 @@ fn outcome_from_profile_error(err: relay_profiling::ProfileError) -> Outcome {
344344
Outcome::Invalid(discard_reason)
345345
}
346346

347+
fn track_sampling_metrics(
348+
project_state: &ProjectState,
349+
context: &DynamicSamplingContext,
350+
event: &Event,
351+
) {
352+
// We only collect this metric for the root transaction event, so ignore secondary projects.
353+
if !project_state.is_matching_key(context.public_key) {
354+
return;
355+
}
356+
357+
let transaction_info = match event.transaction_info.value() {
358+
Some(info) => info,
359+
None => return,
360+
};
361+
362+
let changes = match transaction_info.changes.value() {
363+
Some(value) => value.as_slice(),
364+
None => return,
365+
};
366+
367+
let last_change = changes.last().and_then(|a| a.value());
368+
let source = event.get_transaction_source().as_str();
369+
let platform = event.platform.as_str().unwrap_or("other");
370+
let sdk_name = event.sdk_name();
371+
let sdk_version = event.sdk_version();
372+
373+
metric!(
374+
histogram(RelayHistograms::DynamicSamplingChanges) = changes.len() as u64,
375+
source = source,
376+
platform = platform,
377+
sdk_name = sdk_name,
378+
sdk_version = sdk_version,
379+
);
380+
381+
if let Some(&total) = transaction_info.propagations.value() {
382+
// If there was no change, there were no propagations that happened with a wrong name.
383+
let change = last_change
384+
.and_then(|c| c.propagations.value())
385+
.map_or(0, |v| *v);
386+
387+
metric!(
388+
histogram(RelayHistograms::DynamicSamplingPropagationCount) = change,
389+
source = source,
390+
platform = platform,
391+
sdk_name = sdk_name,
392+
sdk_version = sdk_version,
393+
);
394+
395+
let percentage = ((change as f64) / (total as f64)).min(1.0);
396+
metric!(
397+
histogram(RelayHistograms::DynamicSamplingPropagationPercentage) = percentage,
398+
source = source,
399+
platform = platform,
400+
sdk_name = sdk_name,
401+
sdk_version = sdk_version,
402+
);
403+
}
404+
405+
if let (Some(&start), Some(&change), Some(&end)) = (
406+
event.start_timestamp.value(),
407+
last_change.and_then(|c| c.timestamp.value()),
408+
event.timestamp.value(),
409+
) {
410+
let delay_ms = (change - start).num_milliseconds();
411+
if delay_ms >= 0 {
412+
metric!(
413+
histogram(RelayHistograms::DynamicSamplingChangeDuration) = delay_ms as u64,
414+
source = source,
415+
platform = platform,
416+
sdk_name = sdk_name,
417+
sdk_version = sdk_version,
418+
);
419+
}
420+
421+
let duration_ms = (end - start).num_milliseconds() as f64;
422+
if delay_ms >= 0 && duration_ms >= 0.0 {
423+
let percentage = ((delay_ms as f64) / duration_ms).min(1.0);
424+
metric!(
425+
histogram(RelayHistograms::DynamicSamplingChangePercentage) = percentage,
426+
source = source,
427+
platform = platform,
428+
sdk_name = sdk_name,
429+
sdk_version = sdk_version,
430+
);
431+
}
432+
}
433+
}
434+
347435
/// Synchronous service for processing envelopes.
348436
pub struct EnvelopeProcessor {
349437
config: Arc<Config>,
@@ -1625,6 +1713,10 @@ impl EnvelopeProcessor {
16251713
}
16261714
);
16271715
state.transaction_metrics_extracted = true;
1716+
1717+
if let Some(context) = state.envelope.sampling_context() {
1718+
track_sampling_metrics(&state.project_state, context, event);
1719+
}
16281720
}
16291721
Ok(())
16301722
}

0 commit comments

Comments
 (0)