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
1 change: 1 addition & 0 deletions src/Packages/Audience/Runtime/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ internal static class MessageFields
internal const string Type = "type";
internal const string UserId = "userId";
internal const string DeviceId = "deviceId";
internal const string ConsentLevel = "consentLevel";
}

/// <summary>
Expand Down
15 changes: 10 additions & 5 deletions src/Packages/Audience/Runtime/Events/MessageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ internal static Dictionary<string, object> Track(
string? userId,
string? deviceId,
string packageVersion,
string consentLevel,
Dictionary<string, object>? properties = null,
bool testMode = false)
{
var msg = BuildBase(MessageTypes.Track, packageVersion, testMode);
var msg = BuildBase(MessageTypes.Track, packageVersion, consentLevel, testMode);
msg["eventName"] = Truncate(eventName, Constants.MaxFieldLength);

if (!string.IsNullOrEmpty(anonymousId))
Expand All @@ -43,10 +44,11 @@ internal static Dictionary<string, object> Identify(
string? deviceId,
string identityType,
string packageVersion,
string consentLevel,
Dictionary<string, object>? traits = null,
bool testMode = false)
{
var msg = BuildBase(MessageTypes.Identify, packageVersion, testMode);
var msg = BuildBase(MessageTypes.Identify, packageVersion, consentLevel, testMode);

if (!string.IsNullOrEmpty(anonymousId))
msg["anonymousId"] = Truncate(anonymousId, Constants.MaxFieldLength);
Expand Down Expand Up @@ -75,9 +77,10 @@ internal static Dictionary<string, object> Alias(
string toType,
string? deviceId,
string packageVersion,
string consentLevel,
bool testMode = false)
{
var msg = BuildBase(MessageTypes.Alias, packageVersion, testMode);
var msg = BuildBase(MessageTypes.Alias, packageVersion, consentLevel, testMode);
msg["fromId"] = Truncate(fromId, Constants.MaxFieldLength);
msg["fromType"] = Truncate(fromType, Constants.MaxFieldLength);
msg["toId"] = Truncate(toId, Constants.MaxFieldLength);
Expand All @@ -89,7 +92,8 @@ internal static Dictionary<string, object> Alias(
return msg;
}

private static Dictionary<string, object> BuildBase(string type, string packageVersion, bool testMode)
private static Dictionary<string, object> BuildBase(
string type, string packageVersion, string consentLevel, bool testMode)
{
var msg = new Dictionary<string, object>
{
Expand All @@ -101,7 +105,8 @@ private static Dictionary<string, object> BuildBase(string type, string packageV
["library"] = Constants.LibraryName,
["libraryVersion"] = Truncate(packageVersion, Constants.MaxFieldLength)
},
["surface"] = Constants.Surface
["surface"] = Constants.Surface,
[MessageFields.ConsentLevel] = consentLevel
};
if (testMode)
msg["test"] = true;
Expand Down
10 changes: 6 additions & 4 deletions src/Packages/Audience/Runtime/ImmutableAudience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,8 @@ public static void Track(IEvent evt)
var deviceId = Identity.GetOrCreateDeviceId(config.PersistentDataPath!, state.Level);
// ToProperties returns a fresh dict per call, so no snapshot needed.
var userId = state.Level == ConsentLevel.Full ? state.UserId : null;
var msg = MessageBuilder.Track(eventName, anonymousId, userId, deviceId, Constants.LibraryVersion, properties, config.TestMode);
var msg = MessageBuilder.Track(eventName, anonymousId, userId, deviceId, Constants.LibraryVersion,
state.Level.ToLowercaseString(), properties, config.TestMode);
EnqueueTrack(msg);
}

Expand Down Expand Up @@ -382,7 +383,7 @@ public static void Track(string eventName, Dictionary<string, object>? propertie
var deviceId = Identity.GetOrCreateDeviceId(config.PersistentDataPath!, state.Level);
var userId = state.Level == ConsentLevel.Full ? state.UserId : null;
var msg = MessageBuilder.Track(eventName, anonymousId, userId, deviceId, Constants.LibraryVersion,
SnapshotCallerDict(properties), config.TestMode);
state.Level.ToLowercaseString(), SnapshotCallerDict(properties), config.TestMode);
EnqueueTrack(msg);
}

Expand Down Expand Up @@ -429,7 +430,7 @@ public static void Identify(string userId, IdentityType identityType, Dictionary
var anonymousId = Identity.GetOrCreate(config.PersistentDataPath!, level);
var deviceId = Identity.GetOrCreateDeviceId(config.PersistentDataPath!, level);
var msg = MessageBuilder.Identify(anonymousId, userId, deviceId, identityType.ToLowercaseString(),
Constants.LibraryVersion, SnapshotCallerDict(traits), config.TestMode);
Constants.LibraryVersion, level.ToLowercaseString(), SnapshotCallerDict(traits), config.TestMode);
EnqueueIdentity(msg);
}

Expand Down Expand Up @@ -461,7 +462,7 @@ public static void Alias(string fromId, IdentityType fromType, string toId, Iden

var deviceId = Identity.GetOrCreateDeviceId(config.PersistentDataPath!, state.Level);
var msg = MessageBuilder.Alias(fromId, fromType.ToLowercaseString(), toId, toType.ToLowercaseString(),
deviceId, Constants.LibraryVersion, config.TestMode);
deviceId, Constants.LibraryVersion, state.Level.ToLowercaseString(), config.TestMode);
EnqueueIdentity(msg);
}

Expand Down Expand Up @@ -1018,6 +1019,7 @@ private static void EnqueueTrack(Dictionary<string, object>? msg)
{
var state = _state;
if (!state.Level.CanTrack()) return null;
m[MessageFields.ConsentLevel] = state.Level.ToLowercaseString();
if (state.Level != ConsentLevel.Full)
m.Remove(MessageFields.UserId);
return m;
Expand Down
19 changes: 16 additions & 3 deletions src/Packages/Audience/Runtime/Transport/DiskStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,20 @@ private void ApplyAnonymousDowngradeToFile(string path)
return;
}

if (type == MessageTypes.Track && msg.ContainsKey(MessageFields.UserId))
RewriteTrackWithoutUserId(path, msg);
if (type == MessageTypes.Track && TrackNeedsDowngradeToAnonymous(msg))
RewriteTrackForAnonymous(path, msg);
}

// A queued track needs rewriting on a Full -> Anonymous downgrade if it
// still carries a userId or a consentLevel other than "anonymous". The
// check keeps already-anonymous messages untouched so a fail-closed
// rewrite error can only ever discard events that actually had to change.
private static bool TrackNeedsDowngradeToAnonymous(Dictionary<string, object> msg)
{
if (msg.ContainsKey(MessageFields.UserId)) return true;
return !(msg.TryGetValue(MessageFields.ConsentLevel, out var cl)
&& cl is string s
&& s == ConsentLevel.Anonymous.ToLowercaseString());
}

private static bool IsIdentityMessage(string type) =>
Expand All @@ -168,9 +180,10 @@ private static bool TryReadMessage(string path, [NotNullWhen(true)] out Dictiona
return true;
}

private void RewriteTrackWithoutUserId(string path, Dictionary<string, object> msg)
private void RewriteTrackForAnonymous(string path, Dictionary<string, object> msg)
{
msg.Remove(MessageFields.UserId);
msg[MessageFields.ConsentLevel] = ConsentLevel.Anonymous.ToLowercaseString();

try
{
Expand Down
77 changes: 52 additions & 25 deletions src/Packages/Audience/Tests/Runtime/Events/MessageBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ public class MessageBuilderTests
private const string PackageVersion = "1.2.3";
private const string AnonId = "anon-1";
private const string DeviceId = "device-1";
private const string Consent = "anonymous";

[Test]
public void Track_RequiredFieldsPresent()
{
var result = MessageBuilder.Track("level_complete", AnonId, null, null, PackageVersion);
var result = MessageBuilder.Track("level_complete", AnonId, null, null, PackageVersion, Consent);

Assert.AreEqual("track", result["type"]);
Assert.IsTrue(result.ContainsKey("messageId"));
Expand All @@ -30,23 +31,23 @@ public void Track_EventNameLongerThan256Chars_TruncatedTo256()
{
var longName = new string('x', 300);

var result = MessageBuilder.Track(longName, null, null, null, PackageVersion);
var result = MessageBuilder.Track(longName, null, null, null, PackageVersion, Consent);

Assert.AreEqual(256, ((string)result["eventName"]).Length);
}

[Test]
public void Track_NullUserId_NotPresentInDict()
{
var result = MessageBuilder.Track("evt", AnonId, null, null, PackageVersion);
var result = MessageBuilder.Track("evt", AnonId, null, null, PackageVersion, Consent);

Assert.IsFalse(result.ContainsKey("userId"));
}

[Test]
public void Track_NonNullUserId_PresentInDict()
{
var result = MessageBuilder.Track("evt", AnonId, "user-99", null, PackageVersion);
var result = MessageBuilder.Track("evt", AnonId, "user-99", null, PackageVersion, Consent);

Assert.IsTrue(result.ContainsKey("userId"));
Assert.AreEqual("user-99", result["userId"]);
Expand All @@ -55,7 +56,7 @@ public void Track_NonNullUserId_PresentInDict()
[Test]
public void Track_DeviceId_PresentWhenProvided()
{
var result = MessageBuilder.Track("evt", AnonId, null, DeviceId, PackageVersion);
var result = MessageBuilder.Track("evt", AnonId, null, DeviceId, PackageVersion, Consent);

Assert.IsTrue(result.ContainsKey("deviceId"));
Assert.AreEqual(DeviceId, result["deviceId"]);
Expand All @@ -64,15 +65,15 @@ public void Track_DeviceId_PresentWhenProvided()
[Test]
public void Track_DeviceId_AbsentWhenNull()
{
var result = MessageBuilder.Track("evt", AnonId, null, null, PackageVersion);
var result = MessageBuilder.Track("evt", AnonId, null, null, PackageVersion, Consent);

Assert.IsFalse(result.ContainsKey("deviceId"));
}

[Test]
public void Identify_TypeAndIdentityFieldsPresent()
{
var result = MessageBuilder.Identify("anon-42", "user-42", null, "steam", PackageVersion);
var result = MessageBuilder.Identify("anon-42", "user-42", null, "steam", PackageVersion, "full");

Assert.AreEqual("identify", result["type"]);
Assert.AreEqual("anon-42", result["anonymousId"]);
Expand All @@ -83,7 +84,7 @@ public void Identify_TypeAndIdentityFieldsPresent()
[Test]
public void Identify_DeviceId_PresentWhenProvided()
{
var result = MessageBuilder.Identify("anon-42", "user-42", DeviceId, "steam", PackageVersion);
var result = MessageBuilder.Identify("anon-42", "user-42", DeviceId, "steam", PackageVersion, "full");

Assert.IsTrue(result.ContainsKey("deviceId"));
Assert.AreEqual(DeviceId, result["deviceId"]);
Expand All @@ -92,7 +93,7 @@ public void Identify_DeviceId_PresentWhenProvided()
[Test]
public void Alias_AllFourFieldsPresent()
{
var result = MessageBuilder.Alias("from-id", "email", "to-id", "steam", null, PackageVersion);
var result = MessageBuilder.Alias("from-id", "email", "to-id", "steam", null, PackageVersion, "full");

Assert.AreEqual("alias", result["type"]);
Assert.AreEqual("from-id", result["fromId"]);
Expand All @@ -104,7 +105,7 @@ public void Alias_AllFourFieldsPresent()
[Test]
public void Alias_DeviceId_PresentWhenProvided()
{
var result = MessageBuilder.Alias("from-id", "email", "to-id", "steam", DeviceId, PackageVersion);
var result = MessageBuilder.Alias("from-id", "email", "to-id", "steam", DeviceId, PackageVersion, "full");

Assert.IsTrue(result.ContainsKey("deviceId"));
Assert.AreEqual(DeviceId, result["deviceId"]);
Expand All @@ -113,9 +114,9 @@ public void Alias_DeviceId_PresentWhenProvided()
[Test]
public void AllMessages_ContextContainsLibraryAndLibraryVersion()
{
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion);
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion);
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion);
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent);
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion, "full");
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion, "full");

foreach (var msg in new[] { track, identify, alias })
{
Expand All @@ -128,15 +129,41 @@ public void AllMessages_ContextContainsLibraryAndLibraryVersion()
[Test]
public void AllMessages_SurfaceIsUnity()
{
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion);
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion);
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion);
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent);
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion, "full");
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion, "full");

Assert.AreEqual("unity", track["surface"]);
Assert.AreEqual("unity", identify["surface"]);
Assert.AreEqual("unity", alias["surface"]);
}

[Test]
public void AllMessages_ConsentLevelStamped()
{
// Every message carries the consent level it was built under, so the
// backend records the explicit level instead of inferring it.
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion, "anonymous");
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion, "full");
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion, "full");

Assert.AreEqual("anonymous", track["consentLevel"]);
Assert.AreEqual("full", identify["consentLevel"]);
Assert.AreEqual("full", alias["consentLevel"]);
}

[Test]
public void Track_ConsentLevelFull_NoUserId_IsFullButUnidentified()
{
// Full consent does not require a userId (e.g. before Identify()); the
// explicit consentLevel is exactly what distinguishes this from
// anonymous traffic.
var result = MessageBuilder.Track("evt", AnonId, null, null, PackageVersion, "full");

Assert.AreEqual("full", result["consentLevel"]);
Assert.IsFalse(result.ContainsKey("userId"));
}

[Test]
public void AllMessages_MessageId_ParsesAsGuid()
{
Expand All @@ -154,7 +181,7 @@ public void Track_MessageId_IsUniquePerCall()
// Backend deduplicates on messageId; collisions silently drop events.
var ids = new HashSet<string>();
for (var i = 0; i < 1000; i++)
ids.Add((string)MessageBuilder.Track("evt", null, null, null, PackageVersion)["messageId"]);
ids.Add((string)MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent)["messageId"]);
Assert.AreEqual(1000, ids.Count);
}

Expand Down Expand Up @@ -196,24 +223,24 @@ public void AllMessages_Context_LibraryAndLibraryVersionAreNonEmptyStrings()
[Test]
public void Track_TestModeTrue_IncludesTestFlag()
{
var result = MessageBuilder.Track("evt", null, null, null, PackageVersion, testMode: true);
var result = MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent, testMode: true);
Assert.IsTrue(result.ContainsKey("test"), "test field must be present when testMode is true");
Assert.AreEqual(true, result["test"]);
}

[Test]
public void Track_TestModeFalse_ExcludesTestFlag()
{
var result = MessageBuilder.Track("evt", null, null, null, PackageVersion, testMode: false);
var result = MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent, testMode: false);
Assert.IsFalse(result.ContainsKey("test"), "test field must not be present when testMode is false");
}

[Test]
public void AllMessages_TestModeTrue_AllIncludeTestFlag()
{
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion, testMode: true);
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion, testMode: true);
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion, testMode: true);
var track = MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent, testMode: true);
var identify = MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion, "full", testMode: true);
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion, "full", testMode: true);

foreach (var msg in new[] { track, identify, alias })
{
Expand All @@ -224,9 +251,9 @@ public void AllMessages_TestModeTrue_AllIncludeTestFlag()

private static IEnumerable<Dictionary<string, object>> EveryMessageType()
{
yield return MessageBuilder.Track("evt", null, null, null, PackageVersion);
yield return MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion);
yield return MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion);
yield return MessageBuilder.Track("evt", null, null, null, PackageVersion, Consent);
yield return MessageBuilder.Identify(null, "u1", null, "steam", PackageVersion, "full");
yield return MessageBuilder.Alias("f", "t1", "t", "t2", null, PackageVersion, "full");
}
}
}
Loading
Loading