Skip to content
Open
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 examples/audience/Assets/SampleApp/Scripts/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

// Grants test access without exposing these helpers on the SDK consumer surface.
[assembly: InternalsVisibleTo("Immutable.Audience.Samples.SampleApp.Tests")]
11 changes: 11 additions & 0 deletions examples/audience/Assets/SampleApp/Scripts/AssemblyInfo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -51,33 +51,33 @@ internal readonly struct EventSpec
EventField.Text("platform", optional: true),
}),
new EventSpec("wishlist_remove", new[] { EventField.Text("gameId") }),
new EventSpec("purchase", new[] {
EventField.Text("currency"),
EventField.Number("value"),
EventField.Text("itemId", optional: true),
EventField.Text("itemName", optional: true),
EventField.Number("quantity", optional: true),
EventField.Text("transactionId", optional: true),
new EventSpec(EventNames.Purchase, new[] {
EventField.Text(EventPropertyKeys.Currency),
EventField.Number(EventPropertyKeys.Value),
EventField.Text(EventPropertyKeys.ItemId, optional: true),
EventField.Text(EventPropertyKeys.ItemName, optional: true),
EventField.Number(EventPropertyKeys.Quantity, optional: true),
EventField.Text(EventPropertyKeys.TransactionId, optional: true),
}),
// game_launch is deliberately absent. The Event Reference v1 defines
// it as auto-tracked on Init with no public typed class; firing it
// from the Send button would double-emit.
new EventSpec("progression", new[] {
EventField.Enum("status", new[] { "start", "complete", "fail" }),
EventField.Text("world", optional: true),
EventField.Text("level", optional: true),
EventField.Text("stage", optional: true),
EventField.Number("score", optional: true),
EventField.Number("durationSec", optional: true),
new EventSpec(EventNames.Progression, new[] {
EventField.Enum(EventPropertyKeys.Status, new[] { "start", "complete", "fail" }),
EventField.Text(EventPropertyKeys.World, optional: true),
EventField.Text(EventPropertyKeys.Level, optional: true),
EventField.Text(EventPropertyKeys.Stage, optional: true),
EventField.Number(EventPropertyKeys.Score, optional: true),
EventField.Number(EventPropertyKeys.DurationSec, optional: true),
}),
new EventSpec("resource", new[] {
EventField.Enum("flow", new[] { "sink", "source" }),
EventField.Text("currency"),
EventField.Number("amount"),
EventField.Text("itemType", optional: true),
EventField.Text("itemId", optional: true),
new EventSpec(EventNames.Resource, new[] {
EventField.Enum(EventPropertyKeys.Flow, new[] { "sink", "source" }),
EventField.Text(EventPropertyKeys.Currency),
EventField.Number(EventPropertyKeys.Amount),
EventField.Text(EventPropertyKeys.ItemType, optional: true),
EventField.Text(EventPropertyKeys.ItemId, optional: true),
}),
new EventSpec("milestone_reached", new[] { EventField.Text("name") }),
new EventSpec(EventNames.MilestoneReached, new[] { EventField.Text(EventPropertyKeys.Name) }),
new EventSpec("game_page_viewed", new[] {
EventField.Text("gameId"),
EventField.Text("gameName", optional: true),
Expand All @@ -99,7 +99,7 @@ internal readonly struct EventSpec
{
switch (name)
{
case "progression":
case EventNames.Progression:
return new Progression
{
Status = ParseProgressionStatus(props),
Expand All @@ -109,7 +109,7 @@ internal readonly struct EventSpec
Score = OptionalInt(props, "score"),
DurationSec = OptionalFloat(props, "durationSec"),
};
case "resource":
case EventNames.Resource:
return new Resource
{
Flow = ParseResourceFlow(props),
Expand All @@ -118,7 +118,7 @@ internal readonly struct EventSpec
ItemType = OptionalString(props, "itemType"),
ItemId = OptionalString(props, "itemId"),
};
case "purchase":
case EventNames.Purchase:
return new Purchase
{
Currency = OptionalString(props, "currency") ?? "",
Expand All @@ -128,7 +128,7 @@ internal readonly struct EventSpec
Quantity = OptionalInt(props, "quantity"),
TransactionId = OptionalString(props, "transactionId"),
};
case "milestone_reached":
case EventNames.MilestoneReached:
return new MilestoneReached { Name = OptionalString(props, "name") ?? "" };
default:
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ private VisualElement BuildLogRow(LogEntry entry)
private static string FormatLogEntry(LogEntry entry, bool singleLine)
{
var sb = new StringBuilder()
.Append(entry.Timestamp.ToString("o", CultureInfo.InvariantCulture))
.Append(entry.Timestamp.ToString(Constants.IsoTimestampFormat, CultureInfo.InvariantCulture))
.Append(" [").Append(entry.Source == LogSource.Sdk ? "SDK" : "APP").Append("] ")
.Append(entry.Label);
if (!string.IsNullOrEmpty(entry.Body))
Expand Down
14 changes: 7 additions & 7 deletions examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private void OnSendCatalogueEvent(EventSpec spec, Dictionary<string, VisualEleme
{
["event"] = spec.Name,
["overload"] = "typed",
["properties"] = typed.ToProperties(),
[MessageFields.Properties] = typed.ToProperties(),
}, 2);
}

Expand All @@ -141,7 +141,7 @@ private void OnSendCatalogueEvent(EventSpec spec, Dictionary<string, VisualEleme
{
["event"] = spec.Name,
["overload"] = "string",
["properties"] = props,
[MessageFields.Properties] = props,
}, 2);
});

Expand All @@ -155,7 +155,7 @@ private void OnSendCustomEvent() => RunAndLog("track()", () =>
var props = string.IsNullOrEmpty(f.RawProps) ? null : JsonReader.DeserializeObject(f.RawProps);
ImmutableAudience.Track(f.Name, props);
var echo = new Dictionary<string, object> { ["event"] = f.Name };
if (props != null) echo["properties"] = props;
if (props != null) echo[MessageFields.Properties] = props;
return Json.Serialize(echo, 2);
});

Expand Down Expand Up @@ -199,10 +199,10 @@ private void OnIdentify() => RunAndLog("identify()", () =>
var payload = new Dictionary<string, object>
{
["id"] = f.Id,
["identityType"] = f.Type,
[MessageFields.IdentityType] = f.Type,
["accepted"] = accepted,
};
if (traits != null) payload["traits"] = traits;
if (traits != null) payload[MessageFields.Traits] = traits;
return Json.Serialize(payload, 2);
});

Expand Down Expand Up @@ -233,8 +233,8 @@ private void OnAlias() => RunAndLog("alias()", () =>
}
return Json.Serialize(new Dictionary<string, object>
{
["from"] = new Dictionary<string, object> { ["id"] = f.FromId, ["identityType"] = f.FromType },
["to"] = new Dictionary<string, object> { ["id"] = f.ToId, ["identityType"] = f.ToType },
["from"] = new Dictionary<string, object> { ["id"] = f.FromId, [MessageFields.IdentityType] = f.FromType },
["to"] = new Dictionary<string, object> { ["id"] = f.ToId, [MessageFields.IdentityType] = f.ToType },
["accepted"] = accepted,
}, 2);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void SetUp()
// (and identity/queue) to disk under <persistentDataPath>/imtbl_audience.
// Without wiping it, a SetConsent(None) from a prior test leaks into
// the next test's Init via ConsentStore.Load.
var sdkDir = System.IO.Path.Combine(Application.persistentDataPath, SampleAppUi.SdkPersistedDirName);
var sdkDir = AudiencePaths.AudienceDir(Application.persistentDataPath);
if (System.IO.Directory.Exists(sdkDir))
System.IO.Directory.Delete(sdkDir, recursive: true);

Expand Down
3 changes: 3 additions & 0 deletions src/Packages/Audience/Runtime/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
// JsonReader.DeserializeObject; both stay internal so their
// signatures aren't frozen into the public API.
[assembly: InternalsVisibleTo("Immutable.Audience.Samples.SampleApp")]

// SampleApp tests pin against the same internal constants the runtime SDK uses.
[assembly: InternalsVisibleTo("Immutable.Audience.Samples.SampleApp.Tests")]
7 changes: 7 additions & 0 deletions src/Packages/Audience/Runtime/Core/AudiencePaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ internal static class AudiencePaths
private const string ConsentFileName = "consent";
private const string QueueDirName = "queue";

// Queue files are named <ticks>_<uuid>.json.
internal const string QueueFileExtension = ".json";
internal const string QueueGlob = "*" + QueueFileExtension;

// Files ending in this suffix are mid-write and must not be read.
internal const string TempFileSuffix = ".tmp";

internal static string AudienceDir(string persistentDataPath) =>
Path.Combine(persistentDataPath, RootDirName);

Expand Down
2 changes: 1 addition & 1 deletion src/Packages/Audience/Runtime/Core/ConsentStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static void Save(string persistentDataPath, ConsentLevel level)
Directory.CreateDirectory(AudiencePaths.AudienceDir(persistentDataPath));

var filePath = AudiencePaths.ConsentFile(persistentDataPath);
var tmpPath = filePath + ".tmp";
var tmpPath = filePath + AudiencePaths.TempFileSuffix;

File.WriteAllText(tmpPath, ((int)level).ToString());

Expand Down
127 changes: 127 additions & 0 deletions src/Packages/Audience/Runtime/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ internal static class Constants
internal const int MaxBatchSize = 100;
internal const int StaleEventDays = 30;
internal const int MaxFieldLength = 256; // Backend schema limit.

// Timestamp format the backend wants on every event.
internal const string IsoTimestampFormat = "o";

// Format that lets numbers survive a JSON round-trip unchanged.
internal const string RoundTripNumberFormat = "R";
internal const int ControlPlaneRequestTimeoutSeconds = 30;

internal const string LibraryName = "com.immutable.audience";
Expand All @@ -25,6 +31,10 @@ internal static class Constants
internal const string ConsentSource = "UnitySDK";

internal const string PublishableKeyHeader = "x-immutable-publishable-key";
internal const string ContentEncodingHeader = "Content-Encoding";

internal const string MediaTypeJson = "application/json";
internal const string GzipEncoding = "gzip";

internal static string MessagesUrl(string? publishableKey, string? baseUrlOverride = null) =>
BaseUrl(publishableKey, baseUrlOverride) + MessagesPath;
Expand Down Expand Up @@ -52,12 +62,129 @@ internal static class MessageTypes
internal const string Alias = "alias";
}

// Property keys for the auto-fired game_launch event.
internal static class GameLaunchPropertyKeys
{
internal const string Platform = "platform";
internal const string Version = "version";
internal const string BuildGuid = "buildGuid";
internal const string UnityVersion = "unityVersion";
internal const string OsFamily = "osFamily";
internal const string DeviceModel = "deviceModel";
internal const string Gpu = "gpu";
internal const string GpuVendor = "gpuVendor";
internal const string Cpu = "cpu";
internal const string CpuCores = "cpuCores";
internal const string RamMb = "ramMb";
internal const string ScreenDpi = "screenDpi";
internal const string DistributionPlatform = "distributionPlatform";
}

// Keys merged into every event's context dictionary.
internal static class ContextKeys
{
internal const string UserAgent = "userAgent";
internal const string Timezone = "timezone";
internal const string Locale = "locale";
internal const string Screen = "screen";
}

// JSON keys for the consent-sync PUT body.
internal static class ConsentBodyFields
{
internal const string Status = "status";
internal const string Source = "source";
}

// JSON keys for the messages POST envelope and response.
internal static class ResponseFields
{
internal const string MessagesEnvelope = "messages";
internal const string Rejected = "rejected";
}

// Keys inside each event's "properties" dict.
internal static class EventPropertyKeys
{
// Shared across multiple events (Session + Progression for DurationSec,
// Resource + Purchase for Currency / ItemId).
internal const string SessionId = "sessionId";
internal const string DurationSec = "durationSec";
internal const string Currency = "currency";
internal const string ItemId = "itemId";

// Progression-specific
internal const string Status = "status";
internal const string World = "world";
internal const string Level = "level";
internal const string Stage = "stage";
internal const string Score = "score";

// Resource-specific
internal const string Flow = "flow";
internal const string Amount = "amount";
internal const string ItemType = "itemType";

// Purchase-specific
internal const string Value = "value";
internal const string ItemName = "itemName";
internal const string Quantity = "quantity";
internal const string TransactionId = "transactionId";

// MilestoneReached-specific
internal const string Name = "name";
}

// Event names we send on Track.
internal static class EventNames
{
// Session lifecycle (auto-fired)
internal const string SessionStart = "session_start";
internal const string SessionEnd = "session_end";
internal const string SessionHeartbeat = "session_heartbeat";

// Init lifecycle (auto-fired)
internal const string GameLaunch = "game_launch";

// Typed events (IEvent implementations)
internal const string Progression = "progression";
internal const string Resource = "resource";
internal const string Purchase = "purchase";
internal const string MilestoneReached = "milestone_reached";
}

// Wire-format field names that cross module boundaries inside the SDK
// (read by one module, written by another).
internal static class MessageFields
{
// Envelope keys present on every message
internal const string Type = "type";
internal const string MessageId = "messageId";
internal const string EventTimestamp = "eventTimestamp";
internal const string Context = "context";
internal const string Surface = "surface";

// Track envelope
internal const string EventName = "eventName";
internal const string Properties = "properties";

// Identity envelope (track, identify, alias)
internal const string AnonymousId = "anonymousId";
internal const string UserId = "userId";

// Identify envelope
internal const string IdentityType = "identityType";
internal const string Traits = "traits";

// Alias envelope
internal const string FromId = "fromId";
internal const string FromType = "fromType";
internal const string ToId = "toId";
internal const string ToType = "toType";

// Context dictionary keys
internal const string Library = "library";
internal const string LibraryVersion = "libraryVersion";
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Packages/Audience/Runtime/Core/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ internal static void ClearCache()
// 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";
var tmpPath = filePath + AudiencePaths.TempFileSuffix;
File.WriteAllText(tmpPath, newId);

try
Expand Down
Loading
Loading