test(mobile): check that SDK log batches reach _rumlog on all four tracks (#16353)

This commit is contained in:
Vamsi Karthik R
2026-11-05 03:25:30 +00:00
committed by GitHub
parent 51dd5a3acf
commit 0ed4d011c5
18 changed files with 207 additions and 11 deletions
+13 -2
View File
@@ -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};
+7 -6
View File
@@ -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
@@ -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")
}
@@ -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,
@@ -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}) {
<Btn label="Trigger native crash" onPress={crash} tone="danger" />
<Btn label="Go to Details →" onPress={() => go('details')} />
<Btn label="Go to Checkout form →" onPress={() => go('form')} />
<Btn label="Send logs" onPress={sendLogs} />
<Text style={styles.status}>{status}</Text>
</View>
);
+1 -1
View File
@@ -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`).
+3 -2
View File
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'],
});
@@ -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,
});
@@ -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'],
});
@@ -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,
});
+1
View File
@@ -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).
+12
View File
@@ -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} ` +
+49
View File
@@ -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,
};