Skip to content

Commit dbb9763

Browse files
authored
feat(metrics): Two modes for accepting transaction names (#1352)
Add a new project config flag transactionMetrics.acceptTransactionNames that can be either: - "strict" - Only accept transaction names from low-cardinality sources. - "clientBased" - Accept all transaction names, except for browser JS SDKs, where strict rules apply. In both cases, either omit the transaction name (when source is unknown), or replace by sentinel "<< unparametrized >>". By adding this to project configs, we can add an option in sentry to consistently sample a percentage of orgs into the new behavior. Sentry-side implemented in getsentry/sentry#37102
1 parent 6728917 commit dbb9763

File tree

2 files changed

+229
-47
lines changed

2 files changed

+229
-47
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
- Support compressed project configs in redis cache. ([#1345](https://github.com/getsentry/relay/pull/1345))
1212
- Refactor profile processing into its own crate. ([#1340](https://github.com/getsentry/relay/pull/1340))
13-
13+
- Treat "unknown" transaction source as low cardinality, except for JS. ([#1352](https://github.com/getsentry/relay/pull/1352))
1414
## 22.7.0
1515

1616
**Features**:

relay-server/src/metrics_extraction/transactions.rs

+228-46
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ pub struct CustomMeasurementConfig {
5454
/// The version is an integer scalar, incremented by one on each new version.
5555
const EXTRACT_MAX_VERSION: u16 = 1;
5656

57+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
58+
#[serde(rename_all = "camelCase")]
59+
pub enum AcceptTransactionNames {
60+
/// For some SDKs, accept all transaction names, while for others, apply strict rules.
61+
ClientBased,
62+
63+
/// Only accept transaction names with a low-cardinality source.
64+
/// Any value other than "clientBased" will be interpreted as "strict".
65+
#[serde(other)]
66+
Strict,
67+
}
68+
69+
impl Default for AcceptTransactionNames {
70+
fn default() -> Self {
71+
Self::Strict
72+
}
73+
}
74+
5775
/// Configuration for extracting metrics from transaction payloads.
5876
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
5977
#[serde(default, rename_all = "camelCase")]
@@ -64,6 +82,7 @@ pub struct TransactionMetricsConfig {
6482
extract_custom_tags: BTreeSet<String>,
6583
satisfaction_thresholds: Option<SatisfactionConfig>,
6684
custom_measurements: CustomMeasurementConfig,
85+
accept_transaction_names: AcceptTransactionNames,
6786
}
6887

6988
impl TransactionMetricsConfig {
@@ -170,10 +189,7 @@ fn extract_user_satisfaction(
170189
None
171190
}
172191

173-
/// Decide whether we want to keep the transaction name.
174-
/// High-cardinality sources are excluded to protect our metrics infrastructure.
175-
/// Note that this will produce a discrepancy between metrics and raw transaction data.
176-
fn keep_transaction_name(source: &TransactionSource) -> bool {
192+
fn is_low_cardinality(source: &TransactionSource, treat_unknown_as_low_cardinality: bool) -> bool {
177193
match source {
178194
// For now, we hope that custom transaction names set by users are low-cardinality.
179195
TransactionSource::Custom => true,
@@ -188,8 +204,8 @@ fn keep_transaction_name(source: &TransactionSource) -> bool {
188204
TransactionSource::Task => true,
189205

190206
// "unknown" is the value for old SDKs that do not send a transaction source yet.
191-
// Assume high-cardinality and drop.
192-
TransactionSource::Unknown => false,
207+
// Caller decides how to treat this.
208+
TransactionSource::Unknown => treat_unknown_as_low_cardinality,
193209

194210
// Any other value would be an SDK bug, assume high-cardinality and drop.
195211
TransactionSource::Other(source) => {
@@ -199,10 +215,65 @@ fn keep_transaction_name(source: &TransactionSource) -> bool {
199215
}
200216
}
201217

218+
fn is_browser_sdk(event: &Event) -> bool {
219+
let sdk_name = event
220+
.client_sdk
221+
.value()
222+
.and_then(|sdk| sdk.name.value())
223+
.map(|s| s.as_str())
224+
.unwrap_or_default();
225+
226+
[
227+
"sentry.javascript.angular",
228+
"sentry.javascript.browser",
229+
"sentry.javascript.ember",
230+
"sentry.javascript.gatsby",
231+
"sentry.javascript.react",
232+
"sentry.javascript.remix",
233+
"sentry.javascript.vue",
234+
]
235+
.contains(&sdk_name)
236+
}
237+
238+
/// Decide whether we want to keep the transaction name.
239+
/// High-cardinality sources are excluded to protect our metrics infrastructure.
240+
/// Note that this will produce a discrepancy between metrics and raw transaction data.
241+
fn get_transaction_name(
242+
event: &Event,
243+
accept_transaction_names: &AcceptTransactionNames,
244+
) -> Option<String> {
245+
let original_transaction_name = match event.transaction.value() {
246+
Some(name) => name,
247+
None => {
248+
return None;
249+
}
250+
};
251+
252+
// In client-based mode, handling of "unknown" sources depends on the SDK name.
253+
// In strict mode, treat "unknown" as high cardinality.
254+
let treat_unknown_as_low_cardinality = matches!(
255+
accept_transaction_names,
256+
AcceptTransactionNames::ClientBased
257+
) && !is_browser_sdk(event);
258+
259+
let source = event.get_transaction_source();
260+
let use_original_name = is_low_cardinality(source, treat_unknown_as_low_cardinality);
261+
262+
if use_original_name {
263+
Some(original_transaction_name.clone())
264+
} else {
265+
// Pick a sentinel based on the transaction source:
266+
match source {
267+
TransactionSource::Unknown | TransactionSource::Other(_) => None,
268+
_ => Some("<< unparametrized >>".to_owned()),
269+
}
270+
}
271+
}
272+
202273
/// These are the tags that are added to all extracted metrics.
203274
fn extract_universal_tags(
204275
event: &Event,
205-
custom_tags: &BTreeSet<String>,
276+
config: &TransactionMetricsConfig,
206277
) -> BTreeMap<String, String> {
207278
let mut tags = BTreeMap::new();
208279
if let Some(release) = event.release.as_str() {
@@ -214,10 +285,8 @@ fn extract_universal_tags(
214285
if let Some(environment) = event.environment.as_str() {
215286
tags.insert("environment".to_owned(), environment.to_owned());
216287
}
217-
if let Some(transaction) = event.transaction.as_str() {
218-
if keep_transaction_name(event.get_transaction_source()) {
219-
tags.insert("transaction".to_owned(), transaction.to_owned());
220-
}
288+
if let Some(transaction_name) = get_transaction_name(event, &config.accept_transaction_names) {
289+
tags.insert("transaction".to_owned(), transaction_name);
221290
}
222291

223292
// The platform tag should not increase dimensionality in most cases, because most
@@ -241,6 +310,7 @@ fn extract_universal_tags(
241310
tags.insert("http.method".to_owned(), http_method);
242311
}
243312

313+
let custom_tags = &config.extract_custom_tags;
244314
if !custom_tags.is_empty() {
245315
// XXX(slow): event tags are a flat array
246316
if let Some(event_tags) = event.tags.value() {
@@ -353,7 +423,7 @@ fn extract_transaction_metrics_inner(
353423
None => return,
354424
};
355425

356-
let tags = extract_universal_tags(event, &config.extract_custom_tags);
426+
let tags = extract_universal_tags(event, config);
357427

358428
// Measurements
359429
if let Some(measurements) = event.measurements.value() {
@@ -493,7 +563,7 @@ mod tests {
493563

494564
use crate::metrics_extraction::TaggingRule;
495565
use insta::assert_debug_snapshot;
496-
use relay_general::protocol::TransactionInfo;
566+
use relay_general::protocol::{ClientSdkInfo, TransactionInfo};
497567
use relay_general::store::BreakdownsConfig;
498568
use relay_general::types::Annotated;
499569
use relay_metrics::DurationUnit;
@@ -1226,8 +1296,12 @@ mod tests {
12261296
);
12271297
}
12281298

1229-
#[test]
1230-
fn test_has_transaction_tag() {
1299+
/// Helper function to check if the transaction name is set correctly
1300+
fn extract_transaction_name(
1301+
sdk_name: &str,
1302+
source: &TransactionSource,
1303+
strategy: AcceptTransactionNames,
1304+
) -> Option<String> {
12311305
let json = r#"
12321306
{
12331307
"type": "transaction",
@@ -1238,7 +1312,7 @@ mod tests {
12381312
}
12391313
"#;
12401314

1241-
let config: TransactionMetricsConfig = serde_json::from_str(
1315+
let mut config: TransactionMetricsConfig = serde_json::from_str(
12421316
r#"
12431317
{
12441318
"extractMetrics": [
@@ -1249,40 +1323,148 @@ mod tests {
12491323
)
12501324
.unwrap();
12511325

1252-
let event = Annotated::<Event>::from_json(json).unwrap();
1326+
let mut event = Annotated::<Event>::from_json(json).unwrap();
1327+
{
1328+
let event = event.value_mut().as_mut().unwrap();
1329+
event.transaction_info.set_value(Some(TransactionInfo {
1330+
source: Annotated::new(source.clone()),
1331+
original: Annotated::empty(),
1332+
}));
1333+
1334+
event.client_sdk.set_value(Some(ClientSdkInfo {
1335+
name: Annotated::new(sdk_name.to_owned()),
1336+
..Default::default()
1337+
}));
1338+
}
1339+
1340+
config.accept_transaction_names = strategy;
1341+
1342+
let mut metrics = vec![];
1343+
extract_transaction_metrics(&config, None, &[], event.value().unwrap(), &mut metrics);
1344+
1345+
assert_eq!(metrics.len(), 1);
1346+
metrics[0].tags.get("transaction").cloned()
1347+
}
1348+
1349+
#[test]
1350+
fn test_js_unknown_strict() {
1351+
let name = extract_transaction_name(
1352+
"sentry.javascript.browser",
1353+
&TransactionSource::Unknown,
1354+
AcceptTransactionNames::Strict,
1355+
);
1356+
assert!(name.is_none());
1357+
}
1358+
1359+
#[test]
1360+
fn test_js_unknown_client_based() {
1361+
let name = extract_transaction_name(
1362+
"sentry.javascript.browser",
1363+
&TransactionSource::Unknown,
1364+
AcceptTransactionNames::ClientBased,
1365+
);
1366+
assert!(name.is_none());
1367+
}
12531368

1254-
for (source, expected_transaction_tag) in [
1255-
(TransactionSource::Custom, true),
1256-
(TransactionSource::Url, false),
1257-
(TransactionSource::Route, true),
1258-
(TransactionSource::View, true),
1259-
(TransactionSource::Component, true),
1260-
(TransactionSource::Task, true),
1261-
(TransactionSource::Unknown, false),
1369+
#[test]
1370+
fn test_js_url_strict() {
1371+
let name = extract_transaction_name(
1372+
"sentry.javascript.browser",
1373+
&TransactionSource::Url,
1374+
AcceptTransactionNames::Strict,
1375+
);
1376+
assert_eq!(name, Some("<< unparametrized >>".to_owned()));
1377+
}
1378+
1379+
#[test]
1380+
fn test_js_url_client_based() {
1381+
let name = extract_transaction_name(
1382+
"sentry.javascript.browser",
1383+
&TransactionSource::Url,
1384+
AcceptTransactionNames::Strict,
1385+
);
1386+
assert_eq!(name, Some("<< unparametrized >>".to_owned()));
1387+
}
1388+
1389+
#[test]
1390+
fn test_other_client_unknown_strict() {
1391+
let name = extract_transaction_name(
1392+
"some_client",
1393+
&TransactionSource::Unknown,
1394+
AcceptTransactionNames::Strict,
1395+
);
1396+
assert!(name.is_none());
1397+
}
1398+
1399+
#[test]
1400+
fn test_other_client_unknown_client_based() {
1401+
let name = extract_transaction_name(
1402+
"some_client",
1403+
&TransactionSource::Unknown,
1404+
AcceptTransactionNames::ClientBased,
1405+
);
1406+
assert_eq!(name, Some("foo".to_owned()));
1407+
}
1408+
1409+
#[test]
1410+
fn test_other_client_url_strict() {
1411+
let name = extract_transaction_name(
1412+
"some_client",
1413+
&TransactionSource::Url,
1414+
AcceptTransactionNames::Strict,
1415+
);
1416+
assert_eq!(name, Some("<< unparametrized >>".to_owned()));
1417+
}
1418+
1419+
#[test]
1420+
fn test_other_client_url_client_based() {
1421+
let name = extract_transaction_name(
1422+
"some_client",
1423+
&TransactionSource::Url,
1424+
AcceptTransactionNames::Strict,
1425+
);
1426+
assert_eq!(name, Some("<< unparametrized >>".to_owned()));
1427+
}
1428+
1429+
#[test]
1430+
fn test_any_client_route_strict() {
1431+
let name = extract_transaction_name(
1432+
"some_client",
1433+
&TransactionSource::Route,
1434+
AcceptTransactionNames::Strict,
1435+
);
1436+
assert_eq!(name, Some("foo".to_owned()));
1437+
}
1438+
1439+
#[test]
1440+
fn test_any_client_route_client_based() {
1441+
let name = extract_transaction_name(
1442+
"some_client",
1443+
&TransactionSource::Route,
1444+
AcceptTransactionNames::ClientBased,
1445+
);
1446+
assert_eq!(name, Some("foo".to_owned()));
1447+
}
1448+
1449+
#[test]
1450+
fn test_parse_transaction_name_strategy() {
1451+
for (config, expected_strategy) in [
1452+
(r#"{}"#, AcceptTransactionNames::Strict),
1453+
(
1454+
r#"{"acceptTransactionNames": "unknown-strategy"}"#,
1455+
AcceptTransactionNames::Strict,
1456+
),
1457+
(
1458+
r#"{"acceptTransactionNames": "strict"}"#,
1459+
AcceptTransactionNames::Strict,
1460+
),
12621461
(
1263-
TransactionSource::Other("not-a-valid-source".to_owned()),
1264-
false,
1462+
r#"{"acceptTransactionNames": "clientBased"}"#,
1463+
AcceptTransactionNames::ClientBased,
12651464
),
12661465
] {
1267-
let mut event = event.clone();
1268-
event
1269-
.value_mut()
1270-
.as_mut()
1271-
.unwrap()
1272-
.transaction_info
1273-
.set_value(Some(TransactionInfo {
1274-
source: Annotated::new(source.clone()),
1275-
original: Annotated::empty(),
1276-
}));
1277-
let mut metrics = vec![];
1278-
extract_transaction_metrics(&config, None, &[], event.value().unwrap(), &mut metrics);
1279-
1280-
assert_eq!(
1281-
metrics[0].tags.get("transaction").is_some(),
1282-
expected_transaction_tag,
1283-
"{:?}",
1284-
source
1285-
);
1466+
let config: TransactionMetricsConfig = serde_json::from_str(config).unwrap();
1467+
assert_eq!(config.accept_transaction_names, expected_strategy);
12861468
}
12871469
}
12881470
}

0 commit comments

Comments
 (0)