@@ -7,11 +7,12 @@ use std::net::IpAddr;
77use chrono:: { DateTime , Utc } ;
88use relay_common:: time:: UnixTimestamp ;
99use relay_conventions:: {
10- BROWSER_NAME , BROWSER_VERSION , CLIENT_ADDRESS , OBSERVED_TIMESTAMP_NANOS , USER_AGENT_ORIGINAL ,
11- USER_GEO_CITY , USER_GEO_COUNTRY_CODE , USER_GEO_REGION , USER_GEO_SUBDIVISION ,
10+ AttributeInfo , BROWSER_NAME , BROWSER_VERSION , CLIENT_ADDRESS , OBSERVED_TIMESTAMP_NANOS ,
11+ USER_AGENT_ORIGINAL , USER_GEO_CITY , USER_GEO_COUNTRY_CODE , USER_GEO_REGION ,
12+ USER_GEO_SUBDIVISION , WriteBehavior ,
1213} ;
1314use relay_event_schema:: protocol:: { AttributeType , Attributes , BrowserContext , Geo } ;
14- use relay_protocol:: { Annotated , ErrorKind , Value } ;
15+ use relay_protocol:: { Annotated , ErrorKind , Meta , Remark , RemarkType , Value } ;
1516
1617use crate :: { ClientHints , FromUserAgentInfo as _, RawUserAgentInfo } ;
1718
@@ -164,6 +165,60 @@ pub fn normalize_user_geo(
164165 attributes. insert_if_missing ( USER_GEO_REGION , || geo. region ) ;
165166}
166167
168+ /// Normalizes deprecated attributes according to `sentry-conventions`.
169+ ///
170+ /// Attributes with a status of `"normalize"` will be moved to their replacement name.
171+ /// If there is already a value present under the replacement name, it will be left alone,
172+ /// but the deprecated attribute is removed anyway.
173+ ///
174+ /// Attributes with a status of `"backfill"` will be copied to their replacement name if the
175+ /// replacement name is not present. In any case, the original name is left alone.
176+ pub fn normalize_attribute_names ( attributes : & mut Annotated < Attributes > ) {
177+ normalize_attribute_names_inner ( attributes, relay_conventions:: attribute_info)
178+ }
179+
180+ fn normalize_attribute_names_inner (
181+ attributes : & mut Annotated < Attributes > ,
182+ attribute_info : fn ( & str ) -> Option < & ' static AttributeInfo > ,
183+ ) {
184+ let Some ( attributes) = attributes. value_mut ( ) else {
185+ return ;
186+ } ;
187+
188+ let attribute_names: Vec < _ > = attributes. keys ( ) . cloned ( ) . collect ( ) ;
189+
190+ for name in attribute_names {
191+ let Some ( attribute_info) = attribute_info ( & name) else {
192+ continue ;
193+ } ;
194+
195+ match attribute_info. write_behavior {
196+ WriteBehavior :: CurrentName => continue ,
197+ WriteBehavior :: NewName ( new_name) => {
198+ let Some ( old_attribute) = attributes. get_raw_mut ( & name) else {
199+ continue ;
200+ } ;
201+
202+ let mut meta = Meta :: default ( ) ;
203+ // TODO: Possibly add a new RemarkType for "renamed/moved"
204+ meta. add_remark ( Remark :: new ( RemarkType :: Removed , "attribute.deprecated" ) ) ;
205+ let new_attribute = std:: mem:: replace ( old_attribute, Annotated ( None , meta) ) ;
206+
207+ if !attributes. contains_key ( new_name) {
208+ attributes. insert_raw ( new_name. to_owned ( ) , new_attribute) ;
209+ }
210+ }
211+ WriteBehavior :: BothNames ( new_name) => {
212+ if !attributes. contains_key ( new_name)
213+ && let Some ( current_attribute) = attributes. get_raw ( & name) . cloned ( )
214+ {
215+ attributes. insert_raw ( new_name. to_owned ( ) , current_attribute) ;
216+ }
217+ }
218+ }
219+ }
220+ }
221+
167222#[ cfg( test) ]
168223mod tests {
169224 use relay_protocol:: SerializableAnnotated ;
@@ -206,14 +261,14 @@ mod tests {
206261 DateTime :: from_timestamp_nanos ( 1_234_201_337 ) ,
207262 ) ;
208263
209- insta:: assert_json_snapshot!( SerializableAnnotated ( & attributes) , @r#"
264+ insta:: assert_json_snapshot!( SerializableAnnotated ( & attributes) , @r### "
210265 {
211266 "sentry.observed_timestamp_nanos": {
212267 "type": "string",
213268 "value": "111222333"
214269 }
215270 }
216- "# ) ;
271+ "### ) ;
217272 }
218273
219274 #[ test]
@@ -496,4 +551,117 @@ mod tests {
496551 "# ,
497552 ) ;
498553 }
554+
555+ #[ test]
556+ fn test_normalize_attributes ( ) {
557+ fn mock_attribute_info ( name : & str ) -> Option < & ' static AttributeInfo > {
558+ use relay_conventions:: Pii ;
559+
560+ match name {
561+ "replace.empty" => Some ( & AttributeInfo {
562+ write_behavior : WriteBehavior :: NewName ( "replaced" ) ,
563+ pii : Pii :: Maybe ,
564+ aliases : & [ "replaced" ] ,
565+ } ) ,
566+ "replace.existing" => Some ( & AttributeInfo {
567+ write_behavior : WriteBehavior :: NewName ( "not.replaced" ) ,
568+ pii : Pii :: Maybe ,
569+ aliases : & [ "not.replaced" ] ,
570+ } ) ,
571+ "backfill.empty" => Some ( & AttributeInfo {
572+ write_behavior : WriteBehavior :: BothNames ( "backfilled" ) ,
573+ pii : Pii :: Maybe ,
574+ aliases : & [ "backfilled" ] ,
575+ } ) ,
576+ "backfill.existing" => Some ( & AttributeInfo {
577+ write_behavior : WriteBehavior :: BothNames ( "not.backfilled" ) ,
578+ pii : Pii :: Maybe ,
579+ aliases : & [ "not.backfilled" ] ,
580+ } ) ,
581+ _ => None ,
582+ }
583+ }
584+
585+ let mut attributes = Annotated :: new ( Attributes :: from ( [
586+ (
587+ "replace.empty" . to_owned ( ) ,
588+ Annotated :: new ( "Should be moved" . to_owned ( ) . into ( ) ) ,
589+ ) ,
590+ (
591+ "replace.existing" . to_owned ( ) ,
592+ Annotated :: new ( "Should be removed" . to_owned ( ) . into ( ) ) ,
593+ ) ,
594+ (
595+ "not.replaced" . to_owned ( ) ,
596+ Annotated :: new ( "Should be left alone" . to_owned ( ) . into ( ) ) ,
597+ ) ,
598+ (
599+ "backfill.empty" . to_owned ( ) ,
600+ Annotated :: new ( "Should be copied" . to_owned ( ) . into ( ) ) ,
601+ ) ,
602+ (
603+ "backfill.existing" . to_owned ( ) ,
604+ Annotated :: new ( "Should be left alone" . to_owned ( ) . into ( ) ) ,
605+ ) ,
606+ (
607+ "not.backfilled" . to_owned ( ) ,
608+ Annotated :: new ( "Should be left alone" . to_owned ( ) . into ( ) ) ,
609+ ) ,
610+ ] ) ) ;
611+
612+ normalize_attribute_names_inner ( & mut attributes, mock_attribute_info) ;
613+
614+ insta:: assert_json_snapshot!( SerializableAnnotated ( & attributes) , @r###"
615+ {
616+ "backfill.empty": {
617+ "type": "string",
618+ "value": "Should be copied"
619+ },
620+ "backfill.existing": {
621+ "type": "string",
622+ "value": "Should be left alone"
623+ },
624+ "backfilled": {
625+ "type": "string",
626+ "value": "Should be copied"
627+ },
628+ "not.backfilled": {
629+ "type": "string",
630+ "value": "Should be left alone"
631+ },
632+ "not.replaced": {
633+ "type": "string",
634+ "value": "Should be left alone"
635+ },
636+ "replace.empty": null,
637+ "replace.existing": null,
638+ "replaced": {
639+ "type": "string",
640+ "value": "Should be moved"
641+ },
642+ "_meta": {
643+ "replace.empty": {
644+ "": {
645+ "rem": [
646+ [
647+ "attribute.deprecated",
648+ "x"
649+ ]
650+ ]
651+ }
652+ },
653+ "replace.existing": {
654+ "": {
655+ "rem": [
656+ [
657+ "attribute.deprecated",
658+ "x"
659+ ]
660+ ]
661+ }
662+ }
663+ }
664+ }
665+ "### ) ;
666+ }
499667}
0 commit comments