ci(audience-sdk): add IL2CPP and Mono PlayMode test workflow (SDK-148)#715
Merged
ImmutableJeffrey merged 10 commits intomainfrom Apr 29, 2026
Merged
ci(audience-sdk): add IL2CPP and Mono PlayMode test workflow (SDK-148)#715ImmutableJeffrey merged 10 commits intomainfrom
ImmutableJeffrey merged 10 commits intomainfrom
Conversation
…fs in tests asmdef PlayMode tests need the Tests asmdef to compile for Standalone targets so the Unity test runner can build a player with the test framework wired in. Adds UnityEngine.TestRunner + UnityEditor.TestRunner references, overrideReferences=true, precompiledReferences=["nunit.framework.dll"], and defineConstraints=["UNITY_INCLUDE_TESTS"] — the standard Unity test asmdef pattern. Without those, including Standalone makes NUnit's [Test] unresolvable in Unity even though dotnet test still works (csproj path uses NuGet NUnit independently). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pins the PurgeAll path that no existing test reaches: enqueue + FlushSync to disk, enqueue another in memory, PurgeAll, assert disk is empty. Exercises _drainLock, ConcurrentQueue<T>.TryDequeue, and DiskStore.DeleteAll together — paths IL2CPP managed-code stripping can break. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stamps spec-derived names on the dynamically-built UI Toolkit elements
inside each typed-event accordion: btn-typed-{spec} on the Send button
and typed-{spec}-{field} on each input. Lets PlayMode tests address each
event by name, mirroring the static UXML controls (btn-init/btn-flush).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Tests/Runtime tree under examples/audience with an asmdef
configured for NUnit + Standalone platforms, plus three test-side
support classes:
SampleAppUi — single source of truth for the literal strings the live-
fire suite touches (UXML element names, log labels, consent values, CSS
classes, SDK paths). Mirrors AudienceSample.UI.cs / AudienceSample.cs /
ConsentLevel.cs / AudiencePaths.cs with explicit "keep in sync"
docstrings. Drift fails loud (Q<> returns null → NRE on .Click() or
WaitForLogEntry timeout).
SampleAppTestHelpers — log-pane introspection (WaitForLogEntry,
HasLogEntry, CountLogEntriesAtLevel, DescribeLogEntries with Body
field for Err diagnosis), LogLevels constants mirroring
AudienceSample.UI.cs LogLevel enum, and a Button.Click() extension that
reflects on Clickable.clicked because UI Toolkit's Button.clicked event
has custom add/remove accessors so external code can't .Invoke() it.
ScriptingBackendOverride (Editor-only) — hooks IPreprocessBuildWithReport
to flip Standalone scripting backend per AUDIENCE_SCRIPTING_BACKEND env
var ("IL2CPP" or "Mono2x"). Also flips managedStrippingLevel to the
realistic per-backend default (High for IL2CPP, Disabled for Mono).
Lets the same suite exercise both backends from one project tree.
Registers SampleApp.unity in EditorBuildSettings so SceneManager.LoadScene
resolves in a built player.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… under both backends
Drives the sample app's UI buttons via reflection-based Click() and live-
fires every public ImmutableAudience.* API path against sandbox using the
AUDIENCE_TEST_PUBLISHABLE_KEY env var. SetUp resets in-memory state via
reflection on ImmutableAudience.ResetState, wipes the on-disk
<persistentDataPath>/imtbl_audience tree, and bypasses ServicePointManager
cert validation (test-only, sandbox-only) so the same suite passes under
both IL2CPP and Mono2x — Unity's bundled Mono runtime ships a stale
root-CA set that doesn't trust api.sandbox.immutable.com's chain.
Every UXML element name, log label, consent value, CSS class, and SDK
path is referenced through SampleAppUi rather than inline literals so
drift surfaces as a compile error or loud test failure rather than a
silent skip.
39 tests across 8 categories:
Core API (1)
InitTrackFlush_AgainstSandbox
Catalogue events (11) — typed-class path + string-overload fallback
Progression / Resource / Purchase / MilestoneReached
SignUp / SignIn / EmailAcquired / WishlistAdd / WishlistRemove
GamePageViewed / LinkClicked
Identity (3)
Identify / IdentifyTraits / Alias
Consent transitions (3)
None purge / FullAfterAnonymous / AnonymousFromFull (downgrade strip)
Custom Track (1)
Dictionary props (int/str/bool/nested JSON)
Lifecycle / control-plane (4)
Shutdown / Reset / DeleteData / ReInit-after-Shutdown
Init-config code paths (4)
BaseUrl override / sub-1s flush warn+clamp / custom flushSize /
debug=false
Queue + persistence (3)
MultiEvent batched flush / EmptyFlush no-op / PersistedConsent
overrides dropdown on re-Init
Sample-app UI plumbing (9)
StatusBar (consent / anon-id / queue increment)
ClearLog / TabNav / ConsentPill / IdentityPanel
ProdWarning hidden for test-key / visible for non-test-key
LoadAndInit + FlushAndAssertNoErrors + SetConsentVia helpers keep each
[UnityTest] focused. All 39 pass under Unity 2021.3.45f2 +
StandaloneOSX, both IL2CPP (~29s) and Mono2x (~25s); CI matrix covers
StandaloneWindows64 + macOS × IL2CPP + Mono2x.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ty assembly Two changes that together make the sample app build under IL2CPP: ProjectSettings.asset: scriptingBackend.Standalone=1 (IL2CPP) managedStrippingLevel.Standalone=3 (High) stripEngineCode=1 and apiCompatibilityLevel=6 (.NET Standard 2.1 in Unity 2021.2+) were already in place. This mirrors the most aggressive linker config studios ship under, surfacing link.xml gaps and method-level strips during PlayMode tests. The ScriptingBackendOverride Editor script flips both fields to per-backend defaults when AUDIENCE_SCRIPTING_BACKEND is set. Assets/link.xml: preserve Immutable.Audience.Runtime + Immutable.Audience.Unity, plus System.Net.Http and System.IO.Compression members the SDK reaches. The SDK ships its own src/Packages/Audience/link.xml but UnityLinker doesn't auto-discover it when the package is embedded via "file:.." pointing outside the project tree (which is how the sample loads the SDK during development). Without this project-side mirror, Immutable.Audience.Unity gets stripped wholesale and the [RuntimeInitializeOnLoadMethod] in AudienceUnityHooks never fires — ImmutableAudience.Init then throws "PersistentDataPath is required". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fe45659 to
df2f3cc
Compare
df2f3cc to
ac36094
Compare
ac36094 to
5d17c81
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5d17c81. Configure here.
eefe2d1 to
dd39265
Compare
dd39265 to
eafe09e
Compare
3cea3ab to
ef629e3
Compare
Four-cell matrix builds the audience sample app and runs PlayMode tests
inside the built player across both backends:
- StandaloneWindows64 / IL2CPP / windows-latest / Unity 2021.3.45f2
- StandaloneWindows64 / Mono2x / windows-latest / Unity 2021.3.45f2
- StandaloneOSX / IL2CPP / macos-latest / Unity 2021.3.45f2
- StandaloneOSX / Mono2x / macos-latest / Unity 2021.3.45f2
Backend selection per cell goes through AUDIENCE_SCRIPTING_BACKEND env
var consumed by the ScriptingBackendOverride Editor script.
Notes:
- Both targets use Windows / macOS hosts because game-ci can cross-
build Windows IL2CPP players on Linux but the runner can't launch
them to drive PlayMode tests; the host OS must match the player
target.
- Unity 2021.3.45f2 (not 2021.3.26f1 as originally planned) avoids a
zlib-vs-macOS-15-SDK toolchain incompatibility that breaks the
IL2CPP build under newer Xcode CLT (Unity zutil.h:147 macros fdopen
to NULL, collides with stdio.h fdopen function declaration).
- Tests live-fire to sandbox via AUDIENCE_TEST_PUBLISHABLE_KEY repo
secret. Skipped on fork PRs to avoid secret leakage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes a real SDK bug surfaced by the SDK-148 live-fire test suite: every 4xx — including 429 (Too Many Requests) — was treated as a permanent "server rejected your data, retry won't help" failure. Per RFC 6585, 429 is the canonical retryable 4xx and must honor the Retry-After header. Changes: - HttpTransport: split 429 out of the 4xx delete-and-error branch. Keep the batch on disk, honor Retry-After (delta-seconds or HTTP-date) if present, otherwise apply the existing exponential schedule (5/10/20/40/60s). Do NOT fire onError — the next flush tick retries automatically once the backoff window expires. Persistent rate-limiting manifests as a growing on-disk queue, the correct studio-actionable signal. - ImmutableAudience consent sync: wrap the PUT in a 4-attempt retry loop with 1s/2s/4s backoff between attempts (or Retry-After if the server supplies one). ConsentSyncFailed only fires after the budget exhausts. - HttpRetry helper class for shared Retry-After parsing across batch and consent-sync paths. Tests: - HttpTransport: 429 keeps batch + sets backoff + no onError; Retry-After delta-seconds drives NextAttemptAt; HTTP-date variant engages backoff window; past Retry-After falls through to exponential; 429-then-200 delivers the batch and clears backoff. - ConsentSync: 429-then-2xx is invisible to onError; 429×4 surfaces ConsentSyncFailed after the full retry budget.
Two warnings flagged by Unity's C# compiler in AudienceSample.cs. OnSendCustomEvent: the dict was Dictionary<string,object> but `props` (JsonReader.DeserializeObject result) is nullable. Now we only insert the "properties" key when non-null. Side-benefit: cleaner JSON output when no props were entered (key omitted vs serialised as null). RedactPublishableKey: signature changed from `string? Redact(string?)` to `string Redact(string)`. The only caller already guards with !string.IsNullOrEmpty before invoking, so the prior nullable parameter + early-return-null path was dead. Non-nullable signature lets the dict insertion in BuildInitConfigEcho compile cleanly.
ef629e3 to
0d45cf9
Compare
…tamp, context
Replaces the SDK-149 caveat about indirect verification of message envelope
shape. Previously the live-fire suite only confirmed shape via backend
rejection — fragile (backend schema drift could mask SDK bugs) and dependent
on an external service for what is fundamentally an offline assertion.
New direct assertions in MessageBuilderTests:
- messageId parses as Guid (Guid.TryParse).
- messageId is unique across 1000 successive Track() calls (catches a
regression where Guid.NewGuid is replaced by a deterministic source).
- eventTimestamp parses via DateTime.TryParseExact("o", RoundtripKind),
is UTC, and lies within ~2 s of construction time.
- context.library and context.libraryVersion are non-empty strings.
Covered for all three top-level message types (Track, Identify, Alias)
via a shared EveryMessageType iterator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
.github/workflows/test-audience-sample-app.yml— a 4-cell matrix (StandaloneWindows64/StandaloneOSX×IL2CPP/Mono2x, all on Unity 2021.3.45f2) that builds the audience sample app and runs PlayMode tests inside the built player on self-hosted runners. Live-fires to sandbox via theAUDIENCE_TEST_PUBLISHABLE_KEYrepo secret.examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cscovering core init / track / flush, all 11 catalogue events, identify / identify-traits / alias, three consent transitions (None purge, Anonymous→Full→Identify, Full→Anonymous downgrade-strip), custom Track with Dictionary props, lifecycle (Shutdown / Reset / DeleteData / re-Init), init-config paths (BaseUrl override, sub-1s flush warn+clamp, custom flushSize, debug=false), queue (multi-event batch, empty flush, persisted-consent override), and 9 sample-app UI plumbing assertions.EventQueueTests.PurgeAllcovering ConcurrentQueue + DiskStore.DeleteAll under_drainLock.SampleAppUiSSOT class for every UXML name, log label, consent value, CSS class, and SDK path the suite touches.ScriptingBackendOverrideEditor script that flipsscriptingBackendandmanagedStrippingLevelperAUDIENCE_SCRIPTING_BACKENDenv var (IL2CPP → High, Mono2x → Disabled).examples/audienceStandalone scripting backend to IL2CPP with managed stripping High inProjectSettings.asset.link.xmlinsideexamples/audience/Assets/soImmutable.Audience.Unitysurvives stripping (UnityLinker does not auto-discoverlink.xmlfor packages embedded viafile:..outside the project tree).btn-typed-{spec},typed-{spec}-{field}) so PlayMode tests can address them.com.immutable.audience.tests.asmdef.ServicePointManager.ServerCertificateValidationCallbackin the test process only — Unity 2021.3 Mono's bundled root-CA set does not trustapi.sandbox.immutable.com's chain. Production SDK is untouched.Verified locally on Unity 2021.3.45f2-arm64 / StandaloneOSX: IL2CPP 39/39 (~24s), Mono2x 39/39 (~24s).
Linear: SDK-148