Skip to content

Commit 7f3b250

Browse files
feat(audience): auto-include Unity context on game_launch (SDK-147)
Plan §5.1 and the Event Reference require game_launch to ship with platform, version, buildGuid, unityVersion auto-detected. Until the Day 4 DeviceCollector lands, game_launch was shipping with only the studio-supplied distributionPlatform. - ImmutableAudience gets a new internal seam, LaunchContextProvider, that returns a Dictionary<string, object> merged into game_launch properties. Core stays pure C# - the provider is installed from the Unity layer so no UnityEngine import leaks into Runtime/. - FireGameLaunch wraps the provider call in try/catch with a clear warning. A buggy provider must never prevent the launch event from firing - game_launch is the most load-bearing attribution event. - AudienceUnityHooks installs a default provider that reads Application.platform, Application.version, Application.buildGUID, Application.unityVersion. DeviceCollector (Day 4) can replace or extend this without re-wiring. - config.DistributionPlatform keeps winning over any provider value: studios set it explicitly because Unity cannot auto-detect the distribution store, and that value is the one the attribution pipeline expects. Three new tests: provider fields make it onto the event, config overrides provider for distributionPlatform, and a throwing provider doesn't skip the event. 154 passing. Linear: SDK-147 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dd2ff8b commit 7f3b250

3 files changed

Lines changed: 104 additions & 1 deletion

File tree

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public static class ImmutableAudience
2828
// PersistentDataPath on the config.
2929
internal static Func<string> DefaultPersistentDataPathProvider;
3030

31+
// AudienceUnityHooks sets this so game_launch can auto-include
32+
// Unity context without the core referencing UnityEngine.
33+
internal static Func<Dictionary<string, object>> LaunchContextProvider;
34+
3135
// Starts the SDK. Call once at launch.
3236
public static void Init(AudienceConfig config)
3337
{
@@ -460,6 +464,8 @@ public static void Shutdown()
460464
// Shuts down (if initialised) and clears per-session state so a
461465
// fresh Init starts clean. Used on test teardown and by Unity
462466
// SubsystemRegistration to survive "disable domain reload".
467+
// LaunchContextProvider is not cleared: AudienceUnityHooks
468+
// re-assigns it on the same SubsystemRegistration call.
463469
internal static void ResetState()
464470
{
465471
if (_initialized)
@@ -549,10 +555,32 @@ private static void FireGameLaunch(AudienceConfig config, ConsentLevel consentAt
549555

550556
var properties = new Dictionary<string, object>();
551557

558+
// Unity-side auto-detected context (platform, version, buildGuid,
559+
// unityVersion) from AudienceUnityHooks. Core stays pure C#; the
560+
// Unity layer fills these via LaunchContextProvider.
561+
var provider = LaunchContextProvider;
562+
if (provider != null)
563+
{
564+
Dictionary<string, object> unityContext = null;
565+
try { unityContext = provider(); }
566+
catch (Exception ex)
567+
{
568+
Log.Warn($"LaunchContextProvider threw {ex.GetType().Name}: {ex.Message}. " +
569+
"game_launch will ship without auto-detected Unity context.");
570+
}
571+
572+
if (unityContext != null)
573+
{
574+
foreach (var kvp in unityContext)
575+
properties[kvp.Key] = kvp.Value;
576+
}
577+
}
578+
579+
// Config-supplied distributionPlatform wins over any provider value;
580+
// studios set it explicitly because Unity cannot auto-detect the store.
552581
if (config.DistributionPlatform != null)
553582
properties["distributionPlatform"] = config.DistributionPlatform;
554583

555-
// Device-derived fields (platform, version, buildGuid, unityVersion) land with DeviceCollector.
556584
Track("game_launch", properties.Count > 0 ? properties : null);
557585
}
558586
}

src/Packages/Audience/Runtime/Unity/AudienceUnityHooks.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using UnityEngine;
23

34
namespace Immutable.Audience.Unity
@@ -15,8 +16,18 @@ private static void Install()
1516
Application.quitting += ImmutableAudience.Shutdown;
1617

1718
ImmutableAudience.DefaultPersistentDataPathProvider = () => Application.persistentDataPath;
19+
ImmutableAudience.LaunchContextProvider = BuildLaunchContext;
1820

1921
if (Log.Writer == null) Log.Writer = Debug.Log;
2022
}
23+
24+
private static Dictionary<string, object> BuildLaunchContext() =>
25+
new Dictionary<string, object>
26+
{
27+
["platform"] = Application.platform.ToString(),
28+
["version"] = Application.version,
29+
["buildGuid"] = Application.buildGUID,
30+
["unityVersion"] = Application.unityVersion,
31+
};
2132
}
2233
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public void SetUp()
2626
public void TearDown()
2727
{
2828
ImmutableAudience.ResetState();
29+
ImmutableAudience.LaunchContextProvider = null;
2930
ImmutableAudience.DefaultPersistentDataPathProvider = null;
3031
Identity.Reset(_testDir);
3132
if (Directory.Exists(_testDir))
@@ -659,6 +660,69 @@ public void Init_ConsentNone_DoesNotFireGameLaunch()
659660
Assert.IsFalse(contents.Any(c => c.Contains("\"game_launch\"")));
660661
}
661662

663+
[Test]
664+
public void Init_GameLaunch_IncludesLaunchContextProviderFields()
665+
{
666+
ImmutableAudience.LaunchContextProvider = () => new Dictionary<string, object>
667+
{
668+
["platform"] = "WindowsPlayer",
669+
["version"] = "1.2.3",
670+
["buildGuid"] = "a1b2c3d4e5f6",
671+
["unityVersion"] = "2022.3.20f1",
672+
};
673+
674+
ImmutableAudience.Init(MakeConfig());
675+
ImmutableAudience.Shutdown();
676+
677+
var queueDir = AudiencePaths.QueueDir(_testDir);
678+
var launchFile = Directory.GetFiles(queueDir, "*.json")
679+
.Select(File.ReadAllText)
680+
.FirstOrDefault(c => c.Contains("\"game_launch\""));
681+
Assert.IsNotNull(launchFile, "game_launch should have been enqueued");
682+
StringAssert.Contains("\"platform\":\"WindowsPlayer\"", launchFile);
683+
StringAssert.Contains("\"version\":\"1.2.3\"", launchFile);
684+
StringAssert.Contains("\"buildGuid\":\"a1b2c3d4e5f6\"", launchFile);
685+
StringAssert.Contains("\"unityVersion\":\"2022.3.20f1\"", launchFile);
686+
}
687+
688+
[Test]
689+
public void Init_GameLaunch_ConfigDistributionPlatformOverridesProvider()
690+
{
691+
ImmutableAudience.LaunchContextProvider = () => new Dictionary<string, object>
692+
{
693+
["distributionPlatform"] = "provider_value",
694+
};
695+
696+
var config = MakeConfig();
697+
config.DistributionPlatform = DistributionPlatforms.Steam;
698+
ImmutableAudience.Init(config);
699+
ImmutableAudience.Shutdown();
700+
701+
var queueDir = AudiencePaths.QueueDir(_testDir);
702+
var launchFile = Directory.GetFiles(queueDir, "*.json")
703+
.Select(File.ReadAllText)
704+
.First(c => c.Contains("\"game_launch\""));
705+
StringAssert.Contains("\"distributionPlatform\":\"steam\"", launchFile);
706+
Assert.IsFalse(launchFile.Contains("provider_value"),
707+
"config.DistributionPlatform should win over the provider's value");
708+
}
709+
710+
[Test]
711+
public void Init_GameLaunch_ProviderThrows_StillFiresEvent()
712+
{
713+
ImmutableAudience.LaunchContextProvider = () =>
714+
throw new InvalidOperationException("provider exploded");
715+
716+
Assert.DoesNotThrow(() => ImmutableAudience.Init(MakeConfig()));
717+
ImmutableAudience.Shutdown();
718+
719+
var queueDir = AudiencePaths.QueueDir(_testDir);
720+
var contents = Directory.GetFiles(queueDir, "*.json")
721+
.Select(File.ReadAllText).ToList();
722+
Assert.IsTrue(contents.Any(c => c.Contains("\"game_launch\"")),
723+
"game_launch must still ship when the context provider throws");
724+
}
725+
662726
// -----------------------------------------------------------------
663727
// Shutdown
664728
// -----------------------------------------------------------------

0 commit comments

Comments
 (0)