use std::collections::{BTreeMap, BTreeSet};
use relay_base_schema::events::EventType;
use relay_base_schema::project::ProjectId;
use relay_common::time::UnixTimestamp;
use relay_dynamic_config::{CombinedMetricExtractionConfig, TransactionMetricsConfig};
use relay_event_normalization::span::country_subregion::Subregion;
use relay_event_normalization::utils as normalize_utils;
use relay_event_schema::protocol::{
AsPair, BrowserContext, Event, OsContext, PerformanceScoreContext, TraceContext,
TransactionSource,
};
use relay_metrics::{Bucket, DurationUnit, FiniteF64};
use relay_sampling::evaluation::SamplingDecision;
use crate::metrics_extraction::generic;
use crate::metrics_extraction::transactions::types::{
CommonTag, CommonTags, ExtractMetricsError, LightTransactionTags, TransactionCPRTags,
TransactionMeasurementTags, TransactionMetric,
};
use crate::metrics_extraction::IntoMetric;
use crate::statsd::RelayCounters;
use crate::utils;
pub mod types;
const PLACEHOLDER_UNPARAMETERIZED: &str = "<< unparameterized >>";
const PERFORMANCE_SCORE_TAGS: [CommonTag; 7] = [
CommonTag::BrowserName,
CommonTag::Environment,
CommonTag::GeoCountryCode,
CommonTag::UserSubregion,
CommonTag::Release,
CommonTag::Transaction,
CommonTag::TransactionOp,
];
fn extract_http_method(transaction: &Event) -> Option<String> {
let request = transaction.request.value()?;
let method = request.method.value()?;
Some(method.clone())
}
fn extract_browser_name(event: &Event) -> Option<String> {
let browser = event.context::<BrowserContext>()?;
browser.name.value().cloned()
}
fn extract_os_name(event: &Event) -> Option<String> {
let os = event.context::<OsContext>()?;
os.name.value().cloned()
}
fn extract_geo_country_code(event: &Event) -> Option<String> {
let user = event.user.value()?;
let geo = user.geo.value()?;
geo.country_code.value().cloned()
}
fn is_low_cardinality(source: &TransactionSource) -> bool {
match source {
TransactionSource::Custom => true,
TransactionSource::Url => false,
TransactionSource::Route
| TransactionSource::View
| TransactionSource::Component
| TransactionSource::Task => true,
TransactionSource::Sanitized => true,
TransactionSource::Unknown => true,
TransactionSource::Other(_) => false,
}
}
pub fn get_transaction_name(event: &Event) -> Option<String> {
let original = event.transaction.value()?;
let source = event
.transaction_info
.value()
.and_then(|info| info.source.value());
match source {
Some(source) if is_low_cardinality(source) => Some(original.clone()),
Some(TransactionSource::Other(_)) | None => None,
Some(_) => Some(PLACEHOLDER_UNPARAMETERIZED.to_owned()),
}
}
fn track_transaction_name_stats(event: &Event) {
let name_used = match get_transaction_name(event).as_deref() {
Some(self::PLACEHOLDER_UNPARAMETERIZED) => "placeholder",
Some(_) => "original",
None => "none",
};
relay_statsd::metric!(
counter(RelayCounters::MetricsTransactionNameExtracted) += 1,
source = utils::transaction_source_tag(event),
sdk_name = event
.client_sdk
.value()
.and_then(|sdk| sdk.name.as_str())
.unwrap_or_default(),
name_used = name_used,
);
}
fn extract_light_transaction_tags(tags: &CommonTags) -> LightTransactionTags {
LightTransactionTags {
transaction_op: tags.0.get(&CommonTag::TransactionOp).cloned(),
transaction: tags.0.get(&CommonTag::Transaction).cloned(),
}
}
fn extract_universal_tags(event: &Event, config: &TransactionMetricsConfig) -> CommonTags {
let mut tags = BTreeMap::new();
if let Some(release) = event.release.as_str() {
tags.insert(CommonTag::Release, release.to_string());
}
if let Some(dist) = event.dist.value() {
tags.insert(CommonTag::Dist, dist.to_string());
}
if let Some(environment) = event.environment.as_str() {
tags.insert(CommonTag::Environment, environment.to_string());
}
if let Some(transaction_name) = get_transaction_name(event) {
tags.insert(CommonTag::Transaction, transaction_name);
}
let platform = match event.platform.as_str() {
Some(platform) if relay_event_normalization::is_valid_platform(platform) => platform,
_ => "other",
};
tags.insert(CommonTag::Platform, platform.to_string());
if let Some(trace_context) = event.context::<TraceContext>() {
if let Some(status) = trace_context.status.value() {
tags.insert(CommonTag::TransactionStatus, status.to_string());
}
if let Some(op) = normalize_utils::extract_transaction_op(trace_context) {
tags.insert(CommonTag::TransactionOp, op);
}
}
if let Some(http_method) = extract_http_method(event) {
tags.insert(CommonTag::HttpMethod, http_method);
}
if let Some(browser_name) = extract_browser_name(event) {
tags.insert(CommonTag::BrowserName, browser_name);
}
if let Some(os_name) = extract_os_name(event) {
tags.insert(CommonTag::OsName, os_name);
}
if let Some(geo_country_code) = extract_geo_country_code(event) {
tags.insert(CommonTag::GeoCountryCode, geo_country_code);
}
if let Some(_browser_name) = extract_browser_name(event) {
if let Some(geo_country_code) = extract_geo_country_code(event) {
if let Some(subregion) = Subregion::from_iso2(geo_country_code.as_str()) {
let numerical_subregion = subregion as u8;
tags.insert(CommonTag::UserSubregion, numerical_subregion.to_string());
}
}
}
if let Some(status_code) = normalize_utils::extract_http_status_code(event) {
tags.insert(CommonTag::HttpStatusCode, status_code);
}
if normalize_utils::MOBILE_SDKS.contains(&event.sdk_name()) {
if let Some(device_class) = event.tag_value("device.class") {
tags.insert(CommonTag::DeviceClass, device_class.to_owned());
}
}
let custom_tags = &config.extract_custom_tags;
if !custom_tags.is_empty() {
if let Some(event_tags) = event.tags.value() {
for tag_entry in &**event_tags {
if let Some(entry) = tag_entry.value() {
let (key, value) = entry.as_pair();
if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
if custom_tags.contains(key) {
tags.insert(CommonTag::Custom(key.to_string()), value.to_string());
}
}
}
}
}
}
CommonTags(tags)
}
#[derive(Debug, Default)]
pub struct ExtractedMetrics {
pub project_metrics: Vec<Bucket>,
pub sampling_metrics: Vec<Bucket>,
}
pub struct TransactionExtractor<'a> {
pub config: &'a TransactionMetricsConfig,
pub generic_config: Option<CombinedMetricExtractionConfig<'a>>,
pub transaction_from_dsc: Option<&'a str>,
pub sampling_decision: SamplingDecision,
pub target_project_id: ProjectId,
}
impl TransactionExtractor<'_> {
pub fn extract(&self, event: &Event) -> Result<ExtractedMetrics, ExtractMetricsError> {
let mut metrics = ExtractedMetrics::default();
if event.ty.value() != Some(&EventType::Transaction) {
return Ok(metrics);
}
let (Some(&start), Some(&end)) = (event.start_timestamp.value(), event.timestamp.value())
else {
relay_log::debug!("failed to extract the start and the end timestamps from the event");
return Err(ExtractMetricsError::MissingTimestamp);
};
let Some(timestamp) = UnixTimestamp::from_datetime(end.into_inner()) else {
relay_log::debug!("event timestamp is not a valid unix timestamp");
return Err(ExtractMetricsError::InvalidTimestamp);
};
track_transaction_name_stats(event);
let tags = extract_universal_tags(event, self.config);
let light_tags = extract_light_transaction_tags(&tags);
let measurement_names: BTreeSet<_> = event
.measurements
.value()
.into_iter()
.flat_map(|measurements| measurements.keys())
.map(String::as_str)
.collect();
if let Some(measurements) = event.measurements.value() {
for (name, annotated) in measurements.iter() {
let Some(measurement) = annotated.value() else {
continue;
};
let Some(value) = measurement.value.value().copied() else {
continue;
};
let Some(value) = FiniteF64::new(value) else {
relay_log::error!(
tags.field = format_args!("measurements.{name}"),
"non-finite float value in transaction metric extraction"
);
continue;
};
let is_performance_score = name == "score.total"
|| name
.strip_prefix("score.weight.")
.or_else(|| name.strip_prefix("score."))
.map_or(false, |suffix| measurement_names.contains(suffix));
let measurement_tags = TransactionMeasurementTags {
measurement_rating: get_measurement_rating(name, value.to_f64()),
universal_tags: if is_performance_score {
CommonTags(
tags.0
.iter()
.filter(|&(key, _)| PERFORMANCE_SCORE_TAGS.contains(key))
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<BTreeMap<_, _>>(),
)
} else {
tags.clone()
},
score_profile_version: is_performance_score
.then(|| event.context::<PerformanceScoreContext>())
.and_then(|context| context?.score_profile_version.value().cloned()),
};
metrics.project_metrics.push(
TransactionMetric::Measurement {
name: name.to_string(),
value,
unit: measurement.unit.value().copied().unwrap_or_default(),
tags: measurement_tags,
}
.into_metric(timestamp),
);
}
}
if let Some(breakdowns) = event.breakdowns.value() {
for (breakdown, measurements) in breakdowns.iter() {
if let Some(measurements) = measurements.value() {
for (measurement_name, annotated) in measurements.iter() {
if measurement_name == "total.time" {
continue;
}
let Some(measurement) = annotated.value() else {
continue;
};
let Some(value) = measurement.value.value().copied() else {
continue;
};
let Some(value) = FiniteF64::new(value) else {
relay_log::error!(
tags.field =
format_args!("breakdowns.{breakdown}.{measurement_name}"),
"non-finite float value in transaction metric extraction"
);
continue;
};
metrics.project_metrics.push(
TransactionMetric::Breakdown {
name: format!("{breakdown}.{measurement_name}"),
value,
tags: tags.clone(),
}
.into_metric(timestamp),
);
}
}
}
}
metrics
.project_metrics
.push(TransactionMetric::Usage.into_metric(timestamp));
let duration = relay_common::time::chrono_to_positive_millis(end - start);
if let Some(duration) = FiniteF64::new(duration) {
metrics.project_metrics.push(
TransactionMetric::Duration {
unit: DurationUnit::MilliSecond,
value: duration,
tags: tags.clone(),
}
.into_metric(timestamp),
);
metrics.project_metrics.push(
TransactionMetric::DurationLight {
unit: DurationUnit::MilliSecond,
value: duration,
tags: light_tags,
}
.into_metric(timestamp),
);
} else {
relay_log::error!(
tags.field = "duration",
"non-finite float value in transaction metric extraction"
);
}
let root_counter_tags = {
let mut universal_tags = CommonTags(BTreeMap::default());
if let Some(transaction_from_dsc) = self.transaction_from_dsc {
universal_tags
.0
.insert(CommonTag::Transaction, transaction_from_dsc.to_string());
}
TransactionCPRTags {
decision: self.sampling_decision.to_string(),
target_project_id: self.target_project_id,
universal_tags,
}
};
metrics.sampling_metrics.push(
TransactionMetric::CountPerRootProject {
tags: root_counter_tags,
}
.into_metric(timestamp),
);
if let Some(user) = event.user.value() {
if let Some(value) = user.sentry_user.value() {
metrics.project_metrics.push(
TransactionMetric::User {
value: value.clone(),
tags,
}
.into_metric(timestamp),
);
}
}
if let Some(generic_config) = self.generic_config {
generic::tmp_apply_tags(&mut metrics.project_metrics, event, generic_config.tags());
generic::tmp_apply_tags(&mut metrics.sampling_metrics, event, generic_config.tags());
}
Ok(metrics)
}
}
fn get_measurement_rating(name: &str, value: f64) -> Option<String> {
let rate_range = |meh_ceiling: f64, poor_ceiling: f64| {
debug_assert!(meh_ceiling < poor_ceiling);
Some(if value < meh_ceiling {
"good".to_owned()
} else if value < poor_ceiling {
"meh".to_owned()
} else {
"poor".to_owned()
})
};
match name {
"lcp" => rate_range(2500.0, 4000.0),
"fcp" => rate_range(1000.0, 3000.0),
"fid" => rate_range(100.0, 300.0),
"inp" => rate_range(200.0, 500.0),
"cls" => rate_range(0.1, 0.25),
_ => None,
}
}
#[cfg(test)]
mod tests {
use relay_dynamic_config::{
AcceptTransactionNames, CombinedMetricExtractionConfig, MetricExtractionConfig, TagMapping,
};
use relay_event_normalization::{
normalize_event, set_default_transaction_source, validate_event, BreakdownsConfig,
CombinedMeasurementsConfig, EventValidationConfig, MeasurementsConfig, NormalizationConfig,
PerformanceScoreConfig, PerformanceScoreProfile, PerformanceScoreWeightedComponent,
};
use relay_metrics::BucketValue;
use relay_protocol::{Annotated, RuleCondition};
use super::*;
#[test]
fn test_extract_transaction_metrics() {
let json = r#"
{
"type": "transaction",
"platform": "javascript",
"start_timestamp": "2021-04-26T07:59:01+0100",
"timestamp": "2021-04-26T08:00:00+0100",
"release": "1.2.3",
"dist": "foo ",
"environment": "fake_environment",
"transaction": "gEt /api/:version/users/",
"transaction_info": {"source": "custom"},
"user": {
"id": "user123",
"geo": {
"country_code": "US"
}
},
"tags": {
"fOO": "bar",
"bogus": "absolutely",
"device.class": "1"
},
"measurements": {
"foo": {"value": 420.69},
"lcp": {"value": 3000.0, "unit": "millisecond"}
},
"contexts": {
"trace": {
"trace_id": "ff62a8b040f340bda5d830223def1d81",
"span_id": "bd429c44b67a3eb4",
"op": "mYOp",
"status": "ok"
},
"browser": {
"name": "Chrome"
},
"os": {
"name": "Windows"
}
},
"request": {
"method": "post"
},
"spans": [
{
"description": "<OrganizationContext>",
"op": "react.mount",
"parent_span_id": "8f5a2b8768cafb4e",
"span_id": "bd429c44b67a3eb4",
"start_timestamp": 1597976300.0000000,
"timestamp": 1597976302.0000000,
"trace_id": "ff62a8b040f340bda5d830223def1d81"
}
]
}
"#;
let mut event = Annotated::from_json(json).unwrap();
let breakdowns_config: BreakdownsConfig = serde_json::from_str(
r#"{
"span_ops": {
"type": "spanOperations",
"matches": ["react.mount"]
}
}"#,
)
.unwrap();
validate_event(&mut event, &EventValidationConfig::default()).unwrap();
normalize_event(
&mut event,
&NormalizationConfig {
breakdowns_config: Some(&breakdowns_config),
enrich_spans: false,
performance_score: Some(&PerformanceScoreConfig {
profiles: vec![PerformanceScoreProfile {
name: Some("".into()),
score_components: vec![PerformanceScoreWeightedComponent {
measurement: "lcp".into(),
weight: 0.5,
p10: 2.0,
p50: 3.0,
optional: false,
}],
condition: Some(RuleCondition::all()),
version: Some("alpha".into()),
}],
}),
..Default::default()
},
);
let config: TransactionMetricsConfig = serde_json::from_str(
r#"{
"version": 1,
"extractCustomTags": ["fOO"]
}"#,
)
.unwrap();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(event.value().unwrap().spans, @r###"
[
Span {
timestamp: Timestamp(
2020-08-21T02:18:22Z,
),
start_timestamp: Timestamp(
2020-08-21T02:18:20Z,
),
exclusive_time: 2000.0,
op: "react.mount",
span_id: SpanId(
"bd429c44b67a3eb4",
),
parent_span_id: SpanId(
"8f5a2b8768cafb4e",
),
trace_id: TraceId(
"ff62a8b040f340bda5d830223def1d81",
),
segment_id: ~,
is_segment: ~,
status: ~,
description: "<OrganizationContext>",
tags: ~,
origin: ~,
profile_id: ~,
data: ~,
sentry_tags: ~,
received: ~,
measurements: ~,
platform: ~,
was_transaction: ~,
other: {},
},
]
"###);
insta::assert_debug_snapshot!(extracted.project_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.foo@none",
),
value: Distribution(
[
420.69,
],
),
tags: {
"browser.name": "Chrome",
"dist": "foo",
"environment": "fake_environment",
"fOO": "bar",
"geo.country_code": "US",
"http.method": "POST",
"os.name": "Windows",
"platform": "javascript",
"release": "1.2.3",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"transaction.status": "ok",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.lcp@millisecond",
),
value: Distribution(
[
3000.0,
],
),
tags: {
"browser.name": "Chrome",
"dist": "foo",
"environment": "fake_environment",
"fOO": "bar",
"geo.country_code": "US",
"http.method": "POST",
"measurement_rating": "meh",
"os.name": "Windows",
"platform": "javascript",
"release": "1.2.3",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"transaction.status": "ok",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.score.lcp@ratio",
),
value: Distribution(
[
0.0,
],
),
tags: {
"browser.name": "Chrome",
"environment": "fake_environment",
"geo.country_code": "US",
"release": "1.2.3",
"sentry.score_profile_version": "alpha",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.score.total@ratio",
),
value: Distribution(
[
0.0,
],
),
tags: {
"browser.name": "Chrome",
"environment": "fake_environment",
"geo.country_code": "US",
"release": "1.2.3",
"sentry.score_profile_version": "alpha",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.score.weight.lcp@ratio",
),
value: Distribution(
[
1.0,
],
),
tags: {
"browser.name": "Chrome",
"environment": "fake_environment",
"geo.country_code": "US",
"release": "1.2.3",
"sentry.score_profile_version": "alpha",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/breakdowns.span_ops.ops.react.mount@millisecond",
),
value: Distribution(
[
2000.0,
],
),
tags: {
"browser.name": "Chrome",
"dist": "foo",
"environment": "fake_environment",
"fOO": "bar",
"geo.country_code": "US",
"http.method": "POST",
"os.name": "Windows",
"platform": "javascript",
"release": "1.2.3",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"transaction.status": "ok",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"c:transactions/usage@none",
),
value: Counter(
1.0,
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"browser.name": "Chrome",
"dist": "foo",
"environment": "fake_environment",
"fOO": "bar",
"geo.country_code": "US",
"http.method": "POST",
"os.name": "Windows",
"platform": "javascript",
"release": "1.2.3",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"transaction.status": "ok",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration_light@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"s:transactions/user@none",
),
value: Set(
{
933084975,
},
),
tags: {
"browser.name": "Chrome",
"dist": "foo",
"environment": "fake_environment",
"fOO": "bar",
"geo.country_code": "US",
"http.method": "POST",
"os.name": "Windows",
"platform": "javascript",
"release": "1.2.3",
"transaction": "gEt /api/:version/users/",
"transaction.op": "mYOp",
"transaction.status": "ok",
"user.geo.subregion": "21",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
#[test]
fn test_metric_measurement_units() {
let json = r#"
{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"measurements": {
"fcp": {"value": 1.1},
"stall_count": {"value": 3.3},
"foo": {"value": 8.8}
},
"contexts": {
"trace": {
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
"span_id": "fa90fdead5f74053"
}
}
}
"#;
let mut event = Annotated::from_json(json).unwrap();
normalize_event(&mut event, &NormalizationConfig::default());
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(extracted.project_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.fcp@millisecond",
),
value: Distribution(
[
1.1,
],
),
tags: {
"measurement_rating": "good",
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.foo@none",
),
value: Distribution(
[
8.8,
],
),
tags: {
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.stall_count@none",
),
value: Distribution(
[
3.3,
],
),
tags: {
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"c:transactions/usage@none",
),
value: Counter(
1.0,
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration_light@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"transaction": "<unlabeled transaction>",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
#[test]
fn test_metric_measurement_unit_overrides() {
let json = r#"{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"measurements": {
"fcp": {"value": 1.1, "unit": "second"},
"lcp": {"value": 2.2, "unit": "none"}
},
"contexts": {
"trace": {
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
"span_id": "fa90fdead5f74053"
}
}
}"#;
let mut event = Annotated::from_json(json).unwrap();
normalize_event(&mut event, &NormalizationConfig::default());
let config: TransactionMetricsConfig = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(extracted.project_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.fcp@second",
),
value: Distribution(
[
1.1,
],
),
tags: {
"measurement_rating": "good",
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/measurements.lcp@none",
),
value: Distribution(
[
2.2,
],
),
tags: {
"measurement_rating": "good",
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"c:transactions/usage@none",
),
value: Counter(
1.0,
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"platform": "other",
"transaction": "<unlabeled transaction>",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration_light@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"transaction": "<unlabeled transaction>",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
#[test]
fn test_transaction_duration() {
let json = r#"
{
"type": "transaction",
"platform": "bogus",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"release": "1.2.3",
"environment": "fake_environment",
"transaction": "mytransaction",
"contexts": {
"trace": {
"status": "ok"
}
}
}
"#;
let event = Annotated::from_json(json).unwrap();
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
let duration_metric = extracted
.project_metrics
.iter()
.find(|m| &*m.name == "d:transactions/duration@millisecond")
.unwrap();
assert_eq!(
&*duration_metric.name,
"d:transactions/duration@millisecond"
);
assert_eq!(
duration_metric.value,
BucketValue::distribution(59000.into())
);
assert_eq!(duration_metric.tags.len(), 4);
assert_eq!(duration_metric.tags["release"], "1.2.3");
assert_eq!(duration_metric.tags["transaction.status"], "ok");
assert_eq!(duration_metric.tags["environment"], "fake_environment");
assert_eq!(duration_metric.tags["platform"], "other");
}
#[test]
fn test_custom_measurements() {
let json = r#"
{
"type": "transaction",
"transaction": "foo",
"start_timestamp": "2021-04-26T08:00:00+0100",
"timestamp": "2021-04-26T08:00:02+0100",
"measurements": {
"a_custom1": {"value": 41},
"fcp": {"value": 0.123, "unit": "millisecond"},
"g_custom2": {"value": 42, "unit": "second"},
"h_custom3": {"value": 43}
},
"contexts": {
"trace": {
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
"span_id": "fa90fdead5f74053"
}}
}
"#;
let mut event = Annotated::from_json(json).unwrap();
let measurements_config: MeasurementsConfig = serde_json::from_value(serde_json::json!(
{
"builtinMeasurements": [{"name": "fcp", "unit": "millisecond"}],
"maxCustomMeasurements": 2
}
))
.unwrap();
let config = CombinedMeasurementsConfig::new(Some(&measurements_config), None);
normalize_event(
&mut event,
&NormalizationConfig {
measurements: Some(config),
..Default::default()
},
);
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(extracted.project_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/measurements.a_custom1@none",
),
value: Distribution(
[
41.0,
],
),
tags: {
"platform": "other",
"transaction": "foo",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/measurements.fcp@millisecond",
),
value: Distribution(
[
0.123,
],
),
tags: {
"measurement_rating": "good",
"platform": "other",
"transaction": "foo",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/measurements.g_custom2@second",
),
value: Distribution(
[
42.0,
],
),
tags: {
"platform": "other",
"transaction": "foo",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"c:transactions/usage@none",
),
value: Counter(
1.0,
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/duration@millisecond",
),
value: Distribution(
[
2000.0,
],
),
tags: {
"platform": "other",
"transaction": "foo",
"transaction.status": "unknown",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/duration_light@millisecond",
),
value: Distribution(
[
2000.0,
],
),
tags: {
"transaction": "foo",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
#[test]
fn test_unknown_transaction_status_no_trace_context() {
let json = r#"
{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100"
}
"#;
let event = Annotated::from_json(json).unwrap();
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
let duration_metric = extracted
.project_metrics
.iter()
.find(|m| &*m.name == "d:transactions/duration@millisecond")
.unwrap();
assert_eq!(
duration_metric.tags,
BTreeMap::from([("platform".to_string(), "other".to_string())])
);
}
#[test]
fn test_unknown_transaction_status() {
let json = r#"
{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {
"trace": {
"status": "ok"
}
}
}
"#;
let event = Annotated::from_json(json).unwrap();
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
let duration_metric = extracted
.project_metrics
.iter()
.find(|m| &*m.name == "d:transactions/duration@millisecond")
.unwrap();
assert_eq!(
duration_metric.tags,
BTreeMap::from([
("transaction.status".to_string(), "ok".to_string()),
("platform".to_string(), "other".to_string())
])
);
}
#[test]
fn test_span_tags() {
let json = r#"
{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {
"trace": {
"status": "ok"
}
},
"spans": [
{
"description": "<OrganizationContext>",
"op": "react.mount",
"parent_span_id": "8f5a2b8768cafb4e",
"span_id": "bd429c44b67a3eb4",
"start_timestamp": 1597976300.0000000,
"timestamp": 1597976302.0000000,
"trace_id": "ff62a8b040f340bda5d830223def1d81"
},
{
"description": "POST http://sth.subdomain.domain.tld:targetport/api/hi",
"op": "http.client",
"parent_span_id": "8f5a2b8768cafb4e",
"span_id": "bd2eb23da2beb459",
"start_timestamp": 1597976300.0000000,
"timestamp": 1597976302.0000000,
"trace_id": "ff62a8b040f340bda5d830223def1d81",
"status": "ok",
"data": {
"http.method": "POST",
"http.response.status_code": "200"
}
}
]
}
"#;
let event = Annotated::from_json(json).unwrap();
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(extracted.project_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"c:transactions/usage@none",
),
value: Counter(
1.0,
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {
"http.status_code": "200",
"platform": "other",
"transaction.status": "ok",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"d:transactions/duration_light@millisecond",
),
value: Distribution(
[
59000.0,
],
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
#[test]
fn test_device_class_mobile() {
let json = r#"
{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {
"trace": {
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
"span_id": "fa90fdead5f74053"
}
},
"measurements": {
"frames_frozen": {
"value": 3
}
},
"tags": {
"device.class": "2"
},
"sdk": {
"name": "sentry.cocoa"
}
}
"#;
let event = Annotated::from_json(json).unwrap();
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
let buckets_by_name = extracted
.project_metrics
.into_iter()
.map(|Bucket { name, tags, .. }| (name, tags))
.collect::<BTreeMap<_, _>>();
assert_eq!(
buckets_by_name["d:transactions/measurements.frames_frozen@none"]["device.class"],
"2"
);
assert_eq!(
buckets_by_name["d:transactions/duration@millisecond"]["device.class"],
"2"
);
}
fn extract_transaction_name(json: &str) -> Option<String> {
let mut event = Annotated::<Event>::from_json(json).unwrap();
set_default_transaction_source(event.value_mut().as_mut().unwrap());
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
let duration_metric = extracted
.project_metrics
.iter()
.find(|m| &*m.name == "d:transactions/duration@millisecond")
.unwrap();
duration_metric.tags.get("transaction").cloned()
}
#[test]
fn test_root_counter_keep() {
let json = r#"
{
"type": "transaction",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"transaction": "ignored",
"contexts": {
"trace": {
"status": "ok"
}
}
}
"#;
let event = Annotated::from_json(json).unwrap();
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("root_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(extracted.sampling_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420400),
width: 0,
name: MetricName(
"c:transactions/count_per_root_project@none",
),
value: Counter(
1.0,
),
tags: {
"decision": "keep",
"target_project_id": "4711",
"transaction": "root_transaction",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
#[test]
fn test_legacy_js_looks_like_url() {
let json = r#"
{
"type": "transaction",
"transaction": "foo/",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.javascript.browser"}
}
"#;
let name = extract_transaction_name(json);
assert!(name.is_none());
}
#[test]
fn test_legacy_js_does_not_look_like_url() {
let json = r#"
{
"type": "transaction",
"transaction": "foo",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.javascript.browser"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name.as_deref(), Some("foo"));
}
#[test]
fn test_js_url_strict() {
let json = r#"
{
"type": "transaction",
"transaction": "foo",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.javascript.browser"},
"transaction_info": {"source": "url"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name, Some("<< unparameterized >>".to_owned()));
}
#[test]
fn test_python_404() {
let json = r#"
{
"type": "transaction",
"transaction": "foo/",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.python", "integrations":["django"]},
"tags": {"http.status_code": "404"}
}
"#;
let name = extract_transaction_name(json);
assert!(name.is_none());
}
#[test]
fn test_python_200() {
let json = r#"
{
"type": "transaction",
"transaction": "foo/",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.python", "integrations":["django"]},
"tags": {"http.status_code": "200"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name, Some("foo/".to_owned()));
}
#[test]
fn test_express_options() {
let json = r#"
{
"type": "transaction",
"transaction": "foo/",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.javascript.node", "integrations":["Express"]},
"request": {"method": "OPTIONS"}
}
"#;
let name = extract_transaction_name(json);
assert!(name.is_none());
}
#[test]
fn test_express() {
let json = r#"
{
"type": "transaction",
"transaction": "foo/",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "sentry.javascript.node", "integrations":["Express"]},
"request": {"method": "GET"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name, Some("foo/".to_owned()));
}
#[test]
fn test_other_client_unknown() {
let json = r#"
{
"type": "transaction",
"transaction": "foo/",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "some_client"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name.as_deref(), Some("foo/"));
}
#[test]
fn test_other_client_url() {
let json = r#"
{
"type": "transaction",
"transaction": "foo",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "some_client"},
"transaction_info": {"source": "url"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name, Some("<< unparameterized >>".to_owned()));
}
#[test]
fn test_any_client_route() {
let json = r#"
{
"type": "transaction",
"transaction": "foo",
"timestamp": "2021-04-26T08:00:00+0100",
"start_timestamp": "2021-04-26T07:59:01+0100",
"contexts": {"trace": {}},
"sdk": {"name": "some_client"},
"transaction_info": {"source": "route"}
}
"#;
let name = extract_transaction_name(json);
assert_eq!(name, Some("foo".to_owned()));
}
#[test]
fn test_parse_transaction_name_strategy() {
for (config_str, expected_strategy) in [
(r#"{}"#, AcceptTransactionNames::ClientBased),
(
r#"{"acceptTransactionNames": "unknown-strategy"}"#,
AcceptTransactionNames::ClientBased,
),
(
r#"{"acceptTransactionNames": "strict"}"#,
AcceptTransactionNames::Strict,
),
(
r#"{"acceptTransactionNames": "clientBased"}"#,
AcceptTransactionNames::ClientBased,
),
] {
let config: TransactionMetricsConfig = serde_json::from_str(config_str).unwrap();
assert_eq!(config.deprecated1, expected_strategy, "{}", config_str);
}
}
#[test]
fn test_computed_metrics() {
let json = r#"{
"type": "transaction",
"timestamp": 1619420520,
"start_timestamp": 1619420400,
"contexts": {
"trace": {
"trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
"span_id": "fa90fdead5f74053"
}
},
"measurements": {
"frames_frozen": {
"value": 2
},
"frames_slow": {
"value": 1
},
"frames_total": {
"value": 4
},
"stall_total_time": {
"value": 4,
"unit": "millisecond"
}
}
}"#;
let mut event = Annotated::from_json(json).unwrap();
normalize_event(&mut event, &NormalizationConfig::default());
let config = TransactionMetricsConfig::default();
let extractor = TransactionExtractor {
config: &config,
generic_config: None,
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
let metrics_names: Vec<_> = extracted
.project_metrics
.into_iter()
.map(|m| m.name)
.collect();
insta::assert_debug_snapshot!(metrics_names, @r###"
[
MetricName(
"d:transactions/measurements.frames_frozen@none",
),
MetricName(
"d:transactions/measurements.frames_frozen_rate@ratio",
),
MetricName(
"d:transactions/measurements.frames_slow@none",
),
MetricName(
"d:transactions/measurements.frames_slow_rate@ratio",
),
MetricName(
"d:transactions/measurements.frames_total@none",
),
MetricName(
"d:transactions/measurements.stall_percentage@ratio",
),
MetricName(
"d:transactions/measurements.stall_total_time@millisecond",
),
MetricName(
"c:transactions/usage@none",
),
MetricName(
"d:transactions/duration@millisecond",
),
MetricName(
"d:transactions/duration_light@millisecond",
),
]
"###);
}
#[test]
fn test_conditional_tagging() {
let event = Annotated::from_json(
r#"{
"type": "transaction",
"platform": "javascript",
"transaction": "foo",
"start_timestamp": "2021-04-26T08:00:00+0100",
"timestamp": "2021-04-26T08:00:02+0100",
"measurements": {
"lcp": {"value": 41, "unit": "millisecond"}
}
}"#,
)
.unwrap();
let config = TransactionMetricsConfig::new();
let generic_tags: Vec<TagMapping> = serde_json::from_str(
r#"[
{
"metrics": ["d:transactions/duration@millisecond"],
"tags": [
{
"condition": {"op": "gte", "name": "event.duration", "value": 9001},
"key": "satisfaction",
"value": "frustrated"
},
{
"condition": {"op": "gte", "name": "event.duration", "value": 666},
"key": "satisfaction",
"value": "tolerated"
},
{
"condition": {"op": "and", "inner": []},
"key": "satisfaction",
"value": "satisfied"
}
]
}
]"#,
)
.unwrap();
let generic_config = MetricExtractionConfig {
version: 1,
tags: generic_tags,
..Default::default()
};
let combined_config = CombinedMetricExtractionConfig::from(&generic_config);
let extractor = TransactionExtractor {
config: &config,
generic_config: Some(combined_config),
transaction_from_dsc: Some("test_transaction"),
sampling_decision: SamplingDecision::Keep,
target_project_id: ProjectId::new(4711),
};
let extracted = extractor.extract(event.value().unwrap()).unwrap();
insta::assert_debug_snapshot!(extracted.project_metrics, @r###"
[
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/measurements.lcp@millisecond",
),
value: Distribution(
[
41.0,
],
),
tags: {
"measurement_rating": "good",
"platform": "javascript",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"c:transactions/usage@none",
),
value: Counter(
1.0,
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/duration@millisecond",
),
value: Distribution(
[
2000.0,
],
),
tags: {
"platform": "javascript",
"satisfaction": "tolerated",
},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
Bucket {
timestamp: UnixTimestamp(1619420402),
width: 0,
name: MetricName(
"d:transactions/duration_light@millisecond",
),
value: Distribution(
[
2000.0,
],
),
tags: {},
metadata: BucketMetadata {
merges: 1,
received_at: Some(
UnixTimestamp(0),
),
extracted_from_indexed: false,
},
},
]
"###);
}
}