@@ -54,6 +54,24 @@ pub struct CustomMeasurementConfig {
54
54
/// The version is an integer scalar, incremented by one on each new version.
55
55
const EXTRACT_MAX_VERSION : u16 = 1 ;
56
56
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
+
57
75
/// Configuration for extracting metrics from transaction payloads.
58
76
#[ derive( Default , Debug , Clone , Serialize , Deserialize ) ]
59
77
#[ serde( default , rename_all = "camelCase" ) ]
@@ -64,6 +82,7 @@ pub struct TransactionMetricsConfig {
64
82
extract_custom_tags : BTreeSet < String > ,
65
83
satisfaction_thresholds : Option < SatisfactionConfig > ,
66
84
custom_measurements : CustomMeasurementConfig ,
85
+ accept_transaction_names : AcceptTransactionNames ,
67
86
}
68
87
69
88
impl TransactionMetricsConfig {
@@ -170,10 +189,7 @@ fn extract_user_satisfaction(
170
189
None
171
190
}
172
191
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 {
177
193
match source {
178
194
// For now, we hope that custom transaction names set by users are low-cardinality.
179
195
TransactionSource :: Custom => true ,
@@ -188,8 +204,8 @@ fn keep_transaction_name(source: &TransactionSource) -> bool {
188
204
TransactionSource :: Task => true ,
189
205
190
206
// "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 ,
193
209
194
210
// Any other value would be an SDK bug, assume high-cardinality and drop.
195
211
TransactionSource :: Other ( source) => {
@@ -199,10 +215,65 @@ fn keep_transaction_name(source: &TransactionSource) -> bool {
199
215
}
200
216
}
201
217
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
+
202
273
/// These are the tags that are added to all extracted metrics.
203
274
fn extract_universal_tags (
204
275
event : & Event ,
205
- custom_tags : & BTreeSet < String > ,
276
+ config : & TransactionMetricsConfig ,
206
277
) -> BTreeMap < String , String > {
207
278
let mut tags = BTreeMap :: new ( ) ;
208
279
if let Some ( release) = event. release . as_str ( ) {
@@ -214,10 +285,8 @@ fn extract_universal_tags(
214
285
if let Some ( environment) = event. environment . as_str ( ) {
215
286
tags. insert ( "environment" . to_owned ( ) , environment. to_owned ( ) ) ;
216
287
}
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) ;
221
290
}
222
291
223
292
// The platform tag should not increase dimensionality in most cases, because most
@@ -241,6 +310,7 @@ fn extract_universal_tags(
241
310
tags. insert ( "http.method" . to_owned ( ) , http_method) ;
242
311
}
243
312
313
+ let custom_tags = & config. extract_custom_tags ;
244
314
if !custom_tags. is_empty ( ) {
245
315
// XXX(slow): event tags are a flat array
246
316
if let Some ( event_tags) = event. tags . value ( ) {
@@ -353,7 +423,7 @@ fn extract_transaction_metrics_inner(
353
423
None => return ,
354
424
} ;
355
425
356
- let tags = extract_universal_tags ( event, & config. extract_custom_tags ) ;
426
+ let tags = extract_universal_tags ( event, config) ;
357
427
358
428
// Measurements
359
429
if let Some ( measurements) = event. measurements . value ( ) {
@@ -493,7 +563,7 @@ mod tests {
493
563
494
564
use crate :: metrics_extraction:: TaggingRule ;
495
565
use insta:: assert_debug_snapshot;
496
- use relay_general:: protocol:: TransactionInfo ;
566
+ use relay_general:: protocol:: { ClientSdkInfo , TransactionInfo } ;
497
567
use relay_general:: store:: BreakdownsConfig ;
498
568
use relay_general:: types:: Annotated ;
499
569
use relay_metrics:: DurationUnit ;
@@ -1226,8 +1296,12 @@ mod tests {
1226
1296
) ;
1227
1297
}
1228
1298
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 > {
1231
1305
let json = r#"
1232
1306
{
1233
1307
"type": "transaction",
@@ -1238,7 +1312,7 @@ mod tests {
1238
1312
}
1239
1313
"# ;
1240
1314
1241
- let config: TransactionMetricsConfig = serde_json:: from_str (
1315
+ let mut config: TransactionMetricsConfig = serde_json:: from_str (
1242
1316
r#"
1243
1317
{
1244
1318
"extractMetrics": [
@@ -1249,40 +1323,148 @@ mod tests {
1249
1323
)
1250
1324
. unwrap ( ) ;
1251
1325
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
+ }
1253
1368
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
+ ) ,
1262
1461
(
1263
- TransactionSource :: Other ( "not-a-valid-source" . to_owned ( ) ) ,
1264
- false ,
1462
+ r#"{"acceptTransactionNames": "clientBased"}"# ,
1463
+ AcceptTransactionNames :: ClientBased ,
1265
1464
) ,
1266
1465
] {
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) ;
1286
1468
}
1287
1469
}
1288
1470
}
0 commit comments