@@ -425,22 +425,32 @@ fn normalize_http_attributes(
425425
426426 // Skip normalization if not an http span.
427427 // This is equivalent to conditionally scrubbing by span category in the V1 pipeline.
428- if !attributes. contains_key ( HTTP_REQUEST_METHOD ) {
428+ if !attributes. contains_key ( HTTP_REQUEST_METHOD )
429+ && !attributes. contains_key ( LEGACY_HTTP_REQUEST_METHOD )
430+ {
429431 return ;
430432 }
431433
432434 let op = attributes. get_value ( OP ) . and_then ( |v| v. as_str ( ) ) ;
433435
434436 let method = attributes
435437 . get_value ( HTTP_REQUEST_METHOD )
438+ . or_else ( || attributes. get_value ( LEGACY_HTTP_REQUEST_METHOD ) )
436439 . and_then ( |v| v. as_str ( ) ) ;
437440
438441 let server_address = attributes
439442 . get_value ( SERVER_ADDRESS )
440443 . and_then ( |v| v. as_str ( ) ) ;
441444
442- let url = attributes. get_value ( URL_FULL ) . and_then ( |v| v. as_str ( ) ) ;
443-
445+ let url: Option < & str > = attributes
446+ . get_value ( URL_FULL )
447+ . and_then ( |v| v. as_str ( ) )
448+ . or_else ( || {
449+ attributes
450+ . get_value ( DESCRIPTION )
451+ . and_then ( |v| v. as_str ( ) )
452+ . and_then ( |description| description. split_once ( ' ' ) . map ( |( _, url) | url) )
453+ } ) ;
444454 let url_scheme = attributes. get_value ( URL_SCHEME ) . and_then ( |v| v. as_str ( ) ) ;
445455
446456 // If the span op is "http.client" and the method and url are present,
@@ -452,7 +462,7 @@ fn normalize_http_attributes(
452462 . and_then ( |scrubbed_http| domain_from_scrubbed_http ( & scrubbed_http) ) ;
453463
454464 if let Some ( domain) = domain_from_scrubbed_http {
455- ( Some ( domain) , None )
465+ ( Some ( domain) , url . map ( String :: from ) )
456466 } else {
457467 domain_from_server_address ( server_address, url_scheme)
458468 }
@@ -477,6 +487,40 @@ fn normalize_http_attributes(
477487 }
478488}
479489
490+ /// Double writes sentry conventions attributes into legacy attributes.
491+ ///
492+ /// This achieves backwards compatibility as it allows products to continue using legacy attributes
493+ /// while we accumulate spans that conform to sentry conventions.
494+ ///
495+ /// This function is called after attribute value normalization (`normalize_attribute_values`) as it
496+ /// clones normalized attributes into legacy attributes.
497+ pub fn write_legacy_attributes ( attributes : & mut Annotated < Attributes > ) {
498+ let Some ( attributes) = attributes. value_mut ( ) else {
499+ return ;
500+ } ;
501+
502+ // Map of new sentry conventions attributes to legacy SpanV1 attributes
503+ let current_to_legacy_attributes = [
504+ // DB attributes
505+ ( NORMALIZED_DB_QUERY , SENTRY_NORMALIZED_DESCRIPTION ) ,
506+ ( NORMALIZED_DB_QUERY_HASH , SENTRY_GROUP ) ,
507+ ( DB_OPERATION_NAME , SENTRY_ACTION ) ,
508+ ( DB_COLLECTION_NAME , SENTRY_DOMAIN ) ,
509+ // HTTP attributes
510+ ( SERVER_ADDRESS , SENTRY_DOMAIN ) ,
511+ ( HTTP_REQUEST_METHOD , SENTRY_ACTION ) ,
512+ ] ;
513+
514+ for ( current_attribute, legacy_attribute) in current_to_legacy_attributes {
515+ if attributes. contains_key ( current_attribute) {
516+ let Some ( attr) = attributes. get_attribute ( current_attribute) else {
517+ continue ;
518+ } ;
519+ attributes. insert ( legacy_attribute, attr. value . clone ( ) ) ;
520+ }
521+ }
522+ }
523+
480524#[ cfg( test) ]
481525mod tests {
482526 use relay_protocol:: SerializableAnnotated ;
@@ -1346,4 +1390,214 @@ mod tests {
13461390 }
13471391 "# ) ;
13481392 }
1393+
1394+ #[ test]
1395+ fn test_normalize_db_attributes_from_legacy_attributes ( ) {
1396+ let mut attributes = Annotated :: < Attributes > :: from_json (
1397+ r#"
1398+ {
1399+ "sentry.op": {
1400+ "type": "string",
1401+ "value": "db"
1402+ },
1403+ "db.system.name": {
1404+ "type": "string",
1405+ "value": "mongodb"
1406+ },
1407+ "sentry.description": {
1408+ "type": "string",
1409+ "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1410+ },
1411+ "db.operation.name": {
1412+ "type": "string",
1413+ "value": "find"
1414+ },
1415+ "db.collection.name": {
1416+ "type": "string",
1417+ "value": "documents"
1418+ }
1419+ }
1420+ "# ,
1421+ )
1422+ . unwrap ( ) ;
1423+
1424+ normalize_db_attributes ( & mut attributes) ;
1425+
1426+ insta:: assert_json_snapshot!( SerializableAnnotated ( & attributes) , @r#"
1427+ {
1428+ "db.collection.name": {
1429+ "type": "string",
1430+ "value": "documents"
1431+ },
1432+ "db.operation.name": {
1433+ "type": "string",
1434+ "value": "FIND"
1435+ },
1436+ "db.system.name": {
1437+ "type": "string",
1438+ "value": "mongodb"
1439+ },
1440+ "sentry.description": {
1441+ "type": "string",
1442+ "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1443+ },
1444+ "sentry.normalized_db_query": {
1445+ "type": "string",
1446+ "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1447+ },
1448+ "sentry.normalized_db_query.hash": {
1449+ "type": "string",
1450+ "value": "aedc5c7e8cec726b"
1451+ },
1452+ "sentry.op": {
1453+ "type": "string",
1454+ "value": "db"
1455+ }
1456+ }
1457+ "# ) ;
1458+ }
1459+
1460+ #[ test]
1461+ fn test_normalize_http_attributes_from_legacy_attributes ( ) {
1462+ let mut attributes = Annotated :: < Attributes > :: from_json (
1463+ r#"
1464+ {
1465+ "sentry.op": {
1466+ "type": "string",
1467+ "value": "http.client"
1468+ },
1469+ "http.request_method": {
1470+ "type": "string",
1471+ "value": "GET"
1472+ },
1473+ "sentry.description": {
1474+ "type": "string",
1475+ "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
1476+ }
1477+ }
1478+ "# ,
1479+ )
1480+ . unwrap ( ) ;
1481+
1482+ normalize_http_attributes ( & mut attributes, & [ ] ) ;
1483+
1484+ insta:: assert_json_snapshot!( SerializableAnnotated ( & attributes) , @r#"
1485+ {
1486+ "http.request.method": {
1487+ "type": "string",
1488+ "value": "GET"
1489+ },
1490+ "http.request_method": {
1491+ "type": "string",
1492+ "value": "GET"
1493+ },
1494+ "sentry.description": {
1495+ "type": "string",
1496+ "value": "GET https://application.www.xn--85x722f.xn--55qx5d.cn"
1497+ },
1498+ "sentry.op": {
1499+ "type": "string",
1500+ "value": "http.client"
1501+ },
1502+ "server.address": {
1503+ "type": "string",
1504+ "value": "*.xn--85x722f.xn--55qx5d.cn"
1505+ },
1506+ "url.full": {
1507+ "type": "string",
1508+ "value": "https://application.www.xn--85x722f.xn--55qx5d.cn"
1509+ }
1510+ }
1511+ "# ) ;
1512+ }
1513+
1514+ #[ test]
1515+ fn test_write_legacy_attributes ( ) {
1516+ let mut attributes = Annotated :: < Attributes > :: from_json (
1517+ r#"
1518+ {
1519+ "db.collection.name": {
1520+ "type": "string",
1521+ "value": "documents"
1522+ },
1523+ "db.operation.name": {
1524+ "type": "string",
1525+ "value": "FIND"
1526+ },
1527+ "db.query.text": {
1528+ "type": "string",
1529+ "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1530+ },
1531+ "db.system.name": {
1532+ "type": "string",
1533+ "value": "mongodb"
1534+ },
1535+ "sentry.normalized_db_query": {
1536+ "type": "string",
1537+ "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1538+ },
1539+ "sentry.normalized_db_query.hash": {
1540+ "type": "string",
1541+ "value": "aedc5c7e8cec726b"
1542+ },
1543+ "sentry.op": {
1544+ "type": "string",
1545+ "value": "db"
1546+ }
1547+ }
1548+ "# ,
1549+ )
1550+ . unwrap ( ) ;
1551+
1552+ write_legacy_attributes ( & mut attributes) ;
1553+
1554+ insta:: assert_json_snapshot!( SerializableAnnotated ( & attributes) , @r#"
1555+ {
1556+ "db.collection.name": {
1557+ "type": "string",
1558+ "value": "documents"
1559+ },
1560+ "db.operation.name": {
1561+ "type": "string",
1562+ "value": "FIND"
1563+ },
1564+ "db.query.text": {
1565+ "type": "string",
1566+ "value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
1567+ },
1568+ "db.system.name": {
1569+ "type": "string",
1570+ "value": "mongodb"
1571+ },
1572+ "sentry.action": {
1573+ "type": "string",
1574+ "value": "FIND"
1575+ },
1576+ "sentry.domain": {
1577+ "type": "string",
1578+ "value": "documents"
1579+ },
1580+ "sentry.group": {
1581+ "type": "string",
1582+ "value": "aedc5c7e8cec726b"
1583+ },
1584+ "sentry.normalized_db_query": {
1585+ "type": "string",
1586+ "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1587+ },
1588+ "sentry.normalized_db_query.hash": {
1589+ "type": "string",
1590+ "value": "aedc5c7e8cec726b"
1591+ },
1592+ "sentry.normalized_description": {
1593+ "type": "string",
1594+ "value": "{\"find\":\"documents\",\"foo\":\"?\"}"
1595+ },
1596+ "sentry.op": {
1597+ "type": "string",
1598+ "value": "db"
1599+ }
1600+ }
1601+ "# ) ;
1602+ }
13491603}
0 commit comments