Skip to content

ci(audience-sdk): add IL2CPP and Mono PlayMode test workflow (SDK-148)#715

Merged
ImmutableJeffrey merged 10 commits intomainfrom
chore/sdk-148-il2cpp
Apr 29, 2026
Merged

ci(audience-sdk): add IL2CPP and Mono PlayMode test workflow (SDK-148)#715
ImmutableJeffrey merged 10 commits intomainfrom
chore/sdk-148-il2cpp

Conversation

@ImmutableJeffrey
Copy link
Copy Markdown
Collaborator

@ImmutableJeffrey ImmutableJeffrey commented Apr 29, 2026

Summary

  • Adds .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 the AUDIENCE_TEST_PUBLISHABLE_KEY repo secret.
  • Adds 39 PlayMode tests under examples/audience/Assets/SampleApp/Tests/Runtime/SampleAppLiveFireTests.cs covering 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.
  • Adds EventQueueTests.PurgeAll covering ConcurrentQueue + DiskStore.DeleteAll under _drainLock.
  • Adds SampleAppUi SSOT class for every UXML name, log label, consent value, CSS class, and SDK path the suite touches.
  • Adds ScriptingBackendOverride Editor script that flips scriptingBackend and managedStrippingLevel per AUDIENCE_SCRIPTING_BACKEND env var (IL2CPP → High, Mono2x → Disabled).
  • Switches examples/audience Standalone scripting backend to IL2CPP with managed stripping High in ProjectSettings.asset.
  • Mirrors the SDK's link.xml inside examples/audience/Assets/ so Immutable.Audience.Unity survives stripping (UnityLinker does not auto-discover link.xml for packages embedded via file:.. outside the project tree).
  • Names the dynamically-built typed-event Send buttons and inputs (btn-typed-{spec}, typed-{spec}-{field}) so PlayMode tests can address them.
  • Includes Standalone platforms + test-framework references in com.immutable.audience.tests.asmdef.
  • Test SetUp bypasses ServicePointManager.ServerCertificateValidationCallback in the test process only — Unity 2021.3 Mono's bundled root-CA set does not trust api.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

ImmutableJeffrey and others added 6 commits April 29, 2026 12:33
…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>
@ImmutableJeffrey ImmutableJeffrey requested review from a team as code owners April 29, 2026 03:01
@ImmutableJeffrey ImmutableJeffrey force-pushed the chore/sdk-148-il2cpp branch 2 times, most recently from fe45659 to df2f3cc Compare April 29, 2026 03:25
Comment thread .github/workflows/test-audience-sample-app.yml
Comment thread .github/workflows/test-audience-sample-app.yml Outdated
@immutable immutable deleted a comment from cursor Bot Apr 29, 2026
@immutable immutable deleted a comment from cursor Bot Apr 29, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ 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.

Comment thread .github/workflows/test-audience-sample-app.yml
@ImmutableJeffrey ImmutableJeffrey force-pushed the chore/sdk-148-il2cpp branch 3 times, most recently from eefe2d1 to dd39265 Compare April 29, 2026 05:18
@ImmutableJeffrey ImmutableJeffrey changed the title test(audience-sdk): IL2CPP + Mono PlayMode test workflow [SDK-148] ci(audience-sdk): IL2CPP + Mono PlayMode test workflow (SDK-148) Apr 29, 2026
@ImmutableJeffrey ImmutableJeffrey changed the title ci(audience-sdk): IL2CPP + Mono PlayMode test workflow (SDK-148) ci(audience-sdk): add IL2CPP and Mono PlayMode test workflow (SDK-148) Apr 29, 2026
@ImmutableJeffrey ImmutableJeffrey force-pushed the chore/sdk-148-il2cpp branch 2 times, most recently from 3cea3ab to ef629e3 Compare April 29, 2026 05:56
ImmutableJeffrey and others added 3 commits April 29, 2026 16:12
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.
…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.
Copy link
Copy Markdown
Collaborator

@nattb8 nattb8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@ImmutableJeffrey ImmutableJeffrey merged commit 6e75a05 into main Apr 29, 2026
23 checks passed
@ImmutableJeffrey ImmutableJeffrey deleted the chore/sdk-148-il2cpp branch April 29, 2026 06:48
@github-actions github-actions Bot added the chore label Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

2 participants