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
4 changes: 4 additions & 0 deletions src/Packages/Audience/Runtime/Audience.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<Nullable>disable</Nullable>
<!-- Match the Unity asmdef assembly name so InternalsVisibleTo works in both contexts -->
<AssemblyName>Immutable.Audience.Runtime</AssemblyName>
<!-- Emit Immutable.Audience.Runtime.xml so Rider/IntelliSense show summaries.
CS1591 (missing XML comment on publicly visible type or member) is
enabled so future undocumented public members break the build. -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!--
The Unity/ subtree belongs to the sibling Immutable.Audience.Unity asmdef.
Expand Down
75 changes: 59 additions & 16 deletions src/Packages/Audience/Runtime/AudienceConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,89 @@

namespace Immutable.Audience
{
// Configuration passed to ImmutableAudience.Init.
/// <summary>
/// Configuration passed to <see cref="ImmutableAudience.Init"/>.
/// </summary>
public class AudienceConfig
{
// Studio API key. Required — Init throws if null.
/// <summary>
/// Studio API key issued by Immutable Hub.
/// </summary>
/// <remarks>
/// Required. <see cref="ImmutableAudience.Init"/> throws if null or empty.
/// </remarks>
public string? PublishableKey { get; set; }

// Override the default API base URL. When null, keys starting with
// "pk_imapik-test-" resolve to Sandbox and all other keys resolve
// to Production. Set explicitly to target a different backend.
/// <summary>
/// Override the default API base URL.
/// </summary>
/// <remarks>
/// When null, publishable keys starting with <c>pk_imapik-test-</c>
/// resolve to Sandbox. All other keys resolve to Production. Set
/// explicitly to target a different backend.
/// </remarks>
public string? BaseUrl { get; set; }

// Initial consent level.
/// <summary>
/// Initial consent level.
/// </summary>
/// <remarks>
/// If the SDK persisted a different level on a previous launch, that
/// persisted level overrides this default at
/// <see cref="ImmutableAudience.Init"/>.
/// </remarks>
public ConsentLevel Consent { get; set; } = ConsentLevel.None;

// Distribution platform the game is running on.
/// <summary>
/// Distribution platform the game is running on.
/// </summary>
/// <seealso cref="DistributionPlatforms"/>
public string? DistributionPlatform { get; set; }

// Enable debug logging.
/// <summary>
/// Whether the SDK emits debug log lines for every event, flush,
/// and consent change.
/// </summary>
public bool Debug { get; set; } = false;

// How often pending events are flushed to the backend.
/// <summary>
/// Interval between automatic flushes to the backend, in seconds.
/// </summary>
public int FlushIntervalSeconds { get; set; } = Constants.DefaultFlushIntervalSeconds;

// Flush as soon as this many events are queued.
/// <summary>
/// Queued-event threshold that triggers an automatic flush before the
/// next interval tick.
/// </summary>
public int FlushSize { get; set; } = Constants.DefaultFlushSize;

// Optional error callback.
/// <summary>
/// Callback fired when the SDK encounters a recoverable failure.
/// </summary>
/// <remarks>
/// Exceptions thrown from the callback are swallowed so a bad handler
/// cannot wedge the SDK.
/// </remarks>
public Action<AudienceError>? OnError { get; set; }

// Directory the SDK uses for identity, consent, and queued events.
// Unity hooks populate this from Application.persistentDataPath.
/// <summary>
/// Directory the SDK uses for identity, consent, and queued events.
/// Usually populated automatically by Unity hooks.
/// </summary>
public string? PersistentDataPath { get; set; }

// Library version sent on every message.
/// <summary>
/// Library version sent on every message.
/// </summary>
public string PackageVersion { get; set; } = Constants.LibraryVersion;

// Maximum time Shutdown waits for the final flush.
/// <summary>
/// Maximum time <see cref="ImmutableAudience.Shutdown"/> waits for
/// the final flush, in milliseconds.
/// </summary>
public int ShutdownFlushTimeoutMs { get; set; } = 2_000;

// Test seam for HttpTransport; not part of the public API.
// Test seam for HttpTransport. Not part of the public API.
internal System.Net.Http.HttpMessageHandler? HttpHandler { get; set; }
}
}
62 changes: 57 additions & 5 deletions src/Packages/Audience/Runtime/AudienceError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,76 @@

namespace Immutable.Audience
{
/// <summary>
/// Categorises errors raised through <see cref="AudienceConfig.OnError"/>.
/// </summary>
public enum AudienceErrorCode
{
// An event batch failed to flush. Either a local storage read error (batch dropped) or a non-2xx/non-4xx server response — typically 5xx (batch retained and retried with backoff).
/// <summary>
/// An event batch failed to flush.
/// </summary>
/// <remarks>
/// Either a local storage read error (batch dropped) or a non-2xx /
/// non-4xx server response, typically 5xx (batch retained and retried
/// with backoff).
/// </remarks>
FlushFailed,
// Server rejected an event batch with a 4xx status. The batch was dropped; retrying will not help (typically indicates a malformed payload).

/// <summary>
/// Server rejected an event batch with a 4xx status.
/// </summary>
/// <remarks>
/// The batch was dropped. Retrying will not help (typically indicates
/// a malformed payload).
/// </remarks>
ValidationRejected,
// Failed to sync a consent change to the backend. The local consent level has already been applied; the server-side audit trail may be out of date.

/// <summary>
/// Failed to sync a consent change to the backend.
/// </summary>
/// <remarks>
/// The local consent level has already been applied. The server-side
/// audit trail may be out of date.
/// </remarks>
ConsentSyncFailed,
// A network call failed (exception, timeout, or non-2xx response on data deletion). Event batches are retained for retry; data-delete requests are not retried automatically.

/// <summary>
/// A network call failed.
/// </summary>
/// <remarks>
/// Causes include exceptions, timeouts, or a non-2xx response on data
/// deletion. Event batches are retained for retry. Data-delete
/// requests are not retried automatically.
/// </remarks>
NetworkError,
// Failed to persist the consent level to disk. In-memory level still applied but will revert on next launch.

/// <summary>
/// Failed to persist the consent level to disk.
/// </summary>
/// <remarks>
/// The in-memory level is still applied but will revert on next
/// launch.
/// </remarks>
ConsentPersistFailed
}

/// <summary>
/// Error raised through <see cref="AudienceConfig.OnError"/> when the SDK
/// encounters a recoverable failure.
/// </summary>
public class AudienceError : Exception
{
/// <summary>
/// The reason this error was raised.
/// </summary>
public AudienceErrorCode Code { get; }

/// <summary>
/// Wraps a code and message into an <see cref="AudienceError"/> for
/// delivery through <see cref="AudienceConfig.OnError"/>.
/// </summary>
/// <param name="code">The reason for the failure.</param>
/// <param name="message">Human-readable description.</param>
public AudienceError(AudienceErrorCode code, string message)
: base(message)
{
Expand Down
23 changes: 19 additions & 4 deletions src/Packages/Audience/Runtime/ConsentLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@

namespace Immutable.Audience
{
// How much data the Audience SDK is allowed to collect.
/// <summary>
/// How much data the Audience SDK is allowed to collect.
/// </summary>
/// <seealso cref="ImmutableAudience.SetConsent"/>
public enum ConsentLevel
{
// No tracking
/// <summary>
/// No tracking.
/// </summary>
None,
// Anonymous tracking only

/// <summary>
/// Anonymous tracking. Events carry the device's anonymous ID. Identifying
/// the player via
/// <see cref="ImmutableAudience.Identify(string, IdentityType, System.Collections.Generic.Dictionary{string, object})"/>
/// is rejected at this level.
/// </summary>
Anonymous,
// Full tracking

/// <summary>
/// Full tracking. Events may carry a known user ID set via
/// <see cref="ImmutableAudience.Identify(string, IdentityType, System.Collections.Generic.Dictionary{string, object})"/>.
/// </summary>
Full
}

Expand Down
2 changes: 1 addition & 1 deletion src/Packages/Audience/Runtime/Core/ConsentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static class IsExternalInit { }
namespace Immutable.Audience
{
// Pairs the consent level with the user id so the two always move
// together. Updates swap the whole pair at once — a reader never sees
// together. Updates swap the whole pair at once. A reader never sees
// the new consent level alongside a leftover user id.
internal sealed record ConsentState(ConsentLevel Level, string? UserId)
{
Expand Down
15 changes: 14 additions & 1 deletion src/Packages/Audience/Runtime/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,26 @@ internal static class MessageFields
internal const string UserId = "userId";
}

// Common distribution platform values for AudienceConfig.DistributionPlatform.
/// <summary>
/// Common values for <see cref="AudienceConfig.DistributionPlatform"/>.
/// </summary>
public static class DistributionPlatforms
{
/// <summary>Steam.</summary>
public const string Steam = "steam";

/// <summary>Epic Games Store.</summary>
public const string Epic = "epic";

/// <summary>GOG.com.</summary>
public const string GOG = "gog";

/// <summary>itch.io.</summary>
public const string Itch = "itch";

/// <summary>
/// Standalone build, distributed outside any storefront.
/// </summary>
public const string Standalone = "standalone";
}
}
14 changes: 7 additions & 7 deletions src/Packages/Audience/Runtime/Core/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Immutable.Audience
// Reset() at startup to ensure a clean state in that scenario.
internal sealed class Identity
{
// In-memory cache — volatile so background threads always see the latest write.
// In-memory cache. Volatile so background threads always see the latest write.
private static volatile string? _cachedId;
private static readonly object _sync = new object();

Expand Down Expand Up @@ -66,11 +66,11 @@ internal static void ClearCache()
if (!consent.CanTrack())
return null;

// Fast path already loaded this session, no lock needed.
// Fast path: already loaded this session, no lock needed.
if (_cachedId != null)
return _cachedId;

// Slow path first call or after Reset(). Only one thread does the work.
// Slow path: first call or after Reset(). Only one thread does the work.
lock (_sync)
{
// Re-check after acquiring the lock in case another thread beat us here.
Expand All @@ -82,14 +82,14 @@ internal static void ClearCache()

var filePath = AudiencePaths.IdentityFile(persistentDataPath);

// Returning player read the ID we wrote on a previous launch.
// Returning player: read the ID we wrote on a previous launch.
if (File.Exists(filePath))
{
_cachedId = File.ReadAllText(filePath).Trim();
return _cachedId;
}

// New install generate a UUID and persist it atomically.
// New install: generate a UUID and persist it atomically.
// Write to a .tmp file first so a crash mid-write leaves no corrupt file.
var newId = Guid.NewGuid().ToString();
var tmpPath = filePath + ".tmp";
Expand All @@ -101,7 +101,7 @@ internal static void ClearCache()
}
catch (IOException)
{
// Unexpected file appeared between our Exists check and Move (shouldn't happen in practice).
// Unexpected: file appeared between our Exists check and Move (shouldn't happen in practice).
// Delete and retry to ensure a clean state.
File.Delete(filePath);
File.Move(tmpPath, filePath);
Expand All @@ -128,7 +128,7 @@ internal static void Reset(string persistentDataPath)
}
catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
{
// File was never written (e.g. consent was None) — nothing to do.
// File was never written (e.g. consent was None). Nothing to do.
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/Packages/Audience/Runtime/Core/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Immutable.Audience
//
// Start / End / Dispose are not safe to call from multiple threads at once.
// Callers run them one at a time (ImmutableAudience holds its init lock while
// calling Init / SetConsent / Shutdown / Reset the only public entry points
// calling Init / SetConsent / Shutdown / Reset, the only public entry points
// that touch a Session). Pause / Resume / OnHeartbeat are safe to call from
// any thread.
internal sealed class Session : IDisposable
Expand Down Expand Up @@ -63,7 +63,7 @@ internal void Start()
{
// Phase 1: shut down the old timer with the internal lock released
// (the callback takes that lock itself). Old state left intact so a
// trailing callback sends a heartbeat for the old session the
// trailing callback sends a heartbeat for the old session, and the
// backend receives it before the new session_start.
Timer? oldTimer;
lock (_lock)
Expand All @@ -77,7 +77,7 @@ internal void Start()
}
}

// 500ms budget — double-Start is a misuse path.
// 500ms budget. Double-Start is a misuse path.
TimerDisposal.DisposeAndWait(oldTimer, TimeSpan.FromMilliseconds(500));

// Phase 2: populate new state. Re-check _disposed (may have flipped during drain).
Expand Down Expand Up @@ -112,7 +112,7 @@ internal void Pause()
// when End fires while paused), over-crediting engagement.
if (_pausedAt.HasValue)
{
Log.Debug("Session: Pause while already paused — ignoring.");
Log.Debug("Session: Pause while already paused. Ignoring.");
return;
}
_pausedAt = _getUtcNow();
Expand Down Expand Up @@ -248,7 +248,7 @@ internal void OnHeartbeat()
}

// Stops exceptions from the track callback from reaching upstream.
// Heartbeat runs on a background timer an uncaught exception there
// Heartbeat runs on a background timer, where an uncaught exception
// crashes the game on modern .NET. Start / End run on the caller's
// thread, where it would bubble into Init / Shutdown.
private void SafeTrack(string eventName, Dictionary<string, object> properties)
Expand Down
Loading
Loading