Skip to content

Commit b2583bb

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 e03d3c9 commit b2583bb

3 files changed

Lines changed: 173 additions & 2 deletions

File tree

src/Packages/Audience/Runtime/ImmutableAudience.cs

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

54+
// True once Init() has finished setting things up. False before
55+
// Init() and after Shutdown().
56+
public static bool Initialized => _initialized;
57+
58+
// Persisted value, which may differ from AudienceConfig.Consent if
59+
// a prior session changed it.
60+
public static ConsentLevel CurrentConsent => _state.Level;
61+
62+
// The user ID from the most recent Identify() call. Becomes null
63+
// after Reset(), and also when consent drops from Full down to
64+
// Anonymous or None (since anonymous tracking must not know who
65+
// the user is).
66+
public static string? UserId => _state.UserId;
67+
68+
// Display-only — Reset and SetConsent(None) wipe it, so it is not
69+
// a stable identifier across sessions.
70+
public static string? AnonymousId
71+
{
72+
get
73+
{
74+
if (!_initialized) return null;
75+
var config = _config;
76+
if (config == null || !_state.Level.CanTrack()) return null;
77+
// PersistentDataPath is validated non-null in Init; compiler can't propagate that.
78+
return Identity.Get(config.PersistentDataPath!);
79+
}
80+
}
81+
82+
// The current session's ID. A new ID is assigned each time a
83+
// session starts: at Init(), at Reset(), and when the app resumes
84+
// from the background after the previous session has timed out.
85+
// Null while consent is None — granting consent starts a fresh
86+
// session.
87+
public static string? SessionId => _session?.SessionId;
88+
89+
// Memory + disk counts are read without holding the drain lock, so
90+
// the sum can drift by a few events.
91+
public static int QueueSize
92+
{
93+
get
94+
{
95+
// Fence off the volatile _initialized load first, matching
96+
// the protocol documented on the reference fields. Without
97+
// this, a weak-memory-order reader could observe
98+
// _initialized=true but _queue/_store still null — the ?.
99+
// short-circuits to 0 in that case, but the inconsistency
100+
// would break the protocol the file claims to follow.
101+
if (!_initialized) return 0;
102+
var queue = _queue;
103+
var store = _store;
104+
var memory = queue?.InMemoryCount ?? 0;
105+
var disk = store?.Count() ?? 0;
106+
return memory + disk;
107+
}
108+
}
109+
54110
// Starts the SDK. Call once at launch.
55111
public static void Init(AudienceConfig config)
56112
{
@@ -719,8 +775,6 @@ internal static void ResetState()
719775
}
720776
}
721777

722-
internal static ConsentLevel CurrentConsent => _state.Level;
723-
724778
internal static void FlushQueueToDiskForTesting() => _queue?.FlushSync();
725779

726780
// 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
@@ -59,6 +59,117 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
5959
}
6060
}
6161

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

0 commit comments

Comments
 (0)