@@ -1302,7 +1302,7 @@ public void Init_GameLaunch_IncludesAttStatusAndIdfa_WhenContextProviderReturns(
13021302 [ "attStatus" ] = "authorized" ,
13031303 [ "idfa" ] = "11111111-2222-3333-4444-555555555555" ,
13041304 } ;
1305- var config = MakeConfig ( ) ;
1305+ var config = MakeConfig ( ConsentLevel . Full ) ;
13061306 config . EnableMobileAttribution = true ;
13071307 ImmutableAudience . Init ( config ) ;
13081308 ImmutableAudience . Shutdown ( ) ;
@@ -1423,7 +1423,7 @@ public void Init_GameLaunch_IncludesGaidAndLimitFlag_WhenContextProviderReturns(
14231423 [ "gaid" ] = "abcdef01-2345-6789-abcd-ef0123456789" ,
14241424 [ "gaidLimitAdTracking" ] = false ,
14251425 } ;
1426- var config = MakeConfig ( ) ;
1426+ var config = MakeConfig ( ConsentLevel . Full ) ;
14271427 config . EnableMobileAttribution = true ;
14281428 ImmutableAudience . Init ( config ) ;
14291429 ImmutableAudience . Shutdown ( ) ;
@@ -1459,6 +1459,64 @@ public void Init_GameLaunch_OmitsGaid_WhenUserOptedOut()
14591459 "gaid must not appear when the user has opted out" ) ;
14601460 }
14611461
1462+ // -----------------------------------------------------------------
1463+ // Consent-tier tightening: idfa, gaid => Full-only
1464+ //
1465+ // idfa and gaid are cross-app device identifiers — same privacy class
1466+ // as userId. They ship only when consent is Full. State-class keys
1467+ // (attStatus, gaidLimitAdTracking) are non-identifying and ship at
1468+ // Anonymous+Full (CanTrack).
1469+ // -----------------------------------------------------------------
1470+
1471+ [ Test ]
1472+ public void Init_GameLaunch_StripsIdfa_WhenConsentAnonymous ( )
1473+ {
1474+ ImmutableAudience . MobileAttributionContextProvider = ( ) =>
1475+ new Dictionary < string , object >
1476+ {
1477+ [ "attStatus" ] = "authorized" ,
1478+ [ "idfa" ] = "11111111-2222-3333-4444-555555555555" ,
1479+ } ;
1480+ var config = MakeConfig ( ConsentLevel . Anonymous ) ;
1481+ config . EnableMobileAttribution = true ;
1482+ ImmutableAudience . Init ( config ) ;
1483+ ImmutableAudience . Shutdown ( ) ;
1484+
1485+ var launchFile = Directory . GetFiles ( AudiencePaths . QueueDir ( _testDir ) , "*.json" )
1486+ . Select ( File . ReadAllText )
1487+ . First ( c => c . Contains ( "\" game_launch\" " ) ) ;
1488+ StringAssert . Contains ( "\" attStatus\" :\" authorized\" " , launchFile ,
1489+ "attStatus must ship at Anonymous — it is non-identifying state" ) ;
1490+ Assert . IsFalse ( launchFile . Contains ( "\" idfa\" " ) ,
1491+ "idfa must not ship at Anonymous — it is a cross-app device identifier" ) ;
1492+ }
1493+
1494+ [ Test ]
1495+ public void Init_GameLaunch_StripsGaid_WhenConsentAnonymous ( )
1496+ {
1497+ // gaid is stripped at Anonymous; gaidLimitAdTracking is non-identifying
1498+ // state and must still ship so the pipeline can distinguish
1499+ // "fetched, opted out" from "not fetched yet".
1500+ ImmutableAudience . MobileAttributionContextProvider = ( ) =>
1501+ new Dictionary < string , object >
1502+ {
1503+ [ "gaid" ] = "abcdef01-2345-6789-abcd-ef0123456789" ,
1504+ [ "gaidLimitAdTracking" ] = false ,
1505+ } ;
1506+ var config = MakeConfig ( ConsentLevel . Anonymous ) ;
1507+ config . EnableMobileAttribution = true ;
1508+ ImmutableAudience . Init ( config ) ;
1509+ ImmutableAudience . Shutdown ( ) ;
1510+
1511+ var launchFile = Directory . GetFiles ( AudiencePaths . QueueDir ( _testDir ) , "*.json" )
1512+ . Select ( File . ReadAllText )
1513+ . First ( c => c . Contains ( "\" game_launch\" " ) ) ;
1514+ StringAssert . Contains ( "\" gaidLimitAdTracking\" :false" , launchFile ,
1515+ "gaidLimitAdTracking must ship at Anonymous — it is non-identifying state" ) ;
1516+ Assert . IsFalse ( launchFile . Contains ( "\" gaid\" " ) ,
1517+ "gaid must not ship at Anonymous — it is a cross-app device identifier" ) ;
1518+ }
1519+
14621520 // -----------------------------------------------------------------
14631521 // install_referrer_received
14641522 //
@@ -1473,7 +1531,7 @@ public void Init_FiresInstallReferrerReceived_WhenProviderReturnsReferrer()
14731531 {
14741532 ImmutableAudience . MobileInstallReferrerProvider = ( ) =>
14751533 "utm_source=google-play&utm_medium=organic" ;
1476- var config = MakeConfig ( ) ;
1534+ var config = MakeConfig ( ConsentLevel . Full ) ;
14771535 config . EnableMobileAttribution = true ;
14781536 ImmutableAudience . Init ( config ) ;
14791537 ImmutableAudience . Shutdown ( ) ;
@@ -1492,7 +1550,7 @@ public void Init_GameLaunch_NeverIncludesInstallReferrer()
14921550 // installReferrer is exclusively on the dedicated event; ensure
14931551 // we don't regress and start leaking it onto game_launch.
14941552 ImmutableAudience . MobileInstallReferrerProvider = ( ) => "utm_source=test" ;
1495- var config = MakeConfig ( ) ;
1553+ var config = MakeConfig ( ConsentLevel . Full ) ;
14961554 config . EnableMobileAttribution = true ;
14971555 ImmutableAudience . Init ( config ) ;
14981556 ImmutableAudience . Shutdown ( ) ;
@@ -1544,7 +1602,7 @@ public void Init_DoesNotFireInstallReferrerReceived_WhenAlreadyFired()
15441602 // Simulate the second launch: cache is populated, marker is set
15451603 // by the previous Init. Event must not refire.
15461604 ImmutableAudience . MobileInstallReferrerProvider = ( ) => "utm_source=test" ;
1547- var config = MakeConfig ( ) ;
1605+ var config = MakeConfig ( ConsentLevel . Full ) ;
15481606 config . EnableMobileAttribution = true ;
15491607
15501608 ImmutableAudience . Init ( config ) ;
@@ -1554,7 +1612,7 @@ public void Init_DoesNotFireInstallReferrerReceived_WhenAlreadyFired()
15541612 var queueDir = AudiencePaths . QueueDir ( _testDir ) ;
15551613 foreach ( var f in Directory . GetFiles ( queueDir , "*.json" ) ) File . Delete ( f ) ;
15561614
1557- var config2 = MakeConfig ( ) ;
1615+ var config2 = MakeConfig ( ConsentLevel . Full ) ;
15581616 config2 . EnableMobileAttribution = true ;
15591617 ImmutableAudience . Init ( config2 ) ;
15601618 ImmutableAudience . Shutdown ( ) ;
@@ -1598,7 +1656,7 @@ public void Init_FiresInstallReferrerReceived_OnSecondLaunch_WhenFirstMissedCach
15981656 ImmutableAudience . MobileInstallReferrerProvider = ( ) =>
15991657 ++ callCount == 1 ? firstCallReturn : secondCallReturn ;
16001658
1601- var config = MakeConfig ( ) ;
1659+ var config = MakeConfig ( ConsentLevel . Full ) ;
16021660 config . EnableMobileAttribution = true ;
16031661 ImmutableAudience . Init ( config ) ;
16041662 ImmutableAudience . Shutdown ( ) ;
@@ -1612,7 +1670,7 @@ public void Init_FiresInstallReferrerReceived_OnSecondLaunch_WhenFirstMissedCach
16121670
16131671 foreach ( var f in Directory . GetFiles ( queueDir , "*.json" ) ) File . Delete ( f ) ;
16141672
1615- var config2 = MakeConfig ( ) ;
1673+ var config2 = MakeConfig ( ConsentLevel . Full ) ;
16161674 config2 . EnableMobileAttribution = true ;
16171675 ImmutableAudience . Init ( config2 ) ;
16181676 ImmutableAudience . Shutdown ( ) ;
@@ -1643,6 +1701,62 @@ public void Init_InstallReferrerProviderThrows_DoesNotPreventGameLaunch()
16431701 Assert . IsFalse ( blobs . Any ( c => c . Contains ( "\" install_referrer_received\" " ) ) ) ;
16441702 }
16451703
1704+ [ Test ]
1705+ public void Init_DoesNotFireInstallReferrerReceived_WhenConsentAnonymous ( )
1706+ {
1707+ // installReferrer encodes campaign attribution source — Full-only.
1708+ // The sent marker must NOT be written so a later upgrade to Full
1709+ // can fire the event.
1710+ ImmutableAudience . MobileInstallReferrerProvider = ( ) => "utm_source=google-play" ;
1711+ var config = MakeConfig ( ConsentLevel . Anonymous ) ;
1712+ config . EnableMobileAttribution = true ;
1713+ ImmutableAudience . Init ( config ) ;
1714+ ImmutableAudience . Shutdown ( ) ;
1715+
1716+ var blobs = Directory . GetFiles ( AudiencePaths . QueueDir ( _testDir ) , "*.json" )
1717+ . Select ( File . ReadAllText ) . ToList ( ) ;
1718+ Assert . IsFalse ( blobs . Any ( c => c . Contains ( "\" install_referrer_received\" " ) ) ,
1719+ "install_referrer_received must not fire when consent is Anonymous" ) ;
1720+ Assert . IsFalse ( File . Exists ( AudiencePaths . InstallReferrerSentFile ( _testDir ) ) ,
1721+ "sent marker must not be written at Anonymous so a Full upgrade can fire the event" ) ;
1722+ }
1723+
1724+ [ Test ]
1725+ public void Init_FiresInstallReferrerReceived_AfterConsentUpgradedToFull ( )
1726+ {
1727+ // First launch at Anonymous: referrer is available but event is
1728+ // gated — no event fires and no sent marker is written.
1729+ // Second launch at Full: event fires and marker is written.
1730+ ImmutableAudience . MobileInstallReferrerProvider = ( ) => "utm_source=upgrade_test" ;
1731+
1732+ var config = MakeConfig ( ConsentLevel . Anonymous ) ;
1733+ config . EnableMobileAttribution = true ;
1734+ ImmutableAudience . Init ( config ) ;
1735+ ImmutableAudience . Shutdown ( ) ;
1736+
1737+ var queueDir = AudiencePaths . QueueDir ( _testDir ) ;
1738+ var firstBlobs = Directory . GetFiles ( queueDir , "*.json" )
1739+ . Select ( File . ReadAllText ) . ToList ( ) ;
1740+ Assert . IsFalse ( firstBlobs . Any ( c => c . Contains ( "\" install_referrer_received\" " ) ) ,
1741+ "event must not ship on first launch when consent is Anonymous" ) ;
1742+ Assert . IsFalse ( File . Exists ( AudiencePaths . InstallReferrerSentFile ( _testDir ) ) ,
1743+ "sent marker must not exist after Anonymous launch" ) ;
1744+
1745+ foreach ( var f in Directory . GetFiles ( queueDir , "*.json" ) ) File . Delete ( f ) ;
1746+
1747+ var config2 = MakeConfig ( ConsentLevel . Full ) ;
1748+ config2 . EnableMobileAttribution = true ;
1749+ ImmutableAudience . Init ( config2 ) ;
1750+ ImmutableAudience . Shutdown ( ) ;
1751+
1752+ var secondBlobs = Directory . GetFiles ( queueDir , "*.json" )
1753+ . Select ( File . ReadAllText ) . ToList ( ) ;
1754+ Assert . IsTrue ( secondBlobs . Any ( c =>
1755+ c . Contains ( "\" install_referrer_received\" " ) &&
1756+ c . Contains ( "\" installReferrer\" :\" utm_source=upgrade_test\" " ) ) ,
1757+ "event must fire on the first Full-consent launch after an Anonymous launch" ) ;
1758+ }
1759+
16461760 // -----------------------------------------------------------------
16471761 // RequestTrackingAuthorizationAsync
16481762 // -----------------------------------------------------------------
0 commit comments