mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
test(mobile): check that SDK log batches reach _rumlog on all four tracks (#16353)
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+10
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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).
|
||||
|
||||
@@ -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} ` +
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user