@@ -333,6 +333,7 @@ private string GetPluginDirectoryPath()
333333 const string pluginDir = "Plugins" ;
334334 return Path . Combine ( Application . dataPath , pluginDir ) ;
335335 }
336+
336337 /// <summary>
337338 /// Generate path to Crashpad handler binary
338339 /// </summary>
@@ -367,12 +368,27 @@ internal static void CleanScopedAttributes()
367368 {
368369 return ;
369370 }
370- var attributes = JsonUtility . FromJson < ScopedAttributesContainer > ( attributesJson ) ;
371- foreach ( var attributeKey in attributes . Keys )
371+
372+ try
373+ {
374+ var attributes = JsonUtility . FromJson < ScopedAttributesContainer > ( attributesJson ) ;
375+ if ( attributes ? . Keys != null )
376+ {
377+ foreach ( var attributeKey in attributes . Keys )
378+ {
379+ PlayerPrefs . DeleteKey ( string . Format ( ScopedAttributesPattern , attributeKey ) ) ;
380+ }
381+ }
382+ }
383+ catch ( Exception e )
372384 {
373- PlayerPrefs . DeleteKey ( string . Format ( ScopedAttributesPattern , attributeKey ) ) ;
385+ Debug . LogWarning ( $ "Backtrace Failed to parse scoped attributes for cleanup: { e . Message } ") ;
386+ }
387+ finally
388+ {
389+ PlayerPrefs . DeleteKey ( ScopedAttributeListKey ) ;
390+ PlayerPrefs . Save ( ) ;
374391 }
375- PlayerPrefs . DeleteKey ( ScopedAttributeListKey ) ;
376392 }
377393
378394 internal static IDictionary < string , string > GetScopedAttributes ( )
@@ -382,8 +398,25 @@ internal static IDictionary<string, string> GetScopedAttributes()
382398 {
383399 return new Dictionary < string , string > ( ) ;
384400 }
385- var result = new Dictionary < string , string > ( ) ;
386- var attributes = JsonUtility . FromJson < ScopedAttributesContainer > ( attributesJson ) ;
401+
402+ var result = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
403+ ScopedAttributesContainer attributes = null ;
404+
405+ try
406+ {
407+ attributes = JsonUtility . FromJson < ScopedAttributesContainer > ( attributesJson ) ;
408+ }
409+ catch ( Exception e )
410+ {
411+ Debug . LogWarning ( $ "Backtrace Failed to parse scoped attributes at read: { e . Message } ") ;
412+ return result ;
413+ }
414+
415+ if ( attributes ? . Keys == null )
416+ {
417+ return result ;
418+ }
419+
387420 foreach ( var attributeKey in attributes . Keys )
388421 {
389422 var value = PlayerPrefs . GetString ( string . Format ( ScopedAttributesPattern , attributeKey ) , string . Empty ) ;
@@ -411,7 +444,7 @@ internal static IDictionary<string, string> GetScopedAttributes()
411444 }
412445
413446 /// <summary>
414- /// Adds attributes to scoped registry and to native clietn
447+ /// Adds attributes to scoped registry and to native client
415448 /// </summary>
416449 /// <param name="key">attribute key</param>
417450 /// <param name="value">attribute value</param>
@@ -424,23 +457,103 @@ private void AddAttributes(string key, string value)
424457 AddScopedAttribute ( key , value ) ;
425458 }
426459
460+ /// <summary>Load a deduped set of scoped attribute keys from PlayerPrefs.</summary>
461+ private HashSet < string > LoadScopedKeys ( )
462+ {
463+ var keys = new HashSet < string > ( StringComparer . Ordinal ) ;
464+ var attributesJson = PlayerPrefs . GetString ( ScopedAttributeListKey ) ;
465+ if ( HasScopedAttributesEmpty ( attributesJson ) )
466+ {
467+ try
468+ {
469+ var container = JsonUtility . FromJson < ScopedAttributesContainer > ( attributesJson ) ;
470+ if ( container ? . Keys != null && container . Keys . Count > 0 )
471+ {
472+ foreach ( var k in container . Keys )
473+ {
474+ if ( ! string . IsNullOrEmpty ( k ) )
475+ {
476+ keys . Add ( k ) ;
477+ }
478+ }
479+ }
480+ }
481+ catch ( Exception e )
482+ {
483+ Debug . LogWarning ( $ "Backtrace Failed to parse scoped attributes list. { e . Message } ") ;
484+ keys . Clear ( ) ;
485+ }
486+ }
487+ return keys ;
488+ }
489+
490+ /// <summary>Persist a deduped list of keys back to PlayerPrefs.</summary>
491+ private void SaveScopedKeys ( HashSet < string > keys )
492+ {
493+ var container = new ScopedAttributesContainer
494+ {
495+ Keys = keys . OrderBy ( k => k , StringComparer . Ordinal ) . ToList ( )
496+ } ;
497+ PlayerPrefs . SetString ( ScopedAttributeListKey , JsonUtility . ToJson ( container ) ) ;
498+ PlayerPrefs . Save ( ) ;
499+ }
500+
501+ /// <summary>Return the currently stored "value" for a scoped key or "null" if missing.</summary>
502+ private string GetScopedValue ( string key )
503+ {
504+ var prefsKey = string . Format ( ScopedAttributesPattern , key ) ;
505+ return PlayerPrefs . HasKey ( prefsKey ) ? PlayerPrefs . GetString ( prefsKey ) : null ;
506+ }
507+
508+ /// <summary>Set the stored value for a scoped key.</summary>
509+ private void SetScopedValue ( string key , string value )
510+ {
511+ PlayerPrefs . SetString ( string . Format ( ScopedAttributesPattern , key ) , value ?? string . Empty ) ;
512+ }
513+
427514 /// <summary>
428515 /// Adds dictionary of attributes to player prefs for windows crashes captured by unity crash handler
429516 /// </summary>
430- /// <param name="atributes ">Attributes</param>
517+ /// <param name="attributes ">Attributes</param>
431518 internal void AddScopedAttributes ( IDictionary < string , string > attributes )
432519 {
433520 if ( ! _configuration . SendUnhandledGameCrashesOnGameStartup )
434521 {
435522 return ;
436523 }
437- var attributesContainer = new ScopedAttributesContainer ( ) ;
438- foreach ( var attribute in attributes )
524+ if ( attributes == null || attributes . Count == 0 )
439525 {
440- attributesContainer . Keys . Add ( attribute . Key ) ;
441- PlayerPrefs . SetString ( string . Format ( ScopedAttributesPattern , attribute . Key ) , attribute . Value ) ;
526+ return ;
442527 }
443- PlayerPrefs . SetString ( ScopedAttributeListKey , JsonUtility . ToJson ( attributesContainer ) ) ;
528+
529+ var keys = LoadScopedKeys ( ) ;
530+ bool listChanged = false ;
531+
532+ foreach ( var kv in attributes )
533+ {
534+ var key = kv . Key ;
535+ var value = kv . Value ?? string . Empty ;
536+
537+ // skip when unchanged
538+ var current = GetScopedValue ( key ) ;
539+ if ( current == null || ! string . Equals ( current , value , StringComparison . Ordinal ) )
540+ {
541+ SetScopedValue ( key , value ) ;
542+ }
543+
544+ // deduplication
545+ if ( keys . Add ( key ) )
546+ {
547+ listChanged = true ;
548+ }
549+ }
550+
551+ if ( listChanged )
552+ {
553+ SaveScopedKeys ( keys ) ;
554+ }
555+
556+ PlayerPrefs . Save ( ) ;
444557 }
445558
446559 /// <summary>
@@ -454,14 +567,29 @@ private void AddScopedAttribute(string key, string value)
454567 {
455568 return ;
456569 }
457- var attributesJson = PlayerPrefs . GetString ( ScopedAttributeListKey ) ;
458- var attributes = HasScopedAttributesEmpty ( attributesJson )
459- ? JsonUtility . FromJson < ScopedAttributesContainer > ( attributesJson )
460- : new ScopedAttributesContainer ( ) ;
570+ if ( string . IsNullOrEmpty ( key ) )
571+ {
572+ return ;
573+ }
574+
575+ var normalized = value ?? string . Empty ;
576+
577+ // skip when unchanged
578+ var current = GetScopedValue ( key ) ;
579+ if ( current != null && string . Equals ( current , normalized , StringComparison . Ordinal ) )
580+ {
581+ return ;
582+ }
583+
584+ SetScopedValue ( key , normalized ) ;
585+
586+ var keys = LoadScopedKeys ( ) ;
587+ if ( keys . Add ( key ) )
588+ {
589+ SaveScopedKeys ( keys ) ;
590+ }
461591
462- attributes . Keys . Add ( key ) ;
463- PlayerPrefs . SetString ( ScopedAttributeListKey , JsonUtility . ToJson ( attributes ) ) ;
464- PlayerPrefs . SetString ( string . Format ( ScopedAttributesPattern , key ) , value ) ;
592+ PlayerPrefs . Save ( ) ;
465593 }
466594
467595 private static bool HasScopedAttributesEmpty ( string attributesJson )
0 commit comments