diff --git a/src/Packages/Audience/Runtime/AudienceConfig.cs b/src/Packages/Audience/Runtime/AudienceConfig.cs
index 759645562..f6085bca1 100644
--- a/src/Packages/Audience/Runtime/AudienceConfig.cs
+++ b/src/Packages/Audience/Runtime/AudienceConfig.cs
@@ -1,17 +1,24 @@
namespace Immutable.Audience
{
- /// Configuration passed to .
+ // Configuration passed to ImmutableAudience.Init.
public class AudienceConfig
{
+ // Studio API key.
public string PublishableKey { get; set; }
+
+ // Initial consent level.
public ConsentLevel Consent { get; set; } = ConsentLevel.None;
- ///
- /// Distribution platform the game is running on.
- /// Use for common values, or pass any custom string.
- ///
+
+ // Distribution platform the game is running on.
public string DistributionPlatform { get; set; }
+
+ // Enable debug logging.
public bool Debug { get; set; } = false;
+
+ // How often pending events are flushed to the backend.
public int FlushIntervalSeconds { get; set; } = Constants.DefaultFlushIntervalSeconds;
+
+ // Flush as soon as this many events are queued.
public int FlushSize { get; set; } = Constants.DefaultFlushSize;
}
}
diff --git a/src/Packages/Audience/Runtime/ConsentLevel.cs b/src/Packages/Audience/Runtime/ConsentLevel.cs
index 66e518c05..197836d75 100644
--- a/src/Packages/Audience/Runtime/ConsentLevel.cs
+++ b/src/Packages/Audience/Runtime/ConsentLevel.cs
@@ -1,13 +1,13 @@
namespace Immutable.Audience
{
- /// Controls what the Audience SDK tracks.
+ // How much data the Audience SDK is allowed to collect.
public enum ConsentLevel
{
- /// SDK inert. No events queued or sent. No IDs persisted to disk.
+ // No tracking.
None,
- /// Track events with anonymousId only. Identify/Alias discarded with warning.
+ // Anonymous tracking only.
Anonymous,
- /// All events. Identify/Alias send. userId attached to track events.
+ // Full tracking, including identity.
Full
}
}
diff --git a/src/Packages/Audience/Runtime/Core/AudiencePaths.cs b/src/Packages/Audience/Runtime/Core/AudiencePaths.cs
new file mode 100644
index 000000000..52fb40c33
--- /dev/null
+++ b/src/Packages/Audience/Runtime/Core/AudiencePaths.cs
@@ -0,0 +1,24 @@
+using System.IO;
+
+namespace Immutable.Audience
+{
+ internal static class AudiencePaths
+ {
+ private const string RootDirName = "imtbl_audience";
+ private const string IdentityFileName = "identity";
+ private const string ConsentFileName = "consent";
+ private const string QueueDirName = "queue";
+
+ internal static string AudienceDir(string persistentDataPath) =>
+ Path.Combine(persistentDataPath, RootDirName);
+
+ internal static string IdentityFile(string persistentDataPath) =>
+ Path.Combine(AudienceDir(persistentDataPath), IdentityFileName);
+
+ internal static string ConsentFile(string persistentDataPath) =>
+ Path.Combine(AudienceDir(persistentDataPath), ConsentFileName);
+
+ internal static string QueueDir(string persistentDataPath) =>
+ Path.Combine(AudienceDir(persistentDataPath), QueueDirName);
+ }
+}
diff --git a/src/Packages/Audience/Runtime/Core/Constants.cs b/src/Packages/Audience/Runtime/Core/Constants.cs
index 6573730d8..a7ed09792 100644
--- a/src/Packages/Audience/Runtime/Core/Constants.cs
+++ b/src/Packages/Audience/Runtime/Core/Constants.cs
@@ -25,10 +25,7 @@ internal static string BaseUrl(string publishableKey) =>
: ProductionBaseUrl;
}
- ///
- /// String constants for common game distribution platforms.
- /// Any string is accepted -- studios are not limited to these values.
- ///
+ // Common distribution platform values for AudienceConfig.DistributionPlatform.
public static class DistributionPlatforms
{
public const string Steam = "steam";
diff --git a/src/Packages/Audience/Runtime/Core/Identity.cs b/src/Packages/Audience/Runtime/Core/Identity.cs
index 70033f4c7..2b1143d38 100644
--- a/src/Packages/Audience/Runtime/Core/Identity.cs
+++ b/src/Packages/Audience/Runtime/Core/Identity.cs
@@ -15,12 +15,6 @@ internal sealed class Identity
private static volatile string _cachedId;
private static readonly object _sync = new object();
- private static string GetDirectory(string persistentDataPath) =>
- Path.Combine(persistentDataPath, "imtbl_audience");
-
- private static string GetFilePath(string persistentDataPath) =>
- Path.Combine(GetDirectory(persistentDataPath), "identity");
-
// Returns the anonymous ID, generating and persisting it on first call.
// Returns null without touching disk when consent is None.
// Safe to call from any thread after ImmutableAudience.Init() has run on the main thread.
@@ -41,10 +35,10 @@ internal static string GetOrCreate(string persistentDataPath, ConsentLevel conse
if (_cachedId != null)
return _cachedId;
- var dir = GetDirectory(persistentDataPath);
+ var dir = AudiencePaths.AudienceDir(persistentDataPath);
Directory.CreateDirectory(dir); // no-op if already exists
- var filePath = GetFilePath(persistentDataPath);
+ var filePath = AudiencePaths.IdentityFile(persistentDataPath);
// Returning player — read the ID we wrote on a previous launch.
if (File.Exists(filePath))
@@ -85,7 +79,7 @@ internal static void Reset(string persistentDataPath)
{
_cachedId = null;
- var filePath = GetFilePath(persistentDataPath);
+ var filePath = AudiencePaths.IdentityFile(persistentDataPath);
try
{
File.Delete(filePath);
diff --git a/src/Packages/Audience/Runtime/ImmutableAudience.cs b/src/Packages/Audience/Runtime/ImmutableAudience.cs
index 6c882b7b7..d8b78aed2 100644
--- a/src/Packages/Audience/Runtime/ImmutableAudience.cs
+++ b/src/Packages/Audience/Runtime/ImmutableAudience.cs
@@ -1,14 +1,11 @@
namespace Immutable.Audience
{
- ///
- /// Entry point for the Immutable Audience SDK.
- /// Call once on startup, then use the static methods from any thread.
- ///
+ // Entry point for the Immutable Audience SDK.
public static class ImmutableAudience
{
// Scaffold only -- implementation follows in subsequent sub-issues (see SDK-99).
- /// Initialise the SDK. Call once, typically in your game's entry scene.
+ // Starts the SDK. Call once at launch.
public static void Init(AudienceConfig config)
{
throw new System.NotImplementedException(
diff --git a/src/Packages/Audience/Runtime/Transport/DiskStore.cs b/src/Packages/Audience/Runtime/Transport/DiskStore.cs
index 6e691521b..e57e09137 100644
--- a/src/Packages/Audience/Runtime/Transport/DiskStore.cs
+++ b/src/Packages/Audience/Runtime/Transport/DiskStore.cs
@@ -5,10 +5,8 @@
namespace Immutable.Audience
{
- ///
- /// File-per-event persistent store. Each event is written as an atomic
- /// {ticks}_{uuid}.json file inside imtbl_audience/queue/.
- ///
+ // File-per-event persistent store. Each event is written as an atomic
+ // {ticks}_{uuid}.json file inside imtbl_audience/queue/.
internal sealed class DiskStore
{
private readonly string _queueDir;
@@ -19,7 +17,7 @@ internal DiskStore(string persistentDataPath)
Directory.CreateDirectory(_queueDir);
}
- /// Atomically writes as a new event file.
+ // Atomically writes json as a new event file.
internal void Write(string json)
{
var fileName = $"{DateTime.UtcNow.Ticks}_{Guid.NewGuid():N}.json";
@@ -40,10 +38,8 @@ internal void Write(string json)
}
}
- ///
- /// Returns up to file paths, oldest first.
- /// Files older than days are deleted and excluded.
- ///
+ // Returns up to maxSize file paths, oldest first. Stale files
+ // (older than Constants.StaleEventDays) are deleted and excluded.
internal IReadOnlyList ReadBatch(int maxSize)
{
if (maxSize <= 0)
@@ -83,14 +79,14 @@ internal IReadOnlyList ReadBatch(int maxSize)
return result;
}
- /// Deletes the event files at .
+ // Deletes the given event files.
internal void Delete(IEnumerable paths)
{
foreach (var path in paths)
TryDelete(path);
}
- /// Returns the total number of event files currently on disk.
+ // Total number of event files currently on disk.
internal int Count() => Directory.GetFiles(_queueDir, "*.json").Length;
private static void TryDelete(string path)
diff --git a/src/Packages/Audience/Runtime/Transport/EventQueue.cs b/src/Packages/Audience/Runtime/Transport/EventQueue.cs
index 0b7491a5f..8f4ae023b 100644
--- a/src/Packages/Audience/Runtime/Transport/EventQueue.cs
+++ b/src/Packages/Audience/Runtime/Transport/EventQueue.cs
@@ -4,17 +4,11 @@
namespace Immutable.Audience
{
- ///
- /// Thread-safe, disk-persistent batch event queue for the Audience SDK.
- ///
- /// Enqueue is lock-free and safe to call from any thread. A background
- /// drain thread moves events from the in-memory
- /// to , flushing either on a time interval or when the
- /// in-memory batch reaches .
- ///
- /// Call before process exit to flush remaining events
- /// and stop the drain thread cleanly.
- ///
+ // Thread-safe, disk-persistent batch event queue.
+ // Enqueue is lock-free and safe from any thread. A background drain
+ // thread moves events from the in-memory ConcurrentQueue to DiskStore,
+ // flushing on a time interval or when the batch reaches FlushSize.
+ // Call Shutdown before process exit.
internal sealed class EventQueue : IDisposable
{
private readonly DiskStore _store;
@@ -29,9 +23,9 @@ internal sealed class EventQueue : IDisposable
// Volatile so all threads see the shutdown signal immediately.
private volatile bool _disposed;
- /// Pre-created for this queue.
- /// How often to drain to disk regardless of batch size.
- /// Drain to disk immediately when this many events are queued.
+ // store: destination for drained events.
+ // flushIntervalSeconds: how often to drain to disk regardless of batch size.
+ // flushSize: drain to disk immediately when this many events are queued.
internal EventQueue(DiskStore store, int flushIntervalSeconds, int flushSize)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
@@ -46,7 +40,7 @@ internal EventQueue(DiskStore store, int flushIntervalSeconds, int flushSize)
_drainThread.Start();
}
- /// Enqueues a JSON-serialised event. Lock-free; safe from any thread.
+ // Enqueues a JSON-serialised event. Lock-free; safe from any thread.
internal void Enqueue(string json)
{
if (_disposed) return;
@@ -58,19 +52,15 @@ internal void Enqueue(string json)
_flushGate.Set();
}
- ///
- /// Drains the in-memory queue and persists all events to disk immediately.
- /// Blocks until the drain is complete.
- ///
+ // Drains the in-memory queue and persists all events to disk
+ // immediately. Blocks until the drain is complete.
internal void FlushSync()
{
DrainMemoryToDisk();
}
- ///
- /// Flushes all pending events to disk and stops the drain thread.
- /// Safe to call multiple times.
- ///
+ // Flushes all pending events to disk and stops the drain thread.
+ // Safe to call multiple times.
internal void Shutdown()
{
if (_disposed) return;
diff --git a/src/Packages/Audience/Runtime/Transport/HttpTransport.cs b/src/Packages/Audience/Runtime/Transport/HttpTransport.cs
index 093d15c65..7590e4229 100644
--- a/src/Packages/Audience/Runtime/Transport/HttpTransport.cs
+++ b/src/Packages/Audience/Runtime/Transport/HttpTransport.cs
@@ -12,9 +12,7 @@
namespace Immutable.Audience
{
- ///
- /// Sends queued events from to the Audience backend.
- ///
+ // Sends queued events from DiskStore to the Audience backend.
internal sealed class HttpTransport : IDisposable
{
private readonly DiskStore _store;
@@ -27,11 +25,10 @@ internal sealed class HttpTransport : IDisposable
private int _consecutiveFailures;
private DateTime? _nextAttemptAt;
- /// Source of event batches.
- /// Studio API key. Sent as x-immutable-publishable-key on every request.
- /// Optional failure callback. Exceptions thrown inside it are caught and ignored.
- /// Optional . Callers can supply a custom pipeline (e.g. specific for test purposes). Defaults to the standard handler when null.
- /// Optional UTC clock source used for backoff timing (e.g. swappable for deterministic time). Defaults to DateTime.UtcNow when null.
+ // store: source of event batches.
+ // publishableKey: sent as x-immutable-publishable-key on every request.
+ // onError: optional failure callback. Exceptions thrown inside it are caught.
+ // handler / getUtcNow: test seams; null for production use.
internal HttpTransport(
DiskStore store,
string publishableKey,
@@ -48,10 +45,8 @@ internal HttpTransport(
_getUtcNow = getUtcNow ?? (() => DateTime.UtcNow);
}
- ///
- /// Attempts to process one batch: reads it from disk, gzips it, and POSTs it.
- /// Returns true if a batch was consumed (outcome irrelevant), false if the queue was empty.
- ///
+ // Processes one batch. Returns true if a batch was consumed
+ // (outcome irrelevant), false if the queue was empty.
internal async Task SendBatchAsync(CancellationToken ct = default)
{
var batch = _store.ReadBatch(Constants.DefaultFlushSize);
@@ -80,15 +75,18 @@ internal async Task SendBatchAsync(CancellationToken ct = default)
return true;
}
- var compressed = Gzip.Compress(payload);
-
try
{
using var request = new HttpRequestMessage(HttpMethod.Post, _url);
request.Headers.Add("x-immutable-publishable-key", _publishableKey);
+#if IMMUTABLE_AUDIENCE_GZIP
+ var compressed = Gzip.Compress(payload);
request.Content = new ByteArrayContent(compressed);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
request.Content.Headers.Add("Content-Encoding", "gzip");
+#else
+ request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
+#endif
using var response = await _client.SendAsync(request, ct).ConfigureAwait(false);
@@ -143,16 +141,11 @@ internal async Task SendBatchAsync(CancellationToken ct = default)
_ => 60_000,
};
- ///
- /// Earliest UTC time at which the next attempt may run.
- /// Null when no backoff is active (never failed, or last attempt succeeded).
- ///
+ // Earliest UTC time at which the next attempt may run.
+ // Null when no backoff is active.
internal DateTime? NextAttemptAt => _nextAttemptAt;
- ///
- /// True while UtcNow < NextAttemptAt. Flips false as the clock
- /// advances; no reset required.
- ///
+ // True while UtcNow < NextAttemptAt. Flips false as the clock advances.
internal bool IsInBackoffWindow => _getUtcNow() < _nextAttemptAt;
public void Dispose()
@@ -175,14 +168,9 @@ private void ResetBackoff()
_nextAttemptAt = null;
}
- ///
- /// Reads each path and wraps the concatenated JSON bodies in
- /// {"batch":[msg1,msg2,...]}.
- ///
- ///
- /// The batched JSON, or null if every path was unreadable. Caller
- /// treats null as "nothing to send" and deletes the path list.
- ///
+ // Reads each path and wraps the concatenated JSON bodies in
+ // {"batch":[msg1,msg2,...]}. Returns null if every path was
+ // unreadable; the caller treats null as "nothing to send".
private static string? BuildPayload(IReadOnlyList paths)
{
var sb = new StringBuilder("{\"batch\":[");
diff --git a/src/Packages/Audience/Runtime/Utility/Gzip.cs b/src/Packages/Audience/Runtime/Utility/Gzip.cs
index c54d4cd33..8f8b11c4b 100644
--- a/src/Packages/Audience/Runtime/Utility/Gzip.cs
+++ b/src/Packages/Audience/Runtime/Utility/Gzip.cs
@@ -1,13 +1,12 @@
+#if IMMUTABLE_AUDIENCE_GZIP
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Immutable.Audience
{
- ///
- /// Gzip compression using from System.IO.Compression.
- /// Available in Unity 2021+ (.NET Standard 2.1). Pure C#, works on all desktop platforms.
- ///
+ // Gzip compression via GZipStream from System.IO.Compression.
+ // Available in Unity 2021+ (.NET Standard 2.1). Works on all desktop platforms.
internal static class Gzip
{
internal static byte[] Compress(string text)
@@ -23,4 +22,5 @@ internal static byte[] Compress(string text)
return output.ToArray();
}
}
-}
\ No newline at end of file
+}
+#endif
diff --git a/src/Packages/Audience/Runtime/Utility/JsonReader.cs b/src/Packages/Audience/Runtime/Utility/JsonReader.cs
new file mode 100644
index 000000000..bd516dc63
--- /dev/null
+++ b/src/Packages/Audience/Runtime/Utility/JsonReader.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace Immutable.Audience
+{
+ // Minimal JSON reader. Handles the subset produced by Json.Serialize:
+ // objects, strings, numbers, booleans, null, arrays. Reflection-free so
+ // IL2CPP-safe. Throws FormatException on malformed input.
+ internal static class JsonReader
+ {
+ internal static Dictionary DeserializeObject(string json)
+ {
+ var p = new Parser(json);
+ p.SkipWhitespace();
+ var result = p.ReadObject();
+ p.SkipWhitespace();
+ if (p.Pos != json.Length)
+ throw new FormatException($"Trailing content at position {p.Pos}");
+ return result;
+ }
+
+ private struct Parser
+ {
+ private readonly string _s;
+ internal int Pos;
+
+ internal Parser(string s) { _s = s; Pos = 0; }
+
+ internal void SkipWhitespace()
+ {
+ while (Pos < _s.Length)
+ {
+ var c = _s[Pos];
+ if (c == ' ' || c == '\t' || c == '\r' || c == '\n') Pos++;
+ else break;
+ }
+ }
+
+ internal Dictionary ReadObject()
+ {
+ Expect('{');
+ var obj = new Dictionary();
+ SkipWhitespace();
+ if (Peek() == '}') { Pos++; return obj; }
+
+ while (true)
+ {
+ SkipWhitespace();
+ var key = ReadString();
+ SkipWhitespace();
+ Expect(':');
+ SkipWhitespace();
+ obj[key] = ReadValue();
+ SkipWhitespace();
+ var next = Read();
+ if (next == ',') continue;
+ if (next == '}') return obj;
+ throw new FormatException($"Expected ',' or '}}' at position {Pos - 1}");
+ }
+ }
+
+ private List