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
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ private void UpdateAttButtonGate()
private void RefreshIdentityPanel()
{
_identityUserId.text = ImmutableAudience.UserId ?? "—";
_identityIdentityType.text = _mirrorIdentityType ?? "—";
_identityIdentityType.text = ImmutableAudience.CurrentIdentityType?.ToLowercaseString() ?? "—";
_identityTraits.text = _mirrorTraits != null ? Json.Serialize(_mirrorTraits, 2) : "—";
_identityAliases.text = _mirrorAliases.Count == 0 ? "—" : string.Join("\n", _mirrorAliases);
}
Expand Down
10 changes: 4 additions & 6 deletions examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ public sealed partial class AudienceSample : MonoBehaviour
private bool _initialised;
private Action<string>? _priorSdkLogWriter;

// Sample-side identity mirror. SDK owns UserId; type, traits, and
// aliases are tracked here for the Identity panel.
private string? _mirrorIdentityType;
// Sample-side identity mirror. SDK owns UserId and CurrentIdentityType;
// traits and aliases are tracked here for the Identity panel.
private Dictionary<string, object>? _mirrorTraits;
private readonly List<string> _mirrorAliases = new List<string>();

Expand Down Expand Up @@ -192,7 +191,7 @@ private void OnIdentify() => RunAndLog("identify()", () =>
// before this line runs. The one silent no-op left is consent
// below Full, which the UserId check below still catches.
var accepted = string.Equals(ImmutableAudience.UserId, f.Id, StringComparison.Ordinal);
if (accepted) { _mirrorIdentityType = f.Type; _mirrorTraits = traits; }
if (accepted) _mirrorTraits = traits;
OnSdkStateChanged();
var payload = new Dictionary<string, object>
{
Expand All @@ -210,7 +209,7 @@ private void OnIdentifyTraits() => RunAndLog("identify(traits)", () =>
if (string.IsNullOrEmpty(userId)) throw new InvalidOperationException("no active identity; call Identify first");
var traits = ParseTraits(CaptureTraitsUpdate());
if (traits == null || traits.Count == 0) throw new InvalidOperationException("traits required");
ImmutableAudience.Identify(userId, ParseIdentityType(_mirrorIdentityType), traits);
ImmutableAudience.Identify(userId, ImmutableAudience.CurrentIdentityType ?? IdentityType.Custom, traits);
_mirrorTraits = traits;
OnSdkStateChanged();
return Json.Serialize(traits, 2);
Expand Down Expand Up @@ -382,7 +381,6 @@ private static string RedactPublishableKey(string key)

private void ResetIdentityMirror()
{
_mirrorIdentityType = null;
_mirrorTraits = null;
_mirrorAliases.Clear();
}
Expand Down
12 changes: 7 additions & 5 deletions src/Packages/Audience/Runtime/Core/ConsentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ 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
// the new consent level alongside a leftover user id.
internal sealed record ConsentState(ConsentLevel Level, string? UserId)
// Pairs the consent level with the user id and identity type so all three
// always move together. Updates swap the whole set at once. A reader
// never sees the new consent level alongside a leftover user id or
// identity type. IdentityType has no default: every construction site
// must state it explicitly so a future caller can't silently drop it.
internal sealed record ConsentState(ConsentLevel Level, string? UserId, IdentityType? IdentityType)
{
internal static readonly ConsentState None = new(ConsentLevel.None, null);
internal static readonly ConsentState None = new(ConsentLevel.None, null, null);
}
}
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 @@ -50,6 +50,7 @@ internal static class MessageFields
{
internal const string Type = "type";
internal const string UserId = "userId";
internal const string IdentityType = "identityType";
internal const string DeviceId = "deviceId";
internal const string ConsentLevel = "consentLevel";
internal const string SessionId = "sessionId";
Expand Down
6 changes: 5 additions & 1 deletion src/Packages/Audience/Runtime/Events/MessageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal static Dictionary<string, object> Track(
string eventName,
string? anonymousId,
string? userId,
string? identityType,
string? deviceId,
string packageVersion,
string consentLevel,
Expand All @@ -28,6 +29,9 @@ internal static Dictionary<string, object> Track(
if (!string.IsNullOrEmpty(userId))
msg[MessageFields.UserId] = Truncate(userId, Constants.MaxFieldLength);

if (!string.IsNullOrEmpty(identityType))
msg[MessageFields.IdentityType] = Truncate(identityType, Constants.MaxFieldLength);

if (!string.IsNullOrEmpty(deviceId))
msg[MessageFields.DeviceId] = Truncate(deviceId, Constants.MaxFieldLength);

Expand Down Expand Up @@ -65,7 +69,7 @@ internal static Dictionary<string, object> Identify(
if (!string.IsNullOrEmpty(deviceId))
msg[MessageFields.DeviceId] = Truncate(deviceId, Constants.MaxFieldLength);

msg["identityType"] = Truncate(identityType, Constants.MaxFieldLength);
msg[MessageFields.IdentityType] = Truncate(identityType, Constants.MaxFieldLength);

if (traits != null && traits.Count > 0)
{
Expand Down
34 changes: 25 additions & 9 deletions src/Packages/Audience/Runtime/ImmutableAudience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ public static class ImmutableAudience
/// </remarks>
public static string? UserId => _state.UserId;

/// <summary>
/// The identity provider from the most recent
/// <see cref="Identify(string, IdentityType, Dictionary{string, object})"/>
/// call.
/// </summary>
/// <remarks>
/// Null after <see cref="Reset"/> or when consent is below
/// <see cref="ConsentLevel.Full"/>.
/// </remarks>
public static IdentityType? CurrentIdentityType => _state.IdentityType;

/// <summary>
/// An anonymous, persistent ID for this device.
/// </summary>
Expand Down Expand Up @@ -230,7 +241,7 @@ public static void Init(AudienceConfig config)
Log.Enabled = config.Debug;
// Persisted consent overrides the config default (prior downgrade survives restart).
var initialLevel = ConsentStore.Load(config.PersistentDataPath) ?? config.Consent;
_state = new ConsentState(initialLevel, null);
_state = new ConsentState(initialLevel, null, null);

_store = new DiskStore(config.PersistentDataPath);
_queue = new EventQueue(_store, config.FlushIntervalSeconds, config.FlushSize);
Expand Down Expand Up @@ -419,7 +430,8 @@ private static void EnqueueTrackedEvent(
var anonymousId = Identity.GetOrCreate(config.PersistentDataPath!, state.Level);
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,
var identityType = state.Level == ConsentLevel.Full ? state.IdentityType?.ToLowercaseString() : null;
var msg = MessageBuilder.Track(eventName, anonymousId, userId, identityType, deviceId, Constants.LibraryVersion,
state.Level.ToLowercaseString(), properties, sessionId, config.TestMode, timestampOverride);
EnqueueTrack(msg);
}
Expand Down Expand Up @@ -479,7 +491,7 @@ public static void Identify(string userId, IdentityType identityType, Dictionary
}
config = _config;
if (config == null) return;
_state = current with { UserId = userId };
_state = current with { UserId = userId, IdentityType = identityType };
}

var anonymousId = Identity.GetOrCreate(config.PersistentDataPath!, level);
Expand Down Expand Up @@ -564,7 +576,7 @@ public static void Reset()

oldSession = _session;
queueForPurge = _queue;
_state = _state with { UserId = null };
_state = _state with { UserId = null, IdentityType = null };

// Swap under the lock so racing SetConsent/OnPause/OnResume see
// either the old, the new, or null; never a torn reference.
Expand Down Expand Up @@ -642,12 +654,13 @@ public static void SetConsent(ConsentLevel level)
previous = previousState.Level;
if (level == previous) return;

// Atomic swap: Level + UserId publish together. Drop UserId on
// any downgrade out of Full so a racing Track/Identify cannot
// observe (Anonymous, oldUserId).
// Atomic swap: Level + UserId + IdentityType publish together.
// Drop both on any downgrade out of Full so a racing
// Track/Identify cannot observe (Anonymous, oldUserId).
_state = new ConsentState(
level,
level == ConsentLevel.Full ? previousState.UserId : null);
level == ConsentLevel.Full ? previousState.UserId : null,
level == ConsentLevel.Full ? previousState.IdentityType : null);

if (level == ConsentLevel.None)
{
Expand Down Expand Up @@ -931,7 +944,7 @@ public static void Shutdown()

_config = null;
_store = null;
_state = _state with { UserId = null };
_state = _state with { UserId = null, IdentityType = null };
}

// Phase 2 outside _initLock: end session, drain timers, flush, dispose.
Expand Down Expand Up @@ -1026,7 +1039,10 @@ private static void EnqueueTrack(Dictionary<string, object>? msg)
if (!state.Level.CanTrack()) return null;
m[MessageFields.ConsentLevel] = state.Level.ToLowercaseString();
if (state.Level != ConsentLevel.Full)
{
m.Remove(MessageFields.UserId);
m.Remove(MessageFields.IdentityType);
}
return m;
});
}
Expand Down
Loading
Loading