Skip to content

Commit 2aaf8ca

Browse files
feat(audience-diagnostics): expose SDK state as public API
Adds six public getters on ImmutableAudience so studios can build debug HUDs, settings-screen displays, and integration tests without reaching into internals: - Initialized: true between Init and Shutdown - CurrentConsent: live consent level, reflects any SetConsent - UserId: last Identify value, null below Full consent or pre-Identify - AnonymousId: persisted id when consent allows tracking, null otherwise - SessionId: current session id, null when no session is active - QueueSize: in-memory plus on-disk event count awaiting send Every getter is safe to call any time — before Init, after Shutdown, from any thread — and returns a safe default when the SDK cannot answer. Values are snapshots that can drift a tick against the background drain thread; fine for display, do not use for invariants. EventQueue gains an internal InMemoryCount property so QueueSize can sum memory and disk without locking the drain thread. Tests cover every getter's lifecycle edges: pre-Init defaults, Init/Shutdown flips, consent downgrade effects, Reset clearing UserId, and QueueSize growing across Init's session_start/game_launch plus an explicit Track. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ae6780f commit 2aaf8ca

3 files changed

Lines changed: 166 additions & 2 deletions

File tree

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,55 @@ public static class ImmutableAudience
5252
// assignments from SetConsent without taking _initLock.
5353
private static volatile Session? _session;
5454

55+
// Diagnostic getters. Safe from any thread, any time — return a
56+
// zero-value default (false / null / 0 / ConsentLevel.None) outside
57+
// Init..Shutdown. Values are tick-level snapshots, not invariants.
58+
59+
public static bool Initialized => _initialized;
60+
61+
// Persisted value, which may differ from AudienceConfig.Consent if
62+
// a prior session changed it.
63+
public static ConsentLevel CurrentConsent => _state.Level;
64+
65+
public static string? UserId => _state.UserId;
66+
67+
// Display-only — Reset and SetConsent(None) wipe it, so it is not
68+
// a stable identifier across sessions.
69+
public static string? AnonymousId
70+
{
71+
get
72+
{
73+
if (!_initialized) return null;
74+
var config = _config;
75+
if (config == null || !_state.Level.CanTrack()) return null;
76+
// PersistentDataPath is validated non-null in Init; compiler can't propagate that.
77+
return Identity.Get(config.PersistentDataPath!);
78+
}
79+
}
80+
81+
public static string? SessionId => _session?.SessionId;
82+
83+
// Memory + disk counts are read without holding the drain lock, so
84+
// the sum can drift by a few events.
85+
public static int QueueSize
86+
{
87+
get
88+
{
89+
// Fence off the volatile _initialized load first, matching
90+
// the protocol documented on the reference fields. Without
91+
// this, a weak-memory-order reader could observe
92+
// _initialized=true but _queue/_store still null — the ?.
93+
// short-circuits to 0 in that case, but the inconsistency
94+
// would break the protocol the file claims to follow.
95+
if (!_initialized) return 0;
96+
var queue = _queue;
97+
var store = _store;
98+
var memory = queue?.InMemoryCount ?? 0;
99+
var disk = store?.Count() ?? 0;
100+
return memory + disk;
101+
}
102+
}
103+
55104
// Starts the SDK. Call once at launch.
56105
public static void Init(AudienceConfig config)
57106
{
@@ -720,8 +769,6 @@ internal static void ResetState()
720769
}
721770
}
722771

723-
internal static ConsentLevel CurrentConsent => _state.Level;
724-
725772
internal static void FlushQueueToDiskForTesting() => _queue?.FlushSync();
726773

727774
// Drives SendBatch without a real timer so the overlapping-tick guard is testable.

src/Packages/Audience/Runtime/Transport/EventQueue.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ internal EventQueue(DiskStore store, int flushIntervalSeconds, int flushSize)
4949
_drainThread.Start();
5050
}
5151

52+
// Approximate count of events currently in the in-memory queue
53+
// awaiting drain to disk. Lock-free read on ConcurrentQueue.Count
54+
// — a snapshot that can race with concurrent enqueue / dequeue.
55+
// Good enough for status-panel display; not an invariant.
56+
internal int InMemoryCount => _memory.Count;
57+
5258
// Enqueues a message dictionary. Lock-free; safe from any thread.
5359
// The dictionary is not copied -- callers must not mutate it after
5460
// enqueue. Serialisation happens on the drain thread so Track() stays

src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,117 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
6060
}
6161
}
6262

63+
// -----------------------------------------------------------------
64+
// Diagnostic getters (Initialized / CurrentConsent / UserId /
65+
// AnonymousId / SessionId / QueueSize)
66+
// -----------------------------------------------------------------
67+
68+
[Test]
69+
public void Initialized_FlipsAroundInitAndShutdown()
70+
{
71+
Assert.IsFalse(ImmutableAudience.Initialized,
72+
"Initialized should be false before Init");
73+
74+
ImmutableAudience.Init(MakeConfig());
75+
Assert.IsTrue(ImmutableAudience.Initialized,
76+
"Initialized should flip true after Init");
77+
78+
ImmutableAudience.Shutdown();
79+
Assert.IsFalse(ImmutableAudience.Initialized,
80+
"Initialized should flip back to false after Shutdown");
81+
}
82+
83+
[Test]
84+
public void CurrentConsent_ReflectsLatestSetConsent()
85+
{
86+
ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
87+
Assert.AreEqual(ConsentLevel.Anonymous, ImmutableAudience.CurrentConsent);
88+
89+
ImmutableAudience.SetConsent(ConsentLevel.Full);
90+
Assert.AreEqual(ConsentLevel.Full, ImmutableAudience.CurrentConsent);
91+
92+
ImmutableAudience.SetConsent(ConsentLevel.None);
93+
Assert.AreEqual(ConsentLevel.None, ImmutableAudience.CurrentConsent);
94+
}
95+
96+
[Test]
97+
public void UserId_Uninitialised_ReturnsNull()
98+
{
99+
Assert.IsNull(ImmutableAudience.UserId);
100+
}
101+
102+
[Test]
103+
public void UserId_AfterIdentifyAndReset_TracksState()
104+
{
105+
ImmutableAudience.Init(MakeConfig(ConsentLevel.Full));
106+
Assert.IsNull(ImmutableAudience.UserId,
107+
"UserId should be null until Identify is called");
108+
109+
ImmutableAudience.Identify("player-42", IdentityType.Custom);
110+
Assert.AreEqual("player-42", ImmutableAudience.UserId,
111+
"UserId must reflect the most recent Identify call");
112+
113+
ImmutableAudience.Reset();
114+
Assert.IsNull(ImmutableAudience.UserId,
115+
"Reset must clear UserId so the next player is not attributed to the previous one");
116+
}
117+
118+
[Test]
119+
public void AnonymousId_ConsentNone_ReturnsNull()
120+
{
121+
// Anonymous identifier is consent-gated: below tracking consent,
122+
// no stable id should leak through the getter.
123+
ImmutableAudience.Init(MakeConfig(ConsentLevel.None));
124+
125+
Assert.IsNull(ImmutableAudience.AnonymousId);
126+
}
127+
128+
[Test]
129+
public void AnonymousId_ConsentAnonymous_ReturnsPersistedId()
130+
{
131+
ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
132+
// Track once so Identity.GetOrCreate runs and writes the id file.
133+
ImmutableAudience.Track("warmup_event");
134+
135+
var id = ImmutableAudience.AnonymousId;
136+
Assert.IsFalse(string.IsNullOrEmpty(id),
137+
"AnonymousId should return the persisted id once tracking has created one");
138+
}
139+
140+
[Test]
141+
public void SessionId_MirrorsSessionLifecycle()
142+
{
143+
Assert.IsNull(ImmutableAudience.SessionId,
144+
"SessionId should be null before Init");
145+
146+
ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
147+
Assert.IsFalse(string.IsNullOrEmpty(ImmutableAudience.SessionId),
148+
"SessionId should be non-null once Init creates a session");
149+
150+
ImmutableAudience.Shutdown();
151+
Assert.IsNull(ImmutableAudience.SessionId,
152+
"SessionId should be null after Shutdown disposes the session");
153+
}
154+
155+
[Test]
156+
public void QueueSize_ZeroBeforeInit_GrowsWithEnqueue()
157+
{
158+
Assert.AreEqual(0, ImmutableAudience.QueueSize,
159+
"QueueSize should be 0 before Init");
160+
161+
ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
162+
// Init enqueues session_start + game_launch; those stay
163+
// in-memory until a flush. QueueSize sums memory + disk so the
164+
// pre-flush snapshot must be > 0.
165+
var afterInit = ImmutableAudience.QueueSize;
166+
Assert.Greater(afterInit, 0,
167+
"QueueSize should include session_start and game_launch after Init");
168+
169+
ImmutableAudience.Track("explicit_track_event");
170+
Assert.Greater(ImmutableAudience.QueueSize, afterInit,
171+
"QueueSize should grow when a new event is enqueued");
172+
}
173+
63174
// -----------------------------------------------------------------
64175
// Unity context provider
65176
// -----------------------------------------------------------------

0 commit comments

Comments
 (0)