Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions src/Packages/Audience/Runtime/ImmutableAudience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,59 @@ public static class ImmutableAudience
// assignments from SetConsent without taking _initLock.
private static volatile Session? _session;

// Diagnostic getters. Safe from any thread, any time — return a
// zero-value default (false / null / 0 / ConsentLevel.None) outside
// Init..Shutdown. Values are tick-level snapshots, not invariants.

// Flipped true at the end of a successful Init, false by Shutdown.
public static bool Initialized => _initialized;

// Persisted value, which may differ from AudienceConfig.Consent if
// a prior session changed it.
public static ConsentLevel CurrentConsent => _state.Level;

// Last value passed to Identify(). Cleared by Reset and any consent
// downgrade out of Full.
public static string? UserId => _state.UserId;
Comment thread
ImmutableJeffrey marked this conversation as resolved.

// Display-only — Reset and SetConsent(None) wipe it, so it is not
// a stable identifier across sessions.
public static string? AnonymousId
{
get
{
if (!_initialized) return null;
var config = _config;
if (config == null || !_state.Level.CanTrack()) return null;
// PersistentDataPath is validated non-null in Init; compiler can't propagate that.
return Identity.Get(config.PersistentDataPath!);
}
}

// Changes on extended-pause rollover. Also null while consent is None.
public static string? SessionId => _session?.SessionId;

// Memory + disk counts are read without holding the drain lock, so
// the sum can drift by a few events.
public static int QueueSize
{
get
{
// Fence off the volatile _initialized load first, matching
// the protocol documented on the reference fields. Without
// this, a weak-memory-order reader could observe
// _initialized=true but _queue/_store still null — the ?.
// short-circuits to 0 in that case, but the inconsistency
// would break the protocol the file claims to follow.
if (!_initialized) return 0;
var queue = _queue;
var store = _store;
var memory = queue?.InMemoryCount ?? 0;
var disk = store?.Count() ?? 0;
return memory + disk;
}
}

// Starts the SDK. Call once at launch.
public static void Init(AudienceConfig config)
{
Expand Down Expand Up @@ -720,8 +773,6 @@ internal static void ResetState()
}
}

internal static ConsentLevel CurrentConsent => _state.Level;

internal static void FlushQueueToDiskForTesting() => _queue?.FlushSync();

// Drives SendBatch without a real timer so the overlapping-tick guard is testable.
Expand Down
6 changes: 6 additions & 0 deletions src/Packages/Audience/Runtime/Transport/EventQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ internal EventQueue(DiskStore store, int flushIntervalSeconds, int flushSize)
_drainThread.Start();
}

// Approximate count of events currently in the in-memory queue
// awaiting drain to disk. Lock-free read on ConcurrentQueue.Count
// — a snapshot that can race with concurrent enqueue / dequeue.
// Good enough for status-panel display; not an invariant.
internal int InMemoryCount => _memory.Count;

// Enqueues a message dictionary. Lock-free; safe from any thread.
// The dictionary is not copied -- callers must not mutate it after
// enqueue. Serialisation happens on the drain thread so Track() stays
Expand Down
111 changes: 111 additions & 0 deletions src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,117 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
}
}

// -----------------------------------------------------------------
// Diagnostic getters (Initialized / CurrentConsent / UserId /
// AnonymousId / SessionId / QueueSize)
// -----------------------------------------------------------------

[Test]
public void Initialized_FlipsAroundInitAndShutdown()
{
Assert.IsFalse(ImmutableAudience.Initialized,
"Initialized should be false before Init");

ImmutableAudience.Init(MakeConfig());
Assert.IsTrue(ImmutableAudience.Initialized,
"Initialized should flip true after Init");

ImmutableAudience.Shutdown();
Assert.IsFalse(ImmutableAudience.Initialized,
"Initialized should flip back to false after Shutdown");
}

[Test]
public void CurrentConsent_ReflectsLatestSetConsent()
{
ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
Assert.AreEqual(ConsentLevel.Anonymous, ImmutableAudience.CurrentConsent);

ImmutableAudience.SetConsent(ConsentLevel.Full);
Assert.AreEqual(ConsentLevel.Full, ImmutableAudience.CurrentConsent);

ImmutableAudience.SetConsent(ConsentLevel.None);
Assert.AreEqual(ConsentLevel.None, ImmutableAudience.CurrentConsent);
}

[Test]
public void UserId_Uninitialised_ReturnsNull()
{
Assert.IsNull(ImmutableAudience.UserId);
}

[Test]
public void UserId_AfterIdentifyAndReset_TracksState()
{
ImmutableAudience.Init(MakeConfig(ConsentLevel.Full));
Assert.IsNull(ImmutableAudience.UserId,
"UserId should be null until Identify is called");

ImmutableAudience.Identify("player-42", IdentityType.Custom);
Assert.AreEqual("player-42", ImmutableAudience.UserId,
"UserId must reflect the most recent Identify call");

ImmutableAudience.Reset();
Assert.IsNull(ImmutableAudience.UserId,
"Reset must clear UserId so the next player is not attributed to the previous one");
}

[Test]
public void AnonymousId_ConsentNone_ReturnsNull()
{
// Anonymous identifier is consent-gated: below tracking consent,
// no stable id should leak through the getter.
ImmutableAudience.Init(MakeConfig(ConsentLevel.None));

Assert.IsNull(ImmutableAudience.AnonymousId);
}

[Test]
public void AnonymousId_ConsentAnonymous_ReturnsPersistedId()
{
ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
// Track once so Identity.GetOrCreate runs and writes the id file.
ImmutableAudience.Track("warmup_event");

var id = ImmutableAudience.AnonymousId;
Assert.IsFalse(string.IsNullOrEmpty(id),
"AnonymousId should return the persisted id once tracking has created one");
}

[Test]
public void SessionId_MirrorsSessionLifecycle()
{
Assert.IsNull(ImmutableAudience.SessionId,
"SessionId should be null before Init");

ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
Assert.IsFalse(string.IsNullOrEmpty(ImmutableAudience.SessionId),
"SessionId should be non-null once Init creates a session");

ImmutableAudience.Shutdown();
Assert.IsNull(ImmutableAudience.SessionId,
"SessionId should be null after Shutdown disposes the session");
}

[Test]
public void QueueSize_ZeroBeforeInit_GrowsWithEnqueue()
{
Assert.AreEqual(0, ImmutableAudience.QueueSize,
"QueueSize should be 0 before Init");

ImmutableAudience.Init(MakeConfig(ConsentLevel.Anonymous));
// Init enqueues session_start + game_launch; those stay
// in-memory until a flush. QueueSize sums memory + disk so the
// pre-flush snapshot must be > 0.
var afterInit = ImmutableAudience.QueueSize;
Assert.Greater(afterInit, 0,
"QueueSize should include session_start and game_launch after Init");

ImmutableAudience.Track("explicit_track_event");
Assert.Greater(ImmutableAudience.QueueSize, afterInit,
"QueueSize should grow when a new event is enqueued");
}

// -----------------------------------------------------------------
// Unity context provider
// -----------------------------------------------------------------
Expand Down
Loading