Skip to content

Commit 886a718

Browse files
nattb8claude
andcommitted
feat(audience): remove sandbox routing, add TestMode (SDK-352, SDK-353)
- Remove `TestKeyPrefix`, `SandboxBaseUrl`, and key-prefix-based URL routing from Constants; all keys now resolve to production by default. - Remove `WarnIfKeyEnvironmentMismatch` and the associated log messages. - Simplify `BaseUrl`/`MessagesUrl`/`ConsentUrl`/`DataUrl` signatures to drop the unused `publishableKey` parameter. - Add `TestMode` boolean to `AudienceConfig` (default `false`). When `true`, all outbound events carry `test: true` so the backend can filter them. - Delete `PublishableKeyPrefixTests` (tested removed sandbox logic). - Update `ConstantsTests`, `HttpTransportTests`, `ConsentSyncTests`, `MessageBuilderTests`, and `AudienceConfigTests` to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 26dae71 commit 886a718

12 files changed

Lines changed: 89 additions & 300 deletions

File tree

src/Packages/Audience/Runtime/AudienceConfig.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ public class AudienceConfig
2121
/// Override the default API base URL.
2222
/// </summary>
2323
/// <remarks>
24-
/// When null, publishable keys starting with <c>pk_imapik-test-</c>
25-
/// resolve to Sandbox. All other keys resolve to Production. Set
26-
/// explicitly to target a different backend.
24+
/// When null, all events are sent to the production backend. Set
25+
/// explicitly to target a different backend (e.g. an internal dev
26+
/// environment).
2727
/// </remarks>
2828
public string? BaseUrl { get; set; }
2929

30+
/// <summary>
31+
/// Enables test mode. When <c>true</c>, all events are tagged with
32+
/// <c>test: true</c> so the backend can filter them from production
33+
/// analytics. Default <c>false</c>.
34+
/// </summary>
35+
public bool TestMode { get; set; } = false;
36+
3037
/// <summary>
3138
/// Initial consent level.
3239
/// </summary>

src/Packages/Audience/Runtime/Core/Constants.cs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ namespace Immutable.Audience
44
{
55
internal static class Constants
66
{
7-
internal const string TestKeyPrefix = "pk_imapik-test-";
8-
internal const string SandboxBaseUrl = "https://api.sandbox.immutable.com";
97
internal const string ProductionBaseUrl = "https://api.immutable.com";
108

119
internal const string MessagesPath = "/v1/audience/messages";
@@ -26,21 +24,17 @@ internal static class Constants
2624

2725
internal const string PublishableKeyHeader = "x-immutable-publishable-key";
2826

29-
internal static string MessagesUrl(string? publishableKey, string? baseUrlOverride = null) =>
30-
BaseUrl(publishableKey, baseUrlOverride) + MessagesPath;
31-
internal static string ConsentUrl(string? publishableKey, string? baseUrlOverride = null) =>
32-
BaseUrl(publishableKey, baseUrlOverride) + ConsentPath;
33-
internal static string DataUrl(string? publishableKey, string? baseUrlOverride = null) =>
34-
BaseUrl(publishableKey, baseUrlOverride) + DataPath;
27+
internal static string MessagesUrl(string? baseUrlOverride = null) =>
28+
BaseUrl(baseUrlOverride) + MessagesPath;
29+
internal static string ConsentUrl(string? baseUrlOverride = null) =>
30+
BaseUrl(baseUrlOverride) + ConsentPath;
31+
internal static string DataUrl(string? baseUrlOverride = null) =>
32+
BaseUrl(baseUrlOverride) + DataPath;
3533

36-
// Override wins when non-empty; otherwise test keys map to Sandbox
37-
// and every other key maps to Production. Matches @imtbl/audience.
38-
internal static string BaseUrl(string? publishableKey, string? baseUrlOverride = null)
34+
internal static string BaseUrl(string? baseUrlOverride = null)
3935
{
4036
if (!string.IsNullOrEmpty(baseUrlOverride)) return baseUrlOverride!;
41-
return publishableKey != null && publishableKey.StartsWith(TestKeyPrefix)
42-
? SandboxBaseUrl
43-
: ProductionBaseUrl;
37+
return ProductionBaseUrl;
4438
}
4539
}
4640

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ internal static Dictionary<string, object> Track(
1212
string? anonymousId,
1313
string? userId,
1414
string packageVersion,
15-
Dictionary<string, object>? properties = null)
15+
Dictionary<string, object>? properties = null,
16+
bool testMode = false)
1617
{
17-
var msg = BuildBase(MessageTypes.Track, packageVersion);
18+
var msg = BuildBase(MessageTypes.Track, packageVersion, testMode);
1819
msg["eventName"] = Truncate(eventName, Constants.MaxFieldLength);
1920

2021
if (!string.IsNullOrEmpty(anonymousId))
@@ -37,9 +38,10 @@ internal static Dictionary<string, object> Identify(
3738
string? userId,
3839
string identityType,
3940
string packageVersion,
40-
Dictionary<string, object>? traits = null)
41+
Dictionary<string, object>? traits = null,
42+
bool testMode = false)
4143
{
42-
var msg = BuildBase(MessageTypes.Identify, packageVersion);
44+
var msg = BuildBase(MessageTypes.Identify, packageVersion, testMode);
4345

4446
if (!string.IsNullOrEmpty(anonymousId))
4547
msg["anonymousId"] = Truncate(anonymousId, Constants.MaxFieldLength);
@@ -63,19 +65,20 @@ internal static Dictionary<string, object> Alias(
6365
string fromType,
6466
string toId,
6567
string toType,
66-
string packageVersion)
68+
string packageVersion,
69+
bool testMode = false)
6770
{
68-
var msg = BuildBase(MessageTypes.Alias, packageVersion);
71+
var msg = BuildBase(MessageTypes.Alias, packageVersion, testMode);
6972
msg["fromId"] = Truncate(fromId, Constants.MaxFieldLength);
7073
msg["fromType"] = Truncate(fromType, Constants.MaxFieldLength);
7174
msg["toId"] = Truncate(toId, Constants.MaxFieldLength);
7275
msg["toType"] = Truncate(toType, Constants.MaxFieldLength);
7376
return msg;
7477
}
7578

76-
private static Dictionary<string, object> BuildBase(string type, string packageVersion)
79+
private static Dictionary<string, object> BuildBase(string type, string packageVersion, bool testMode)
7780
{
78-
return new Dictionary<string, object>
81+
var msg = new Dictionary<string, object>
7982
{
8083
[MessageFields.Type] = type,
8184
["messageId"] = Guid.NewGuid().ToString(),
@@ -87,6 +90,9 @@ private static Dictionary<string, object> BuildBase(string type, string packageV
8790
},
8891
["surface"] = Constants.Surface
8992
};
93+
if (testMode)
94+
msg["test"] = true;
95+
return msg;
9096
}
9197

9298
private static string Truncate(string s, int maxLen)

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,6 @@ public static void Init(AudienceConfig config)
195195
return;
196196
}
197197

198-
WarnIfKeyEnvironmentMismatch(config.PublishableKey, config.BaseUrl);
199-
200198
_config = config;
201199
Log.Enabled = config.Debug;
202200
// Persisted consent overrides the config default (prior downgrade survives restart).
@@ -332,7 +330,7 @@ public static void Track(IEvent evt)
332330
var anonymousId = Identity.GetOrCreate(config.PersistentDataPath!, state.Level);
333331
// ToProperties returns a fresh dict per call, so no snapshot needed.
334332
var userId = state.Level == ConsentLevel.Full ? state.UserId : null;
335-
var msg = MessageBuilder.Track(eventName, anonymousId, userId, config.PackageVersion, properties);
333+
var msg = MessageBuilder.Track(eventName, anonymousId, userId, config.PackageVersion, properties, config.TestMode);
336334
EnqueueTrack(msg);
337335
}
338336

@@ -359,7 +357,7 @@ public static void Track(string eventName, Dictionary<string, object>? propertie
359357
var anonymousId = Identity.GetOrCreate(config.PersistentDataPath!, state.Level);
360358
var userId = state.Level == ConsentLevel.Full ? state.UserId : null;
361359
var msg = MessageBuilder.Track(eventName, anonymousId, userId, config.PackageVersion,
362-
SnapshotCallerDict(properties));
360+
SnapshotCallerDict(properties), config.TestMode);
363361
EnqueueTrack(msg);
364362
}
365363

@@ -405,7 +403,7 @@ public static void Identify(string userId, IdentityType identityType, Dictionary
405403

406404
var anonymousId = Identity.GetOrCreate(config.PersistentDataPath!, level);
407405
var msg = MessageBuilder.Identify(anonymousId, userId, identityType.ToLowercaseString(),
408-
config.PackageVersion, SnapshotCallerDict(traits));
406+
config.PackageVersion, SnapshotCallerDict(traits), config.TestMode);
409407
EnqueueIdentity(msg);
410408
}
411409

@@ -436,7 +434,7 @@ public static void Alias(string fromId, IdentityType fromType, string toId, Iden
436434
if (config == null) return;
437435

438436
var msg = MessageBuilder.Alias(fromId, fromType.ToLowercaseString(), toId, toType.ToLowercaseString(),
439-
config.PackageVersion);
437+
config.PackageVersion, config.TestMode);
440438
EnqueueIdentity(msg);
441439
}
442440

@@ -511,7 +509,7 @@ public static Task DeleteData(string? userId = null)
511509
query = "anonymousId=" + Uri.EscapeDataString(anonymousId);
512510
}
513511

514-
var url = Constants.DataUrl(config.PublishableKey, config.BaseUrl) + "?" + query;
512+
var url = Constants.DataUrl(config.BaseUrl) + "?" + query;
515513
var onError = config.OnError;
516514
var publishableKey = config.PublishableKey;
517515
var cancellationToken = _shutdownCancellationSource?.Token ?? CancellationToken.None;
@@ -675,7 +673,7 @@ private static void SyncConsentToBackend(AudienceConfig config, ConsentLevel lev
675673
var client = _controlClient;
676674
if (client == null) return;
677675

678-
var url = Constants.ConsentUrl(config.PublishableKey, config.BaseUrl);
676+
var url = Constants.ConsentUrl(config.BaseUrl);
679677
var publishableKey = config.PublishableKey;
680678
var onError = config.OnError;
681679
var cancellationToken = _shutdownCancellationSource?.Token ?? CancellationToken.None;
@@ -985,25 +983,6 @@ internal static void ResetState()
985983
private static Dictionary<string, object>? SnapshotCallerDict(Dictionary<string, object>? src) =>
986984
src != null ? new Dictionary<string, object>(src) : null;
987985

988-
// Only the exact production/sandbox swap is flagged; custom dev/staging
989-
// URLs are intentional and left alone.
990-
private static void WarnIfKeyEnvironmentMismatch(string publishableKey, string? baseUrlOverride)
991-
{
992-
if (string.IsNullOrEmpty(baseUrlOverride)) return;
993-
994-
var trimmed = baseUrlOverride!.TrimEnd('/');
995-
var isTestKey = publishableKey.StartsWith(Constants.TestKeyPrefix);
996-
997-
if (isTestKey && trimmed == Constants.ProductionBaseUrl)
998-
{
999-
Log.Warn(AudienceLogs.TestKeyAgainstProduction);
1000-
}
1001-
else if (!isTestKey && trimmed == Constants.SandboxBaseUrl)
1002-
{
1003-
Log.Warn(AudienceLogs.NonTestKeyAgainstSandbox);
1004-
}
1005-
}
1006-
1007986
// Checks the current consent inside the drain lock. If consent has
1008987
// since dropped to None the message is discarded. If it dropped to
1009988
// Anonymous the userId is stripped.

src/Packages/Audience/Runtime/Transport/HttpTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ internal HttpTransport(
4141
{
4242
_store = store ?? throw new ArgumentNullException(nameof(store));
4343
_publishableKey = publishableKey ?? throw new ArgumentNullException(nameof(publishableKey));
44-
_url = Constants.MessagesUrl(publishableKey, baseUrlOverride);
44+
_url = Constants.MessagesUrl(baseUrlOverride);
4545
_onError = onError;
4646
// disposeHandler: false so the consumer can reuse their handler
4747
// across Init/Shutdown cycles (matches _controlClient's policy).

src/Packages/Audience/Runtime/Utility/Log.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ internal static class AudienceLogs
5151
"Init called more than once. Ignoring; original config retained. " +
5252
"Call Shutdown() first if reconfiguring is intended.";
5353

54-
internal static readonly string TestKeyAgainstProduction =
55-
$"Publishable key has the test prefix ({Constants.TestKeyPrefix}) but BaseUrl points to production. " +
56-
"The backend will reject events with 401. Either remove the BaseUrl override (test keys " +
57-
"default to sandbox) or use a non-test publishable key.";
58-
59-
internal static readonly string NonTestKeyAgainstSandbox =
60-
"Publishable key is not a test key but BaseUrl points to sandbox. " +
61-
"The backend will reject events with 401. Either remove the BaseUrl override (non-test " +
62-
$"keys default to production) or use a test publishable key ({Constants.TestKeyPrefix}).";
63-
6454
// ---- Track ----
6555

6656
internal const string TrackIEventNull =

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,19 @@ public void SKAdNetworkIds_RoundTrips()
3737
var config = new AudienceConfig { SKAdNetworkIds = ids };
3838
Assert.AreSame(ids, config.SKAdNetworkIds);
3939
}
40+
41+
[Test]
42+
public void TestMode_DefaultsToFalse()
43+
{
44+
var config = new AudienceConfig();
45+
Assert.IsFalse(config.TestMode);
46+
}
47+
48+
[Test]
49+
public void TestMode_RoundTrips()
50+
{
51+
var config = new AudienceConfig { TestMode = true };
52+
Assert.IsTrue(config.TestMode);
53+
}
4054
}
4155
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void SetConsent_FiresPut_WithExpectedBodyShape()
4242
var put = WaitForPut(handler);
4343
var body = JsonReader.DeserializeObject(put.Body);
4444

45-
Assert.AreEqual(Constants.ConsentUrl("pk_imapik-test-key1"), put.Url);
45+
Assert.AreEqual(Constants.ConsentUrl(), put.Url);
4646
Assert.AreEqual("full", body["status"]);
4747
Assert.AreEqual(Constants.ConsentSource, body["source"]);
4848
Assert.IsTrue(body.ContainsKey("anonymousId"));

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

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,22 @@ internal class ConstantsTests
1111
// -----------------------------------------------------------------
1212

1313
[Test]
14-
public void BaseUrl_TestKey_ResolvesToSandbox()
14+
public void BaseUrl_NoOverride_ResolvesToProduction()
1515
{
16-
Assert.AreEqual(Constants.SandboxBaseUrl,
17-
Constants.BaseUrl("pk_imapik-test-abc"));
16+
Assert.AreEqual(Constants.ProductionBaseUrl, Constants.BaseUrl());
1817
}
1918

2019
[Test]
21-
public void BaseUrl_NonTestKey_ResolvesToProduction()
20+
public void BaseUrl_Override_WinsOverDefault()
2221
{
23-
Assert.AreEqual(Constants.ProductionBaseUrl,
24-
Constants.BaseUrl("pk_imapik-prod-abc"));
25-
}
26-
27-
[Test]
28-
public void BaseUrl_NullKey_ResolvesToProduction()
29-
{
30-
Assert.AreEqual(Constants.ProductionBaseUrl,
31-
Constants.BaseUrl(null));
32-
}
33-
34-
[Test]
35-
public void BaseUrl_Override_WinsOverKeyPrefix()
36-
{
37-
// Override wins even for a test-prefixed key that would
38-
// otherwise derive to Sandbox.
3922
const string custom = "https://api.dev.immutable.com";
40-
Assert.AreEqual(custom,
41-
Constants.BaseUrl("pk_imapik-test-abc", custom));
23+
Assert.AreEqual(custom, Constants.BaseUrl(custom));
4224
}
4325

4426
[Test]
45-
public void BaseUrl_EmptyOverride_FallsBackToKeyDerivation()
27+
public void BaseUrl_EmptyOverride_FallsBackToProduction()
4628
{
47-
// Empty-string override is treated as "no override" so the
48-
// key-prefix fallback still kicks in.
49-
Assert.AreEqual(Constants.SandboxBaseUrl,
50-
Constants.BaseUrl("pk_imapik-test-abc", ""));
29+
Assert.AreEqual(Constants.ProductionBaseUrl, Constants.BaseUrl(""));
5130
}
5231

5332
// -----------------------------------------------------------------

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,35 @@ public void AllMessages_Context_LibraryAndLibraryVersionAreNonEmptyStrings()
156156
}
157157
}
158158

159+
[Test]
160+
public void Track_TestModeTrue_IncludesTestFlag()
161+
{
162+
var result = MessageBuilder.Track("evt", null, null, PackageVersion, testMode: true);
163+
Assert.IsTrue(result.ContainsKey("test"), "test field must be present when testMode is true");
164+
Assert.AreEqual(true, result["test"]);
165+
}
166+
167+
[Test]
168+
public void Track_TestModeFalse_ExcludesTestFlag()
169+
{
170+
var result = MessageBuilder.Track("evt", null, null, PackageVersion, testMode: false);
171+
Assert.IsFalse(result.ContainsKey("test"), "test field must not be present when testMode is false");
172+
}
173+
174+
[Test]
175+
public void AllMessages_TestModeTrue_AllIncludeTestFlag()
176+
{
177+
var track = MessageBuilder.Track("evt", null, null, PackageVersion, testMode: true);
178+
var identify = MessageBuilder.Identify(null, "u1", "steam", PackageVersion, testMode: true);
179+
var alias = MessageBuilder.Alias("f", "t1", "t", "t2", PackageVersion, testMode: true);
180+
181+
foreach (var msg in new[] { track, identify, alias })
182+
{
183+
Assert.IsTrue(msg.ContainsKey("test"), $"{msg["type"]} must include test field in test mode");
184+
Assert.AreEqual(true, msg["test"]);
185+
}
186+
}
187+
159188
private static IEnumerable<Dictionary<string, object>> EveryMessageType()
160189
{
161190
yield return MessageBuilder.Track("evt", null, null, PackageVersion);

0 commit comments

Comments
 (0)