Skip to content

Commit d5d8099

Browse files
refactor(audience): make identityType mandatory on Identify and Alias
CDP requires identityType on every identify / alias event so it can match records to the correct identity namespace during data deletion. Enforce the invariant through the type system end to end: - `Identify(userId, identityType, traits?)` — identityType is non-nullable. The null/empty warn-and-drop block is gone; the type signature no longer lies about optionality. - `Alias(fromId, fromType, toId, toType)` — fromType and toType are non-nullable. Same block removed. - `MessageBuilder.Identify` — identityType parameter is non-nullable and always emitted; the conditional that omitted an empty field is gone so the wire shape cannot drift. - `IdentityType.ToLowercaseString()` — casting to an out-of-range enum value now throws `ArgumentOutOfRangeException` rather than returning null. A silent null used to leak through to a dropped event; now the programmer error fails loud at the call site. Tests repointed: invalid-enum-cast cases assert the throw, and MessageBuilder tests that passed `null` for identityType now pass a valid value since the parameter is no longer nullable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 843e842 commit d5d8099

6 files changed

Lines changed: 39 additions & 44 deletions

File tree

src/Packages/Audience/Runtime/Events/MessageBuilder.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal static Dictionary<string, object> Track(
3535
internal static Dictionary<string, object> Identify(
3636
string? anonymousId,
3737
string? userId,
38-
string? identityType,
38+
string identityType,
3939
string packageVersion,
4040
Dictionary<string, object>? traits = null)
4141
{
@@ -47,8 +47,7 @@ internal static Dictionary<string, object> Identify(
4747
if (!string.IsNullOrEmpty(userId))
4848
msg[MessageFields.UserId] = Truncate(userId, Constants.MaxFieldLength);
4949

50-
if (!string.IsNullOrEmpty(identityType))
51-
msg["identityType"] = Truncate(identityType, Constants.MaxFieldLength);
50+
msg["identityType"] = Truncate(identityType, Constants.MaxFieldLength);
5251

5352
if (traits != null && traits.Count > 0)
5453
{

src/Packages/Audience/Runtime/IdentityType.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ public enum IdentityType
1717

1818
internal static class IdentityTypeExtensions
1919
{
20-
// Returns null on unknown casts. The string overloads of Identify /
21-
// Alias check for null/empty and drop + warn, so an out-of-range
22-
// cast surfaces as a dropped event, not a corrupt wire payload.
23-
internal static string? ToLowercaseString(this IdentityType type) => type switch
20+
// Throws on unknown casts. The CDP pipeline requires identityType on
21+
// every identify / alias event to match records to the correct
22+
// identity namespace during data deletion, so an out-of-range cast
23+
// must fail loudly rather than ship an event with a missing or
24+
// empty namespace.
25+
internal static string ToLowercaseString(this IdentityType type) => type switch
2426
{
2527
IdentityType.Passport => "passport",
2628
IdentityType.Steam => "steam",
@@ -30,7 +32,8 @@ internal static class IdentityTypeExtensions
3032
IdentityType.Discord => "discord",
3133
IdentityType.Email => "email",
3234
IdentityType.Custom => "custom",
33-
_ => null,
35+
_ => throw new System.ArgumentOutOfRangeException(nameof(type), type,
36+
"Unknown IdentityType value; cast an out-of-range value."),
3437
};
3538
}
3639
}

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ public static void Identify(string userId, IdentityType identityType, Dictionary
175175

176176
// Attach a known user id to subsequent events. String overload for
177177
// providers not in IdentityType.
178-
public static void Identify(string userId, string? identityType, Dictionary<string, object>? traits = null)
178+
//
179+
// identityType is required: CDP uses it to match identify events to
180+
// the correct identity namespace when processing data-deletion
181+
// requests, so an event without one cannot be cleaned up.
182+
public static void Identify(string userId, string identityType, Dictionary<string, object>? traits = null)
179183
{
180184
if (!_initialized) return;
181185

@@ -185,11 +189,6 @@ public static void Identify(string userId, string? identityType, Dictionary<stri
185189
Log.Warn("Identify called with null or empty userId — dropping.");
186190
return;
187191
}
188-
if (string.IsNullOrEmpty(identityType))
189-
{
190-
Log.Warn("Identify called with null or empty identityType — dropping.");
191-
return;
192-
}
193192
if (_consent != ConsentLevel.Full)
194193
{
195194
Log.Warn($"Identify discarded — requires Full consent, current is {_consent}");
@@ -213,7 +212,11 @@ public static void Alias(string fromId, IdentityType fromType, string toId, Iden
213212

214213
// Link two user ids for the same player. String overload for
215214
// providers not in IdentityType.
216-
public static void Alias(string fromId, string? fromType, string toId, string? toType)
215+
//
216+
// fromType and toType are required: CDP uses them to match alias
217+
// events to the correct identity namespaces when processing
218+
// data-deletion requests.
219+
public static void Alias(string fromId, string fromType, string toId, string toType)
217220
{
218221
if (!_initialized) return;
219222

@@ -222,11 +225,6 @@ public static void Alias(string fromId, string? fromType, string toId, string? t
222225
Log.Warn("Alias called with null or empty fromId/toId — dropping.");
223226
return;
224227
}
225-
if (string.IsNullOrEmpty(fromType) || string.IsNullOrEmpty(toType))
226-
{
227-
Log.Warn("Alias called with null or empty fromType/toType — dropping.");
228-
return;
229-
}
230228
if (_consent != ConsentLevel.Full)
231229
{
232230
Log.Warn($"Alias discarded — requires Full consent, current is {_consent}");

src/Packages/Audience/Tests/Runtime/Events/MessageBuilderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void Alias_AllFourFieldsPresent()
7575
public void AllMessages_ContextContainsLibraryAndLibraryVersion()
7676
{
7777
var track = MessageBuilder.Track("evt", null, null, PackageVersion);
78-
var identify = MessageBuilder.Identify(null, "u1", null, PackageVersion);
78+
var identify = MessageBuilder.Identify(null, "u1", "steam", PackageVersion);
7979
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", PackageVersion);
8080

8181
foreach (var msg in new[] { track, identify, alias })
@@ -90,7 +90,7 @@ public void AllMessages_ContextContainsLibraryAndLibraryVersion()
9090
public void AllMessages_SurfaceIsUnity()
9191
{
9292
var track = MessageBuilder.Track("evt", null, null, PackageVersion);
93-
var identify = MessageBuilder.Identify(null, "u1", null, PackageVersion);
93+
var identify = MessageBuilder.Identify(null, "u1", "steam", PackageVersion);
9494
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", PackageVersion);
9595

9696
Assert.AreEqual("unity", track["surface"]);

src/Packages/Audience/Tests/Runtime/IdentityTypeTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using NUnit.Framework;
23

34
namespace Immutable.Audience.Tests
@@ -19,14 +20,15 @@ public void ToLowercaseString_MapsEachEnumValueToLowercaseBackendString(Identity
1920
}
2021

2122
[Test]
22-
public void ToLowercaseString_UnknownValue_ReturnsNull()
23+
public void ToLowercaseString_UnknownValue_Throws()
2324
{
24-
// Never-throw contract: an out-of-range cast should not surface an
25-
// exception on the game thread. Callers drop the event via their
26-
// null/empty check instead.
25+
// CDP matches identify / alias events by identityType during data
26+
// deletion; an out-of-range cast would otherwise ship an event
27+
// with an empty namespace that CDP cannot clean up. Fail loud so
28+
// the programmer error surfaces at the call site instead.
2729
var invalid = (IdentityType)999;
2830

29-
Assert.IsNull(invalid.ToLowercaseString());
31+
Assert.Throws<ArgumentOutOfRangeException>(() => invalid.ToLowercaseString());
3032
}
3133
}
3234
}

src/Packages/Audience/Tests/Runtime/ImmutableAudienceTests.cs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -133,36 +133,29 @@ public void Identify_NullUserId_DoesNotEnqueue()
133133
}
134134

135135
[Test]
136-
public void Identify_InvalidIdentityTypeCast_DoesNotThrow_AndDropsEvent()
136+
public void Identify_InvalidIdentityTypeCast_Throws()
137137
{
138138
ImmutableAudience.Init(MakeConfig(ConsentLevel.Full));
139139

140140
var invalid = (IdentityType)999;
141141

142-
Assert.DoesNotThrow(() => ImmutableAudience.Identify("user1", invalid));
143-
144-
ImmutableAudience.Shutdown();
145-
var queueDir = AudiencePaths.QueueDir(_testDir);
146-
var contents = Directory.GetFiles(queueDir, "*.json").Select(File.ReadAllText);
147-
Assert.IsFalse(contents.Any(c => c.Contains("\"identify\"")),
148-
"invalid enum cast must drop the identify event, not enqueue it");
142+
Assert.Throws<ArgumentOutOfRangeException>(
143+
() => ImmutableAudience.Identify("user1", invalid),
144+
"invalid enum cast must throw so a broken call fails loud rather than " +
145+
"shipping an identify event CDP cannot match for deletion");
149146
}
150147

151148
[Test]
152-
public void Alias_InvalidIdentityTypeCast_DoesNotThrow_AndDropsEvent()
149+
public void Alias_InvalidIdentityTypeCast_Throws()
153150
{
154151
ImmutableAudience.Init(MakeConfig(ConsentLevel.Full));
155152

156153
var invalid = (IdentityType)999;
157154

158-
Assert.DoesNotThrow(() =>
159-
ImmutableAudience.Alias("fromId", invalid, "toId", IdentityType.Steam));
160-
161-
ImmutableAudience.Shutdown();
162-
var queueDir = AudiencePaths.QueueDir(_testDir);
163-
var contents = Directory.GetFiles(queueDir, "*.json").Select(File.ReadAllText);
164-
Assert.IsFalse(contents.Any(c => c.Contains("\"alias\"")),
165-
"invalid enum cast must drop the alias event, not enqueue it");
155+
Assert.Throws<ArgumentOutOfRangeException>(
156+
() => ImmutableAudience.Alias("fromId", invalid, "toId", IdentityType.Steam),
157+
"invalid enum cast must throw so a broken alias call fails loud rather " +
158+
"than shipping an event CDP cannot match for deletion");
166159
}
167160

168161
[Test]

0 commit comments

Comments
 (0)