diff --git a/.github/workflows/mobile-sdk-e2e.yml b/.github/workflows/mobile-sdk-e2e.yml index a17a9139f3..628e074873 100644 --- a/.github/workflows/mobile-sdk-e2e.yml +++ b/.github/workflows/mobile-sdk-e2e.yml @@ -354,7 +354,8 @@ jobs: - name: Install Protoc run: bash ./.github/protoc.sh - uses: actions/setup-node@v4 - with: { node-version: "20" } + # web/package-lock.json needs npm 11, so npm ci fails on Node 20's npm 10. + with: { node-version: "24" } - name: Build binary + web UI (embedded) env: NODE_OPTIONS: "--max-old-space-size=8192" @@ -404,7 +405,8 @@ jobs: - name: Install Protoc (macOS) run: brew install protobuf || true - uses: actions/setup-node@v4 - with: { node-version: "20" } + # web/package-lock.json needs npm 11, so npm ci fails on Node 20's npm 10. + with: { node-version: "24" } - name: Build binary + web UI (embedded) env: NODE_OPTIONS: "--max-old-space-size=8192" @@ -449,6 +451,15 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 steps: + # Some ubuntu-24.04 images leave too little disk for the emulator system image ("No space left on device"). + - name: Remove unused tools + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h / + - uses: actions/checkout@v4 # Test the LATEST SDK on a release event: the mobile-sdk-release dispatch carries {sdk, version}; diff --git a/tests/mobile-testing/README.md b/tests/mobile-testing/README.md index 95f7fdf1cf..20b52657a6 100644 --- a/tests/mobile-testing/README.md +++ b/tests/mobile-testing/README.md @@ -76,7 +76,7 @@ npx playwright test --grep @masking npx playwright test --grep @known-bug # the skipped #2289 markers ``` -### Coverage (all four mobile tracks) — 42 tests +### Coverage (all four mobile tracks) — 46 tests Each platform has a **core RUM** bundle (`utils/coreRumSpec.js`: views + actions + handled error + native crash in `_rumdata`, then the crashed session rendering in the dashboard) **plus** per-capability specs built from shared factories in **`utils/rumChecks.js`** — so the platforms stay @@ -87,14 +87,15 @@ at parity without copy-paste: - `networkSuite` — a successful fetch is a resource (**positive**) + a 4xx status is captured (**negative**) - `maskingSuite` — no PII leaks into the session replay (MASK_ALL) - `bgFgSuite` — a pre-background view and a post-foreground view share one session +- `logsSuite` — one tap's info/warn/error log burst lands in `_rumlog` as exactly three rows - `noPhoneHomeAndroidSuite` — the installed APK contains zero Datadog hosts (**security/negative**) | Project | App | Service | Specs | |---|---|---|---| -| `rn-android` | `apps/o2-rum-tester` (Android) | `o2-rum-tester` | crash, network(+404), handled-error, views, masking, attributes, user-identity, background-foreground, no-phone-home | -| `android-native` | `apps/o2-native-android` | `o2-native-android` | core bundle, attributes, user-identity, no-phone-home | -| `rn-ios` | `apps/o2-rum-tester` (iOS) | `o2-rum-tester` | core bundle, network(+404), attributes, user-identity, masking | -| `ios-native` | `apps/o2-native-ios` | `o2-native-ios` | core bundle, attributes | +| `rn-android` | `apps/o2-rum-tester` (Android) | `o2-rum-tester` | crash, network(+404), handled-error, views, masking, attributes, user-identity, background-foreground, logs, no-phone-home | +| `android-native` | `apps/o2-native-android` | `o2-native-android` | core bundle, attributes, user-identity, logs, no-phone-home | +| `rn-ios` | `apps/o2-rum-tester` (iOS) | `o2-rum-tester` | core bundle, network(+404), attributes, user-identity, masking, logs | +| `ios-native` | `apps/o2-native-ios` | `o2-native-ios` | core bundle, attributes, logs | See **`docs/COVERAGE-MATRIX.md`** for the full capability×platform matrix, on-device validation status, and the deferred follow-ups (native/iOS background-foreground, native network/masking, @@ -103,7 +104,7 @@ iOS-native user identity — each needs a fixture-app change or a background har Prereqs: Android emulator booted for the Android projects; an iOS simulator booted (udid in `.env` `IOS_SIM_UDID`) for the iOS projects; the fixture apps built + installed (see `apps/`). -Tags: `@mobile @rn-android @rn-ios @android-native @ios-native @P0 @P1 @crash @network @negative @errors @views @replay @masking @lifecycle @attributes @user-identity @security @known-bug`. +Tags: `@mobile @rn-android @rn-ios @android-native @ios-native @P0 @P1 @crash @network @negative @errors @views @replay @masking @lifecycle @logs @attributes @user-identity @security @known-bug`. ## Notes diff --git a/tests/mobile-testing/apps/o2-native-android/app/src/main/java/com/o2native/MainActivity.kt b/tests/mobile-testing/apps/o2-native-android/app/src/main/java/com/o2native/MainActivity.kt index f5326b7c1b..c15e255119 100644 --- a/tests/mobile-testing/apps/o2-native-android/app/src/main/java/com/o2native/MainActivity.kt +++ b/tests/mobile-testing/apps/o2-native-android/app/src/main/java/com/o2native/MainActivity.kt @@ -8,6 +8,7 @@ import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import com.openobserve.android.log.Logger import com.openobserve.android.okhttp.OpenObserveInterceptor import com.openobserve.android.rum.GlobalRumMonitor import com.openobserve.android.rum.RumActionType @@ -29,6 +30,8 @@ class MainActivity : AppCompatActivity() { ) .build() + private val logger by lazy { Logger.Builder().setName("o2-e2e").build() } + private fun fetch(url: String) { http.newCall(Request.Builder().url(url).build()).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) {} @@ -82,6 +85,13 @@ class MainActivity : AppCompatActivity() { button("Fetch resource 404") { fetch("https://jsonplaceholder.typicode.com/this-path-does-not-exist-404") } + // Several logs per tap so the SDK uploads a multi-event batch (#16229). + button("Send logs") { + val burst = mapOf("log_burst" to System.currentTimeMillis().toString()) + logger.i("O2 log burst info", attributes = burst) + logger.w("O2 log burst warn", attributes = burst) + logger.e("O2 log burst error", attributes = burst) + } button("Trigger native crash") { throw RuntimeException("O2 Native — intentional uncaught crash") } diff --git a/tests/mobile-testing/apps/o2-native-ios/Sources/MainViewController.swift b/tests/mobile-testing/apps/o2-native-ios/Sources/MainViewController.swift index fe088c8286..fce597653d 100644 --- a/tests/mobile-testing/apps/o2-native-ios/Sources/MainViewController.swift +++ b/tests/mobile-testing/apps/o2-native-ios/Sources/MainViewController.swift @@ -1,9 +1,12 @@ import UIKit import OpenObserveRUM +import OpenObserveLogs class MainViewController: UIViewController { // A URLSession built with the instrumented delegate → its requests are tracked as RUM resources. private let session = URLSession(configuration: .default, delegate: O2SessionDelegate(), delegateQueue: nil) + // Module-qualified because os.Logger shares the name. + private lazy var logger = OpenObserveLogs.Logger.create(with: .init(name: "o2-e2e")) private func fetch(_ url: String) { guard let u = URL(string: url) else { return } @@ -41,6 +44,7 @@ class MainViewController: UIViewController { stack.addArrangedSubview(button("Go to Checkout", #selector(goToCheckout))) stack.addArrangedSubview(button("Fetch resource 200", #selector(fetch200))) stack.addArrangedSubview(button("Fetch resource 404", #selector(fetch404))) + stack.addArrangedSubview(button("Send logs", #selector(sendLogs))) stack.addArrangedSubview(button("Trigger native crash", #selector(triggerCrash))) } @@ -51,6 +55,14 @@ class MainViewController: UIViewController { @objc func fetch200() { fetch("https://jsonplaceholder.typicode.com/todos/1") } @objc func fetch404() { fetch("https://jsonplaceholder.typicode.com/this-path-does-not-exist-404") } + // Several logs per tap so the SDK uploads a multi-event batch (#16229). + @objc func sendLogs() { + let burst: [String: Encodable] = ["log_burst": String(Int(Date().timeIntervalSince1970 * 1000))] + logger.info("O2 log burst info", attributes: burst) + logger.warn("O2 log burst warn", attributes: burst) + logger.error("O2 log burst error", attributes: burst) + } + @objc func handledError() { RUMMonitor.shared().addError( error: NSError(domain: "O2NativeIOS", code: 1, diff --git a/tests/mobile-testing/apps/o2-rum-tester/App.tsx b/tests/mobile-testing/apps/o2-rum-tester/App.tsx index bd006477e1..b0422f5fed 100644 --- a/tests/mobile-testing/apps/o2-rum-tester/App.tsx +++ b/tests/mobile-testing/apps/o2-rum-tester/App.tsx @@ -20,6 +20,7 @@ import { OpenObserveProvider, OpenObserveProviderConfiguration, TrackingConsent, + O2Logs, O2Rum, O2SdkReactNative, RumActionType, @@ -145,6 +146,15 @@ function HomeScreen({go}: {go: (s: string) => void}) { } }; + // Several logs per tap so the SDK uploads a multi-event batch (#16229). + const sendLogs = async () => { + const burst = {log_burst: String(Date.now())}; + await O2Logs.info('O2 log burst info', burst); + await O2Logs.warn('O2 log burst warn', burst); + await O2Logs.error('O2 log burst error', burst); + setStatus('logs sent'); + }; + const crash = () => { O2Rum.addAction(RumActionType.TAP, 'Trigger crash'); // uncaught -> native crash report on next launch @@ -161,6 +171,7 @@ function HomeScreen({go}: {go: (s: string) => void}) { go('details')} /> go('form')} /> + {status} ); diff --git a/tests/mobile-testing/docs/CI-NOTES.md b/tests/mobile-testing/docs/CI-NOTES.md index a1053fe9ff..d8531e4651 100644 --- a/tests/mobile-testing/docs/CI-NOTES.md +++ b/tests/mobile-testing/docs/CI-NOTES.md @@ -113,7 +113,7 @@ cleartext-HTTP handling are recorded below / in the git history. ## Validated locally -- Suite parses (`playwright --list`): 42 tests, 4 platforms (+ sdk-drift project). +- Suite parses (`playwright --list`): 46 tests, 4 platforms (+ sdk-drift project). - `gen-config.js` / `gen-config.sh` keep the committed **placeholders** when no env is set, and substitute `O2_RUM_*` when set. - Android/iOS native config wiring is in place (`BuildConfig.OO_*`, `GeneratedConfig`). diff --git a/tests/mobile-testing/docs/COVERAGE-MATRIX.md b/tests/mobile-testing/docs/COVERAGE-MATRIX.md index 97576b3605..6b924e3c0e 100644 --- a/tests/mobile-testing/docs/COVERAGE-MATRIX.md +++ b/tests/mobile-testing/docs/COVERAGE-MATRIX.md @@ -1,6 +1,6 @@ # Mobile RUM SDK — Coverage Matrix & Gap Analysis -_Coverage of the `tests/mobile-testing` suite across the four mobile tracks. **42 tests, 5 Playwright +_Coverage of the `tests/mobile-testing` suite across the four mobile tracks. **46 tests, 5 Playwright projects** (rn-android, android-native, rn-ios, ios-native, sdk-drift). The native tracks are now at RN parity — masking, network, background/foreground, user-identity and no-phone-home ship for both `android-native` and `ios-native` (previously listed here as deferred). The PR-description matrix is the @@ -14,7 +14,7 @@ capabilities is superseded by the shipped specs._ (`pages/rumDashboardPage.js`). - **Per-capability factories** (`utils/rumChecks.js`) keep the four platforms at parity without copy-paste: `attributesSuite`, `userIdentitySuite`, `networkSuite` (positive + 404 negative), - `maskingSuite`, `bgFgSuite`, `noPhoneHomeAndroidSuite`. Each platform's spec is a thin call. + `maskingSuite`, `bgFgSuite`, `logsSuite`, `noPhoneHomeAndroidSuite`. Each platform's spec is a thin call. - **`coreRumSpec.js`** bundles the end-to-end crash journey (views + actions + handled error + native crash in `_rumdata`, then the crashed session rendering in the dashboard) for the three non-RN-Android platforms. @@ -37,6 +37,7 @@ follow-ups) · **⚠** blocked by dashboard bug o2-enterprise#2289. | Attributes: env/service/version (API) | ✓ | ✓ | ✓ | ✓ | | Session replay privacy masking (UI, negative) | ✓ | N/A no form | ✓ | N/A no form | | Background/foreground continuity (API) | ✓ | ⏭ | ⏭ | ⏭ | +| Logs upload: info/warn/error burst lands in `_rumlog` (API) | ✓ | ✓ | ✓ | ✓ | | No-phone-home: no Datadog hosts (security, negative) | ✓ | ✓ | N/A IPA | N/A IPA | | Crashed session viewable (UI) | ✓ | ✓ | ✓ | ✓ | | Error Tracking tab shows error (UI) | ⚠ skip | ⚠ skip | ⚠ skip | ⚠ skip | diff --git a/tests/mobile-testing/maestro/android-native/logs.yaml b/tests/mobile-testing/maestro/android-native/logs.yaml new file mode 100644 index 0000000000..f65e541108 --- /dev/null +++ b/tests/mobile-testing/maestro/android-native/logs.yaml @@ -0,0 +1,11 @@ +appId: com.o2native +--- +# One tap writes an info/warn/error burst through the Logs SDK; the spec asserts all three reach _rumlog. +- launchApp: + clearState: false +- scrollUntilVisible: + element: + text: "Send logs" +- tapOn: "Send logs" +- waitForAnimationToEnd: + timeout: 3000 diff --git a/tests/mobile-testing/maestro/ios-native/logs.yaml b/tests/mobile-testing/maestro/ios-native/logs.yaml new file mode 100644 index 0000000000..3df9506e5f --- /dev/null +++ b/tests/mobile-testing/maestro/ios-native/logs.yaml @@ -0,0 +1,9 @@ +appId: com.o2native.ios +--- +# One tap writes an info/warn/error burst through the Logs SDK; the spec asserts all three reach _rumlog. +- launchApp: + clearState: false +- assertVisible: "O2 Native iOS" +- tapOn: "Send logs" +- waitForAnimationToEnd: + timeout: 3000 diff --git a/tests/mobile-testing/maestro/ios-react-native/logs.yaml b/tests/mobile-testing/maestro/ios-react-native/logs.yaml new file mode 100644 index 0000000000..b34b179a5d --- /dev/null +++ b/tests/mobile-testing/maestro/ios-react-native/logs.yaml @@ -0,0 +1,15 @@ +# Drives O2RumTester on iOS: one tap writes an info/warn/error burst through O2Logs (the spec asserts _rumlog). +appId: org.reactjs.native.example.O2RumTester +--- +- launchApp: + clearState: false +- extendedWaitUntil: + visible: "O2 RUM Tester" + timeout: 40000 +- scrollUntilVisible: + element: + text: "Send logs" +- tapOn: "Send logs" +- assertVisible: "logs sent" +- waitForAnimationToEnd: + timeout: 3000 diff --git a/tests/mobile-testing/maestro/react-native/logs.yaml b/tests/mobile-testing/maestro/react-native/logs.yaml new file mode 100644 index 0000000000..22b144cf7e --- /dev/null +++ b/tests/mobile-testing/maestro/react-native/logs.yaml @@ -0,0 +1,15 @@ +# Drives O2RumTester: one tap writes an info/warn/error burst through O2Logs (the spec asserts _rumlog). +appId: com.o2rumtester +--- +- launchApp: + clearState: false +- extendedWaitUntil: + visible: "O2 RUM Tester" + timeout: 40000 +- scrollUntilVisible: + element: + text: "Send logs" +- tapOn: "Send logs" +- assertVisible: "logs sent" +- waitForAnimationToEnd: + timeout: 3000 diff --git a/tests/mobile-testing/specs/android-native.logs.spec.js b/tests/mobile-testing/specs/android-native.logs.spec.js new file mode 100644 index 0000000000..36895db5b8 --- /dev/null +++ b/tests/mobile-testing/specs/android-native.logs.spec.js @@ -0,0 +1,9 @@ +const { logsSuite } = require('../utils/rumChecks'); +const cfg = require('../utils/config'); + +logsSuite({ + name: 'Android-native · Logs upload (multi-event batch)', + tags: ['@mobile', '@android-native'], + service: cfg.NATIVE_ANDROID_SERVICE, + flows: ['android-native/logs.yaml'], +}); diff --git a/tests/mobile-testing/specs/ios-native.logs.spec.js b/tests/mobile-testing/specs/ios-native.logs.spec.js new file mode 100644 index 0000000000..35140f3019 --- /dev/null +++ b/tests/mobile-testing/specs/ios-native.logs.spec.js @@ -0,0 +1,10 @@ +const { logsSuite } = require('../utils/rumChecks'); +const cfg = require('../utils/config'); + +logsSuite({ + name: 'iOS-native · Logs upload (multi-event batch)', + tags: ['@mobile', '@ios-native'], + service: cfg.NATIVE_IOS_SERVICE, + flows: ['ios-native/logs.yaml'], + device: cfg.IOS_SIM_UDID, +}); diff --git a/tests/mobile-testing/specs/rn-android.logs.spec.js b/tests/mobile-testing/specs/rn-android.logs.spec.js new file mode 100644 index 0000000000..618e7cbc5c --- /dev/null +++ b/tests/mobile-testing/specs/rn-android.logs.spec.js @@ -0,0 +1,9 @@ +const { logsSuite } = require('../utils/rumChecks'); +const cfg = require('../utils/config'); + +logsSuite({ + name: 'RN Android · Logs upload (multi-event batch)', + tags: ['@mobile', '@rn-android'], + service: cfg.RN_SERVICE, + flows: ['react-native/logs.yaml'], +}); diff --git a/tests/mobile-testing/specs/rn-ios.logs.spec.js b/tests/mobile-testing/specs/rn-ios.logs.spec.js new file mode 100644 index 0000000000..c5cc07fc19 --- /dev/null +++ b/tests/mobile-testing/specs/rn-ios.logs.spec.js @@ -0,0 +1,10 @@ +const { logsSuite } = require('../utils/rumChecks'); +const cfg = require('../utils/config'); + +logsSuite({ + name: 'RN iOS · Logs upload (multi-event batch)', + tags: ['@mobile', '@rn-ios'], + service: cfg.RN_SERVICE, + flows: ['ios-react-native/logs.yaml'], + device: cfg.IOS_SIM_UDID, +}); diff --git a/tests/mobile-testing/utils/config.js b/tests/mobile-testing/utils/config.js index b662112c7a..c8d0af01c7 100644 --- a/tests/mobile-testing/utils/config.js +++ b/tests/mobile-testing/utils/config.js @@ -14,6 +14,7 @@ module.exports = { OO_PASS: need('OO_PASS'), RN_ENV: process.env.RN_ENV || 'production', RUM_STREAM: '_rumdata', + RUM_LOG_STREAM: '_rumlog', // Per-platform app ids + service names (the RN app is one service across both platforms; // distinguish RN-iOS vs RN-Android by `source`/`os`, natives have their own service). diff --git a/tests/mobile-testing/utils/ooClient.js b/tests/mobile-testing/utils/ooClient.js index 4b514a21dd..cf89f11c56 100644 --- a/tests/mobile-testing/utils/ooClient.js +++ b/tests/mobile-testing/utils/ooClient.js @@ -122,6 +122,18 @@ const q = { }, opts, ), + // SELECT * because naming the custom log_burst column fails with "unknown field" until a burst has landed. + logsUntil: (service, startMs, messagePrefix, predicate, opts) => + pollUntil( + () => + search( + `SELECT * FROM ${cfg.RUM_LOG_STREAM} ` + + `WHERE service='${esc(service)}' AND message LIKE '${esc(messagePrefix)}%' ORDER BY _timestamp DESC`, + startMs, + ), + predicate, + opts, + ), sessionForService: async (service, startMs, opts) => { const rows = await pollSearch( `SELECT session_id, _timestamp FROM ${cfg.RUM_STREAM} ` + diff --git a/tests/mobile-testing/utils/rumChecks.js b/tests/mobile-testing/utils/rumChecks.js index 43d1f79406..ab5208e1f4 100644 --- a/tests/mobile-testing/utils/rumChecks.js +++ b/tests/mobile-testing/utils/rumChecks.js @@ -233,6 +233,54 @@ function bgFgSuite({ name, tags, service, viewA, viewB, drive, device = '' }) { }); } +/** Logs upload: one tap's info/warn/error burst reaches _rumlog as exactly three rows (#16229). */ +function logsSuite({ name, tags, service, flows, device = '' }) { + const prefix = 'O2 log burst'; + const sent = ['error', 'info', 'warn']; + // The newest burst is this run's tap; earlier bursts in the window come from retries. + const newestBurst = (rows) => + rows.length ? rows.filter((r) => r.log_burst === rows[0].log_burst) : []; + + test.describe(name, () => { + test( + 'a multi-event log batch is uploaded and every event lands in _rumlog', + { tag: [...tags, '@logs', '@P0'] }, + async () => { + const start = Date.now() - 30000; + for (const f of flows) runFlow(f, { device }); + + const rows = await q.logsUntil( + service, + start, + prefix, + (r) => newestBurst(r).length >= sent.length, + { tries: 24, delayMs: 5000 }, + ); + + const burst = newestBurst(rows); + expect( + burst.length, + `rows for the newest log burst (saw ${rows.length} burst rows in total)`, + ).toBe(sent.length); + expect(burst[0].log_burst, 'the custom log attribute is stored').toBeTruthy(); + expect(burst.map((r) => r.status).sort(), 'one row per level sent').toEqual(sent); + expect( + burst.every((r) => r.message === `${prefix} ${r.status}`), + 'each message matches its level', + ).toBe(true); + expect( + burst.every((r) => r.service === service), + 'every row tagged with the service', + ).toBe(true); + expect( + burst.every((r) => !!r.session_id), + 'every log carries the RUM session_id', + ).toBe(true); + }, + ); + }); +} + /** Security (negative): the installed Android APK must contain zero Datadog intake hosts. */ function noPhoneHomeAndroidSuite({ name, tags, appId }) { test.describe(name, () => { @@ -312,6 +360,7 @@ module.exports = { networkSuite, maskingSuite, bgFgSuite, + logsSuite, noPhoneHomeAndroidSuite, noPhoneHomeIosSuite, };