Skip to content

Commit 97fd5d1

Browse files
nattb8claude
andcommitted
feat(audience): remove sandbox routing, add TestMode (SDK-352, SDK-353)
Sandbox removal (SDK-352): - Remove TestKeyPrefix, SandboxBaseUrl, and key-prefix-based URL routing. All keys now resolve to https://api.immutable.com by default; BaseUrl override still works for internal dev environments. - Remove WarnIfKeyEnvironmentMismatch and its log messages. - Simplify BaseUrl/URL helper signatures (drop unused publishableKey param). - Delete PublishableKeyPrefixTests.cs and its .meta file. - Update ConstantsTests, ConsentSyncTests, HttpTransportTests. Test mode (SDK-353): - Add TestMode bool to AudienceConfig (default false). When true, every outbound event carries test: true so the backend can filter test traffic. - Add testMode param to MessageBuilder.Track/Identify/Alias; all call sites in ImmutableAudience pass config.TestMode. - Add TestMode tests to AudienceConfigTests and MessageBuilderTests. Sample app: - Add test-mode Toggle to UXML (default true) with tick-label workaround matching the existing debug toggle. - Wire TestMode through InitForm → BuildAudienceConfig → BuildConfigEcho. - Apply same tick-label injection to test-mode and mobile-attribution toggles. - Remove prod-warning banner (permanently true now that all keys go to prod). - Replace SandboxBaseUrl with ExplicitBaseUrl in test constants; update live-fire tests and stale comments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 26dae71 commit 97fd5d1

18 files changed

Lines changed: 133 additions & 389 deletions

examples/audience/Assets/SampleApp/Resources/AudienceSample.uxml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33

44
<ui:VisualElement name="root" class="root">
55

6-
<ui:Label name="prod-warning" class="prod-warning hidden"
7-
text="⚠ USING PROD ENDPOINT — ACTIONS HERE AFFECT PROD DATA ⚠" />
8-
96
<ui:VisualElement name="sticky-outer" class="sticky-outer">
107
<ui:VisualElement class="sticky-top-main">
118

@@ -78,7 +75,7 @@
7875
<ui:VisualElement class="field placeholder-host">
7976
<ui:Label class="field-label" text="PUBLISHABLE KEY" />
8077
<ui:TextField name="publishable-key" />
81-
<ui:Label class="field-placeholder" text="pk_imapik-test-yourkey" />
78+
<ui:Label class="field-placeholder" text="pk_imapik-yourkey" />
8279
</ui:VisualElement>
8380

8481
<ui:VisualElement class="field">
@@ -99,6 +96,12 @@
9996
text="Mirror SDK internal log output into the in-page event log below." />
10097
</ui:VisualElement>
10198

99+
<ui:VisualElement class="field">
100+
<ui:Toggle name="test-mode" label="TEST MODE" value="true" />
101+
<ui:Label class="helper-text below-field"
102+
text="Tags all outbound events with test: true so the backend can filter them from production analytics." />
103+
</ui:VisualElement>
104+
102105
<ui:VisualElement name="mobile-attribution-field" class="field">
103106
<ui:Toggle name="enable-mobile-attribution" label="MOBILE ATTRIBUTION" />
104107
<ui:Label class="helper-text below-field"
@@ -120,7 +123,7 @@
120123
<ui:TextField name="base-url" />
121124
<ui:Label class="field-placeholder" text="(derived from key)" />
122125
<ui:Label class="helper-text below-field"
123-
text="Bypass the SDK's prefix-based routing and target a specific backend. Leave empty to derive from the publishable key." />
126+
text="Target a specific backend. Leave empty to use the production endpoint." />
124127
</ui:VisualElement>
125128

126129
<ui:VisualElement class="advanced-grid">

examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ private static readonly (string TabId, string PanelId)[] Tabs =
5151
// ---- UXML element fields (Setup tab) ----
5252

5353
private TextField _publishableKey, _baseUrl, _flushInterval, _flushSize;
54+
private Toggle _testMode;
5455
private DropdownField _initialConsent;
5556
private Toggle _debug, _enableMobileAttribution;
5657
private Button _btnInit, _btnFlush, _btnReset, _btnShutdown, _btnDeleteData, _btnRequestAtt;
@@ -75,7 +76,7 @@ private static readonly (string TabId, string PanelId)[] Tabs =
7576
// ---- UXML element fields (Tabs + status bar + header) ----
7677

7778
private readonly List<Button> _tabButtons = new List<Button>();
78-
private Label _prodWarning, _sdkVersionLabel, _titleLabel;
79+
private Label _sdkVersionLabel, _titleLabel;
7980
private Label _statusEndpoint, _statusConsent, _statusAnon, _statusUser, _statusSession, _statusQueue;
8081

8182
// ---- UXML element fields (Log pane) ----
@@ -164,7 +165,7 @@ private T Require<T>(string name) where T : VisualElement =>
164165

165166
private void BindElements()
166167
{
167-
_prodWarning = Require<Label>("prod-warning");
168+
168169
_sdkVersionLabel = Require<Label>("sdk-version");
169170
_titleLabel = _root.Q<Label>(className: "title");
170171

@@ -181,17 +182,19 @@ private void BindElements()
181182
_baseUrl = Require<TextField>("base-url");
182183
_initialConsent = Require<DropdownField>("initial-consent");
183184
_debug = Require<Toggle>("debug");
185+
_testMode = Require<Toggle>("test-mode");
184186
_enableMobileAttribution = Require<Toggle>("enable-mobile-attribution");
185187
// Inject a tick Label — Unity 2021.3 runtime panels render the
186188
// checked state as a plain coloured square otherwise. USS hides
187189
// the tick when unchecked.
188-
var debugCheckmark = _debug.Q<VisualElement>(className: "unity-toggle__checkmark");
189-
if (debugCheckmark != null)
190+
foreach (var toggle in new[] { _debug, _testMode, _enableMobileAttribution })
190191
{
192+
var checkmark = toggle.Q<VisualElement>(className: "unity-toggle__checkmark");
193+
if (checkmark == null) continue;
191194
var tick = new Label("✓");
192195
tick.AddToClassList("debug-tick");
193196
tick.pickingMode = PickingMode.Ignore;
194-
debugCheckmark.Add(tick);
197+
checkmark.Add(tick);
195198
}
196199
_flushInterval = Require<TextField>("flush-interval");
197200
_flushSize = Require<TextField>("flush-size");
@@ -610,15 +613,9 @@ private void RefreshStatusBar()
610613
var key = (_publishableKey.value ?? "").Trim();
611614
var overrideUrl = (_baseUrl?.value ?? "").Trim();
612615
bool keyEmpty = string.IsNullOrEmpty(key);
613-
bool isTest = !keyEmpty && IsTestKey(key);
614616
bool hasOverride = !string.IsNullOrEmpty(overrideUrl);
615-
// BaseUrl override skips prefix-based routing, so the prod-warning
616-
// rule no longer applies (studio is in explicit-target mode).
617-
string? derivedFromKey = keyEmpty ? null : (isTest ? Constants.SandboxBaseUrl : Constants.ProductionBaseUrl);
618-
string? endpoint = hasOverride ? overrideUrl : derivedFromKey;
619-
bool warnState = hasOverride || (!keyEmpty && !isTest);
620-
SetStatusCell(_statusEndpoint, endpoint, warnState ? "state-warn" : "state-ok");
621-
_prodWarning.EnableInClassList("hidden", hasOverride || keyEmpty || isTest);
617+
string? endpoint = hasOverride ? overrideUrl : (keyEmpty ? null : Constants.ProductionBaseUrl);
618+
SetStatusCell(_statusEndpoint, endpoint, hasOverride ? "state-warn" : "state-ok");
622619

623620
var consent = _initialised ? ImmutableAudience.CurrentConsent : ConsentOrder[Mathf.Clamp(_initialConsent?.index ?? 0, 0, ConsentOrder.Length - 1)];
624621
int cIdx = Array.IndexOf(ConsentOrder, consent);
@@ -676,16 +673,18 @@ internal readonly struct InitForm
676673
public readonly string BaseUrl;
677674
public readonly ConsentLevel Consent;
678675
public readonly bool Debug;
676+
public readonly bool TestMode;
679677
public readonly bool EnableMobileAttribution;
680678
public readonly int? FlushIntervalMs;
681679
public readonly int? FlushSize;
682680

683-
public InitForm(string publishableKey, string baseUrl, ConsentLevel consent, bool debug, bool enableMobileAttribution, int? flushIntervalMs, int? flushSize)
681+
public InitForm(string publishableKey, string baseUrl, ConsentLevel consent, bool debug, bool testMode, bool enableMobileAttribution, int? flushIntervalMs, int? flushSize)
684682
{
685683
PublishableKey = publishableKey;
686684
BaseUrl = baseUrl;
687685
Consent = consent;
688686
Debug = debug;
687+
TestMode = testMode;
689688
EnableMobileAttribution = enableMobileAttribution;
690689
FlushIntervalMs = flushIntervalMs;
691690
FlushSize = flushSize;
@@ -702,6 +701,7 @@ internal InitForm CaptureInitForm()
702701
baseUrl: (_baseUrl.value ?? "").Trim(),
703702
consent: ConsentOrder[consentIdx],
704703
debug: _debug.value,
704+
testMode: _testMode.value,
705705
enableMobileAttribution: _enableMobileAttribution.value,
706706
flushIntervalMs: flushIntervalMs,
707707
flushSize: flushSize);
@@ -780,9 +780,6 @@ private bool IsAliasReady()
780780
&& (fromId != toId || (_aliasFromType.value ?? "") != (_aliasToType.value ?? ""));
781781
}
782782

783-
private static bool IsTestKey(string? key) =>
784-
!string.IsNullOrEmpty(key) && key!.StartsWith(Constants.TestKeyPrefix, StringComparison.Ordinal);
785-
786783
private static void FlashCopied(VisualElement ve)
787784
{
788785
ve.AddToClassList("copied");

examples/audience/Assets/SampleApp/Scripts/AudienceSample.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,8 @@ private void OnSdkStateChanged()
331331
// ---- Config builders ----
332332

333333
// Maps the captured Setup form to AudienceConfig. BaseUrl null → SDK
334-
// derives the endpoint from the publishable key prefix (test → sandbox,
335-
// else production). The flushInterval clamp emits a warn row when
336-
// the user requests <1s.
334+
// targets the production endpoint. The flushInterval clamp emits a warn
335+
// row when the user requests <1s.
337336
private AudienceConfig BuildAudienceConfig(InitForm form, Action<AudienceError> onError)
338337
{
339338
var config = new AudienceConfig
@@ -342,6 +341,7 @@ private AudienceConfig BuildAudienceConfig(InitForm form, Action<AudienceError>
342341
BaseUrl = string.IsNullOrEmpty(form.BaseUrl) ? null : form.BaseUrl,
343342
Consent = form.Consent,
344343
Debug = form.Debug,
344+
TestMode = form.TestMode,
345345
EnableMobileAttribution = form.EnableMobileAttribution,
346346
OnError = onError,
347347
};
@@ -364,6 +364,7 @@ private static Dictionary<string, object> BuildConfigEcho(AudienceConfig config)
364364
{
365365
["consent"] = config.Consent.ToString(),
366366
["debug"] = config.Debug,
367+
["testMode"] = config.TestMode,
367368
["enableMobileAttribution"] = config.EnableMobileAttribution,
368369
["flushIntervalSeconds"] = config.FlushIntervalSeconds,
369370
["flushSize"] = config.FlushSize,
@@ -377,7 +378,7 @@ private static Dictionary<string, object> BuildConfigEcho(AudienceConfig config)
377378
return echo;
378379
}
379380

380-
// Keeps the pk_imapik-test- / pk_imapik- prefix visible; masks the rest.
381+
// Keeps the pk_imapik- prefix visible; masks the rest.
381382
// Caller must guard against null/empty; signature non-nullable so the
382383
// dictionary insertion in BuildInitConfigEcho doesn't trip CS8601.
383384
private static string RedactPublishableKey(string key)

examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cs

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@ public void SetUp()
4545
System.IO.Directory.Delete(sdkDir, recursive: true);
4646

4747
// Unity's bundled Mono runtime ships a curated root-CA set that
48-
// does not include the chain api.sandbox.immutable.com presents,
49-
// so HttpClient under Mono2x raises "SSL connection could not be
50-
// established" on every Flush. The cert is valid; only Mono's
51-
// verification fails. IL2CPP uses the OS CA store and is fine.
48+
// may not include the full chain the backend presents, so
49+
// HttpClient under Mono2x can raise "SSL connection could not be
50+
// established". The cert is valid; only Mono's verification fails.
51+
// IL2CPP uses the OS CA store and is fine.
5252
//
53-
// Bypass cert validation IN THE TEST PROCESS ONLY so the same
54-
// suite exercises both backends. Production SDK code is
55-
// untouched. Acceptable risk: this test process talks only to
56-
// sandbox; live-fire payloads carry no real user data.
53+
// Bypass cert validation IN THE TEST PROCESS ONLY. Production SDK
54+
// code is untouched.
5755
System.Net.ServicePointManager.ServerCertificateValidationCallback =
5856
(_, _, _, _) => true;
5957

@@ -89,8 +87,7 @@ private IEnumerator LoadAndInit(string? initialConsent = null, Action<VisualElem
8987
}
9088

9189
// Scene load + AudienceSample lookup + root capture, without clicking
92-
// btn-init. Useful for tests that need to inspect pre-init UI state
93-
// (e.g. prod-warning visibility for non-test keys).
90+
// btn-init. Useful for tests that need to inspect pre-init UI state.
9491
private IEnumerator LoadSceneOnly()
9592
{
9693
_key = Environment.GetEnvironmentVariable(SampleAppUi.EnvKey) ?? "";
@@ -331,7 +328,7 @@ public IEnumerator Reset_RegeneratesAnonymousIdAndAcceptsTrack()
331328
{
332329
// Reset clears identity + queue and rolls a new anonymousId. A
333330
// following Track must serialise with the new anonymousId and
334-
// round-trip to sandbox without errors.
331+
// round-trip to the backend without errors.
335332
yield return LoadAndInit();
336333

337334
_root!.Q<Button>(SampleAppUi.Buttons.TypedEvent("progression")).Click();
@@ -495,12 +492,11 @@ public IEnumerator TypedEvent_LinkClicked_FlushReportsOk()
495492
[UnityTest]
496493
public IEnumerator Init_WithBaseUrlOverride_FlushReportsOk()
497494
{
498-
// Explicit BaseUrl skips the publishable-key prefix routing in
499-
// Constants. Same target endpoint here, but the override branch is
500-
// a different config setup path that IL2CPP could strip independently.
495+
// Explicit BaseUrl exercises the override code path independently
496+
// of the default-production path — IL2CPP could strip either branch.
501497
yield return LoadAndInit(configure: root =>
502498
{
503-
root.Q<TextField>(SampleAppUi.Setup.BaseUrl).value = SampleAppUi.SandboxBaseUrl;
499+
root.Q<TextField>(SampleAppUi.Setup.BaseUrl).value = SampleAppUi.ExplicitBaseUrl;
504500
});
505501

506502
_root!.Q<Button>(SampleAppUi.Buttons.TypedEvent("progression")).Click();
@@ -738,30 +734,5 @@ public IEnumerator IdentityPanel_PopulatesUserIdAfterIdentify()
738734
"identity-user-id label should reflect ImmutableAudience.UserId after Identify");
739735
}
740736

741-
[UnityTest]
742-
public IEnumerator ProdWarning_HiddenForTestKey()
743-
{
744-
// The default env-var key is a test key (pk_imapik-test-…). The
745-
// prod-warning banner should stay hidden after RefreshStatusBar.
746-
yield return LoadSceneOnly();
747-
_root!.Q<TextField>(SampleAppUi.Setup.PublishableKey).value = _key;
748-
yield return null;
749-
750-
Assert.IsTrue(_root.Q<Label>(SampleAppUi.ProdWarning).ClassListContains(SampleAppUi.HiddenClass),
751-
"prod-warning should be hidden when the publishable-key is a test key");
752-
}
753-
754-
[UnityTest]
755-
public IEnumerator ProdWarning_VisibleForNonTestKey()
756-
{
757-
// A key without the "test-" segment looks production. Don't actually
758-
// Init so we don't live-fire to prod; just verify the warning UI.
759-
yield return LoadSceneOnly();
760-
_root!.Q<TextField>(SampleAppUi.Setup.PublishableKey).value = "pk_imapik-fakeprod-zzzz";
761-
yield return null;
762-
763-
Assert.IsFalse(_root.Q<Label>(SampleAppUi.ProdWarning).ClassListContains(SampleAppUi.HiddenClass),
764-
"prod-warning should be visible when the publishable-key looks like a prod key");
765-
}
766737
}
767738
}

examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppUi.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@ internal static class SampleAppUi
1919
// Scene asset name registered in EditorBuildSettings.
2020
internal const string SceneName = "SampleApp";
2121

22-
// The env var that carries the sandbox publishable key into the
23-
// built player at test time. Test runs inject it on the Unity CLI;
24-
// CI wires it from the AUDIENCE_TEST_PUBLISHABLE_KEY repo secret.
22+
// The env var that carries the publishable key into the built player at
23+
// test time. Test runs inject it on the Unity CLI; CI wires it from
24+
// the AUDIENCE_TEST_PUBLISHABLE_KEY repo secret.
2525
internal const string EnvKey = "AUDIENCE_TEST_PUBLISHABLE_KEY";
2626

2727
// Mirrors AudiencePaths.RootDirName — the SDK persists consent /
2828
// identity / queue under <persistentDataPath>/imtbl_audience. SetUp
2929
// wipes this between tests so on-disk state can't leak.
3030
internal const string SdkPersistedDirName = "imtbl_audience";
3131

32-
// Mirrors Constants.SandboxBaseUrl — used by the BaseUrl-override test.
33-
internal const string SandboxBaseUrl = "https://api.sandbox.immutable.com";
32+
// Used by the BaseUrl-override test to exercise the explicit-target code path.
33+
internal const string ExplicitBaseUrl = "https://api.immutable.com";
3434

3535
// ---- UXML element names ----
3636
// All names verified against examples/audience/Assets/SampleApp/Resources/AudienceSample.uxml.
@@ -41,6 +41,7 @@ internal static class Setup
4141
internal const string InitialConsent = "initial-consent";
4242
internal const string BaseUrl = "base-url";
4343
internal const string Debug = "debug";
44+
internal const string TestMode = "test-mode";
4445
internal const string FlushInterval = "flush-interval";
4546
internal const string FlushSize = "flush-size";
4647
}
@@ -126,10 +127,6 @@ internal static class Panels
126127
// it as `root.Q<ScrollView>(SampleAppUi.LogScrollView)`.
127128
internal const string LogScrollView = "log";
128129

129-
// The banner that warns when the publishable key looks like a prod
130-
// key. Toggled via the "hidden" CSS class.
131-
internal const string ProdWarning = "prod-warning";
132-
133130
// Mirrors AudienceSample.UI.cs PopulateTypedEventAccordions naming:
134131
// input.name = $"typed-{spec.Name.Replace('_', '-')}-{field.Key.ToLowerInvariant().Replace('_', '-')}";
135132
internal static string TypedEventField(string specName, string fieldKey) =>
@@ -141,9 +138,6 @@ internal static string TypedEventField(string specName, string fieldKey) =>
141138
// on consent buttons, etc.
142139
internal const string ActiveClass = "active";
143140

144-
// Toggled by RefreshStatusBar on the prod-warning banner.
145-
internal const string HiddenClass = "hidden";
146-
147141
// ---- Log labels ----
148142
// Mirrors AudienceSample.cs: every RunAndLog(label, ...) and
149143
// AppendLog(label, ...) call. Tests await these via WaitForLogEntry.

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>

0 commit comments

Comments
 (0)