## What this PR adds end-to-end test suite for the **Mobile RUM SDK** under `tests/mobile-testing/`, plus a **self-contained CI workflow** that runs it on real emulators/simulators against an OpenObserve **built from this PR's source**. Test / fixture / CI only — no product-code/existing test changes ## How each test works (3 layers) Every scenario is a real end-to-end journey, so a failure points at the actual broken layer: 1. **Drive** — a Maestro flow taps through the app like a user (`maestro/<track>/*.yaml`). 2. **API** — query the `_rumdata` stream and assert the telemetry is correct (`utils/ooClient.js`, poll-and-retry for async ingestion). 3. **UI** — open the RUM dashboard and assert the sessions ## Coverage (42 tests across 4 platforms) The **native platforms (android-native, ios-native) are now at parity with React Native** — each new native test (masking, network, bg/fg, user-identity, no-phone-home) was built + validated on a real emulator/simulator before landing. | Capability | Layer | RN-Android | Android-native | RN-iOS | iOS-native | |---|---|:-:|:-:|:-:|:-:| | Core telemetry (views/actions/handled-error/crash → `_rumdata`) | API | ✅ | ✅ | ✅ | ✅ | | Crash session viewable in dashboard | UI | ✅ | ✅ | ✅ | ✅ | | Attributes: env / service / version tagging | API | ✅ | ✅ | ✅ | ✅ | | User identity (`setUser`) | API | ✅ | ✅ | ✅ | ✅ | | Network: fetch → resource + 4xx/5xx | API | ✅ | ✅ | ✅ | ✅ | | Session-replay PII masking | UI | ✅ | ✅ | ✅ | ✅ | | Background / foreground view transitions | API | ✅ | ✅ | — | ✅ | | No-phone-home (no egress to Datadog hosts) | security | ✅ | ✅ | — | ✅ | - Shared per-capability factories keep the specs DRY (`utils/rumChecks.js`, `utils/coreRumSpec.js`). - Negatives are covered: 4xx capture, PII-must-not-leak (masking), no phone-home, handled-error ≠ crash. - `o2-enterprise#2289` dashboard bugs are kept as inline skips that flip green once fixed. - Full capability × platform detail + on-device status: `docs/COVERAGE-MATRIX.md`. ## Fixture apps + build-time config - Four vendored apps under `apps/`: `o2-rum-tester` (React Native — drives both RN platforms), `o2-native-android` (Kotlin), `o2-native-ios` (Swift / xcodegen). - Each reads its RUM target (endpoint / org / token) and SDK version from a **build-time-overridable** config. The committed default is the current pin, so local dev is unchanged; CI regenerates it to point at the runner's OpenObserve and can pin the latest SDK on release. ## CI — `.github/workflows/mobile-sdk-e2e.yml` (self-contained, no secrets) - **Build once per OS from source.** `build_oo_linux` / `build_oo_macos` compile OpenObserve (binary + embedded web UI) from this PR's code and cache it. Device jobs download and run it **natively**, and the app posts real RUM to it (Android reaches the host at `10.0.2.2`, the iOS simulator via `localhost`). No Docker, no shared cluster, no secrets. - **Device jobs run in parallel.** `android` (Linux + KVM emulator → rn-android + android-native) and `ios` (macOS simulator, pinned Xcode 16.4 → rn-ios + ios-native). - **`merge_reports`** combines both platforms into one browsable HTML report — even when a device job fails. - **`decide` picks which platform runs, to keep scarce macOS runners in check:** - PR touching Android or shared mobile-RUM code → **Android auto-runs**; iOS is gated behind the `mobile-e2e` label. - `mobile-e2e` label → both platforms (full pre-merge gate). - `repository_dispatch(mobile-sdk-release)` from an SDK repo → runs that SDK's platform (companion PRs wire this up: openobserve-react-native-rum#3, openobserve-sdk-android#1, openobserve-sdk-ios#1). - `workflow_dispatch` → manual, choose platforms. *Separate workflow :* it needs macOS **and** Android emulators, builds 4 phone apps, and is dispatch-driven — matching the repo's existing per-suite split ## Status Green end-to-end on GitHub runners (Android + iOS device jobs + merge) : https://github.com/openobserve/openobserve/actions/runs/31733717758/job/94565520891 --------- Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
6.6 KiB
Mobile RUM SDK — CI notes
How .github/workflows/mobile-sdk-e2e.yml runs. Model B: self-contained, no secrets — each
device job stands up its own OpenObserve on the runner and points the app at it, exactly like
playwright.yml does for the web tests.
Why self-contained (and why it's possible)
- RUM ingestion is OSS — a plain OpenObserve accepts the data, so no special/enterprise cluster.
- The other test workflows already run a throwaway OpenObserve on the runner with the well-known
defaults (
root@example.com/Complexpass#123/ orgdefault/localhost:5080) as plain values — no secrets. This workflow does the same. - Result: every run is isolated (its own instance), so the Android and iOS jobs run in parallel with no collision — this removes the shared-service problem entirely.
How the app gets pointed at the local instance (build-time config injection)
Each app reads its RUM target from a build-time-overridable config. The committed values are
placeholders (REPLACE_ME / openobserve.example.com); CI regenerates them to point at the
local instance, and mints its own RUM token per run:
| App | Mechanism | Regenerated by |
|---|---|---|
| RN (o2-rum-tester) | inline @gen:* consts in App.tsx (NOT a JSON import — that breaks the Hermes release upload) |
scripts/gen-config.js (postinstall, seds App.tsx from O2_RUM_*) |
| Android-native | BuildConfig.OO_* fields |
build.gradle.kts reads O2_RUM_* via System.getenv |
| iOS-native | Sources/GeneratedConfig.swift |
gen-config.sh (reads O2_RUM_*) |
Env the CI job sets before building: O2_RUM_HOST, O2_RUM_ORG, O2_RUM_TOKEN, O2_RUM_ENV.
With none set, every generator keeps the committed placeholders — so set these (or .env) for a
real local target.
Networking
- Android emulator can't see the host's
localhost; it reaches the host at10.0.2.2. So the app posts tohttp://10.0.2.2:5080while the suite's API/UI checks (on the host) uselocalhost:5080. - iOS simulator shares the host network → app and suite both use
localhost:5080. - Cleartext HTTP to the local instance: Android manifests set
usesCleartextTraffic="true"; the RN iOS build adds an ATSNSAllowsLocalNetworkingkey at build time (committed source stays HTTPS).
Testing the latest SDK (version injection)
The fixture apps pin the SDK version (RN @openobserve/mobile-react-native 0.1.1; the Android
and iOS native SDKs 0.1.0 — separate packages, independently versioned) so ordinary runs are
reproducible. But on a release we want to test the new version, not the frozen pin. So the SDK
version is a build input, defaulting to the pin:
| App | Override mechanism | Env |
|---|---|---|
| RN | workflow npm install @openobserve/mobile-react-native@<ver> before build |
O2_RN_SDK_VERSION |
| Android-native | build.gradle.kts: System.getenv("O2_ANDROID_SDK_VERSION") ?: "<pin>" |
O2_ANDROID_SDK_VERSION |
| iOS-native | workflow sed of project.yml exactVersion before xcodegen |
O2_IOS_SDK_VERSION |
The mobile-sdk-release dispatch carries {sdk, version}; the "Resolve SDK version override" step
maps it to the right env so a release builds + tests against the latest SDK. A manual run can do the
same via the sdk + sdk_version inputs. Unset → the committed pin (reproducible).
Triggers
A decide job turns each event into run_android / run_ios, so device jobs run only where relevant
(macOS runners are scarce):
| Trigger | Fires when | Runs |
|---|---|---|
repository_dispatch (mobile-sdk-release) |
an SDK repo publishes a release (cross-repo) | the SDK's platform (sdk = ios→iOS, android→Android, react-native→both) |
pull_request (paths) |
a PR touches Android/shared mobile-RUM code | preflight + Android device jobs (iOS is label-gated) |
pull_request + mobile-e2e label |
full pre-merge gate | preflight + both platforms |
workflow_dispatch |
manual; run_full_suite=true + platforms |
preflight + the chosen platforms |
The cross-repo trigger
The SDK source lives in other repos, so an SDK change never appears as a PR here. Each SDK repo
sends a dispatch on release. The dispatch workflows are already opened as PRs on the three SDK repos
(openobserve-react-native-rum, openobserve-sdk-android, openobserve-sdk-ios); each needs a repo
secret OO_E2E_DISPATCH_TOKEN = a token that can dispatch on openobserve/openobserve (classic PAT
with repo, or fine-grained with Contents:read + Actions:write). The call they make:
- name: Dispatch mobile-sdk-release
env:
GH_TOKEN: ${{ secrets.OO_E2E_DISPATCH_TOKEN }}
run: |
echo "{\"event_type\":\"mobile-sdk-release\",\"client_payload\":{\"sdk\":\"ios\",\"version\":\"$VER\"}}" \
| gh api repos/openobserve/openobserve/dispatches --input -
sdk is react-native / android / ios per repo; the decide job maps it to the platform(s) to run.
Runners & the OpenObserve build
OpenObserve is built from this repo's source per-OS (not Docker, not a prebuilt binary), cached, then downloaded and run natively by the device jobs so the app posts real RUM to it:
build_oo_linux(ubuntu-latest) +build_oo_macos(macos-15) compile the binary + web UI.- Android →
ubuntu-latest+ KVM emulator, runs the Linux binary; app reaches it at10.0.2.2. - iOS →
macos-15+ simulator (pinned Xcode 16.4), runs the macOS binary; app useslocalhost. merge_reportscombines both platforms into one HTML report (survives a failed device job).
release-ci sets debug-assertions=true, which puts rust-embed in disk-read mode — so each build job
also uploads the built web UI (oo-<os>-frontend) and the device job extracts it to /tmp/dist so
/web serves and the dashboard-UI tests run.
Status
Green end-to-end on GitHub runners (Android + iOS device jobs + merge). Known SDK build fixes and the cleartext-HTTP handling are recorded below / in the git history.
Validated locally
- Suite parses (
playwright --list): 42 tests, 4 platforms (+ sdk-drift project). gen-config.js/gen-config.shkeep the committed placeholders when no env is set, and substituteO2_RUM_*when set.- Android/iOS native config wiring is in place (
BuildConfig.OO_*,GeneratedConfig).
Optional later
- Host next to the enterprise/alpha pipeline (mobile RUM is enterprise-flavored) — but note macOS is free here (public) and billed there (private).
- A per-run RUM
run-idattribute is no longer needed for isolation (each run has its own instance); only relevant if you ever move back to a shared cluster.