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 @@ -3,9 +3,6 @@

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

<ui:Label name="prod-warning" class="prod-warning hidden"
text="⚠ USING PROD ENDPOINT — ACTIONS HERE AFFECT PROD DATA ⚠" />

<ui:VisualElement name="sticky-outer" class="sticky-outer">
<ui:VisualElement class="sticky-top-main">

Expand Down Expand Up @@ -78,7 +75,7 @@
<ui:VisualElement class="field placeholder-host">
<ui:Label class="field-label" text="PUBLISHABLE KEY" />
<ui:TextField name="publishable-key" />
<ui:Label class="field-placeholder" text="pk_imapik-test-yourkey" />
<ui:Label class="field-placeholder" text="pk_imapik-yourkey" />
</ui:VisualElement>

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

<ui:VisualElement class="field">
<ui:Toggle name="test-mode" label="TEST MODE" value="true" />
<ui:Label class="helper-text below-field"
text="Tags all outbound events with test: true so the backend can filter them from production analytics." />
</ui:VisualElement>

<ui:VisualElement name="mobile-attribution-field" class="field">
<ui:Toggle name="enable-mobile-attribution" label="MOBILE ATTRIBUTION" />
<ui:Label class="helper-text below-field"
Expand All @@ -120,7 +123,7 @@
<ui:TextField name="base-url" />
<ui:Label class="field-placeholder" text="(derived from key)" />
<ui:Label class="helper-text below-field"
text="Bypass the SDK's prefix-based routing and target a specific backend. Leave empty to derive from the publishable key." />
text="Target a specific backend. Leave empty to use the production endpoint." />
</ui:VisualElement>

<ui:VisualElement class="advanced-grid">
Expand Down
31 changes: 14 additions & 17 deletions examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static readonly (string TabId, string PanelId)[] Tabs =
// ---- UXML element fields (Setup tab) ----

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

private readonly List<Button> _tabButtons = new List<Button>();
private Label _prodWarning, _sdkVersionLabel, _titleLabel;
private Label _sdkVersionLabel, _titleLabel;
private Label _statusEndpoint, _statusConsent, _statusAnon, _statusUser, _statusSession, _statusQueue;

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

private void BindElements()
{
_prodWarning = Require<Label>("prod-warning");

_sdkVersionLabel = Require<Label>("sdk-version");
_titleLabel = _root.Q<Label>(className: "title");

Expand All @@ -181,17 +182,19 @@ private void BindElements()
_baseUrl = Require<TextField>("base-url");
_initialConsent = Require<DropdownField>("initial-consent");
_debug = Require<Toggle>("debug");
_testMode = Require<Toggle>("test-mode");
_enableMobileAttribution = Require<Toggle>("enable-mobile-attribution");
// Inject a tick Label — Unity 2021.3 runtime panels render the
// checked state as a plain coloured square otherwise. USS hides
// the tick when unchecked.
var debugCheckmark = _debug.Q<VisualElement>(className: "unity-toggle__checkmark");
if (debugCheckmark != null)
foreach (var toggle in new[] { _debug, _testMode, _enableMobileAttribution })
{
var checkmark = toggle.Q<VisualElement>(className: "unity-toggle__checkmark");
if (checkmark == null) continue;
var tick = new Label("✓");
tick.AddToClassList("debug-tick");
tick.pickingMode = PickingMode.Ignore;
debugCheckmark.Add(tick);
checkmark.Add(tick);
}
_flushInterval = Require<TextField>("flush-interval");
_flushSize = Require<TextField>("flush-size");
Expand Down Expand Up @@ -610,15 +613,9 @@ private void RefreshStatusBar()
var key = (_publishableKey.value ?? "").Trim();
var overrideUrl = (_baseUrl?.value ?? "").Trim();
bool keyEmpty = string.IsNullOrEmpty(key);
bool isTest = !keyEmpty && IsTestKey(key);
bool hasOverride = !string.IsNullOrEmpty(overrideUrl);
// BaseUrl override skips prefix-based routing, so the prod-warning
// rule no longer applies (studio is in explicit-target mode).
string? derivedFromKey = keyEmpty ? null : (isTest ? Constants.SandboxBaseUrl : Constants.ProductionBaseUrl);
string? endpoint = hasOverride ? overrideUrl : derivedFromKey;
bool warnState = hasOverride || (!keyEmpty && !isTest);
SetStatusCell(_statusEndpoint, endpoint, warnState ? "state-warn" : "state-ok");
_prodWarning.EnableInClassList("hidden", hasOverride || keyEmpty || isTest);
string? endpoint = hasOverride ? overrideUrl : (keyEmpty ? null : Constants.ProductionBaseUrl);
SetStatusCell(_statusEndpoint, endpoint, hasOverride ? "state-warn" : "state-ok");

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

public InitForm(string publishableKey, string baseUrl, ConsentLevel consent, bool debug, bool enableMobileAttribution, int? flushIntervalMs, int? flushSize)
public InitForm(string publishableKey, string baseUrl, ConsentLevel consent, bool debug, bool testMode, bool enableMobileAttribution, int? flushIntervalMs, int? flushSize)
{
PublishableKey = publishableKey;
BaseUrl = baseUrl;
Consent = consent;
Debug = debug;
TestMode = testMode;
EnableMobileAttribution = enableMobileAttribution;
FlushIntervalMs = flushIntervalMs;
FlushSize = flushSize;
Expand All @@ -702,6 +701,7 @@ internal InitForm CaptureInitForm()
baseUrl: (_baseUrl.value ?? "").Trim(),
consent: ConsentOrder[consentIdx],
debug: _debug.value,
testMode: _testMode.value,
enableMobileAttribution: _enableMobileAttribution.value,
flushIntervalMs: flushIntervalMs,
flushSize: flushSize);
Expand Down Expand Up @@ -780,9 +780,6 @@ private bool IsAliasReady()
&& (fromId != toId || (_aliasFromType.value ?? "") != (_aliasToType.value ?? ""));
}

private static bool IsTestKey(string? key) =>
!string.IsNullOrEmpty(key) && key!.StartsWith(Constants.TestKeyPrefix, StringComparison.Ordinal);

private static void FlashCopied(VisualElement ve)
{
ve.AddToClassList("copied");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,8 @@ private void OnSdkStateChanged()
// ---- Config builders ----

// Maps the captured Setup form to AudienceConfig. BaseUrl null → SDK
// derives the endpoint from the publishable key prefix (test → sandbox,
// else production). The flushInterval clamp emits a warn row when
// the user requests <1s.
// targets the production endpoint. The flushInterval clamp emits a warn
// row when the user requests <1s.
private AudienceConfig BuildAudienceConfig(InitForm form, Action<AudienceError> onError)
{
var config = new AudienceConfig
Expand All @@ -342,6 +341,7 @@ private AudienceConfig BuildAudienceConfig(InitForm form, Action<AudienceError>
BaseUrl = string.IsNullOrEmpty(form.BaseUrl) ? null : form.BaseUrl,
Consent = form.Consent,
Debug = form.Debug,
TestMode = form.TestMode,
EnableMobileAttribution = form.EnableMobileAttribution,
OnError = onError,
};
Expand All @@ -364,6 +364,7 @@ private static Dictionary<string, object> BuildConfigEcho(AudienceConfig config)
{
["consent"] = config.Consent.ToString(),
["debug"] = config.Debug,
["testMode"] = config.TestMode,
["enableMobileAttribution"] = config.EnableMobileAttribution,
["flushIntervalSeconds"] = config.FlushIntervalSeconds,
["flushSize"] = config.FlushSize,
Expand All @@ -377,7 +378,7 @@ private static Dictionary<string, object> BuildConfigEcho(AudienceConfig config)
return echo;
}

// Keeps the pk_imapik-test- / pk_imapik- prefix visible; masks the rest.
// Keeps the pk_imapik- prefix visible; masks the rest.
// Caller must guard against null/empty; signature non-nullable so the
// dictionary insertion in BuildInitConfigEcho doesn't trip CS8601.
private static string RedactPublishableKey(string key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,13 @@ public void SetUp()
System.IO.Directory.Delete(sdkDir, recursive: true);

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

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

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

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

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

[UnityTest]
public IEnumerator ProdWarning_HiddenForTestKey()
{
// The default env-var key is a test key (pk_imapik-test-…). The
// prod-warning banner should stay hidden after RefreshStatusBar.
yield return LoadSceneOnly();
_root!.Q<TextField>(SampleAppUi.Setup.PublishableKey).value = _key;
yield return null;

Assert.IsTrue(_root.Q<Label>(SampleAppUi.ProdWarning).ClassListContains(SampleAppUi.HiddenClass),
"prod-warning should be hidden when the publishable-key is a test key");
}

[UnityTest]
public IEnumerator ProdWarning_VisibleForNonTestKey()
{
// A key without the "test-" segment looks production. Don't actually
// Init so we don't live-fire to prod; just verify the warning UI.
yield return LoadSceneOnly();
_root!.Q<TextField>(SampleAppUi.Setup.PublishableKey).value = "pk_imapik-fakeprod-zzzz";
yield return null;

Assert.IsFalse(_root.Q<Label>(SampleAppUi.ProdWarning).ClassListContains(SampleAppUi.HiddenClass),
"prod-warning should be visible when the publishable-key looks like a prod key");
}
}
}
18 changes: 6 additions & 12 deletions examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ internal static class SampleAppUi
// Scene asset name registered in EditorBuildSettings.
internal const string SceneName = "SampleApp";

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

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

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

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

// The banner that warns when the publishable key looks like a prod
// key. Toggled via the "hidden" CSS class.
internal const string ProdWarning = "prod-warning";

// Mirrors AudienceSample.UI.cs PopulateTypedEventAccordions naming:
// input.name = $"typed-{spec.Name.Replace('_', '-')}-{field.Key.ToLowerInvariant().Replace('_', '-')}";
internal static string TypedEventField(string specName, string fieldKey) =>
Expand All @@ -141,9 +138,6 @@ internal static string TypedEventField(string specName, string fieldKey) =>
// on consent buttons, etc.
internal const string ActiveClass = "active";

// Toggled by RefreshStatusBar on the prod-warning banner.
internal const string HiddenClass = "hidden";

// ---- Log labels ----
// Mirrors AudienceSample.cs: every RunAndLog(label, ...) and
// AppendLog(label, ...) call. Tests await these via WaitForLogEntry.
Expand Down
13 changes: 10 additions & 3 deletions src/Packages/Audience/Runtime/AudienceConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,19 @@ public class AudienceConfig
/// Override the default API base URL.
/// </summary>
/// <remarks>
/// When null, publishable keys starting with <c>pk_imapik-test-</c>
/// resolve to Sandbox. All other keys resolve to Production. Set
/// explicitly to target a different backend.
/// When null, all events are sent to the production backend. Set
/// explicitly to target a different backend (e.g. an internal dev
/// environment).
/// </remarks>
public string? BaseUrl { get; set; }

/// <summary>
/// Enables test mode. When <c>true</c>, all events are tagged with
/// <c>test: true</c> so the backend can filter them from production
/// analytics. Default <c>false</c>.
/// </summary>
public bool TestMode { get; set; } = false;

/// <summary>
/// Initial consent level.
/// </summary>
Expand Down
Loading
Loading