Skip to content

feat: flutter parity — at-most-once export, minimal defaults, exit-info crash classification (v0.1.12) - #1

Merged
nitinstp23 merged 2 commits into
mainfrom
release/v0.1.12
Aug 4, 2026
Merged

feat: flutter parity — at-most-once export, minimal defaults, exit-info crash classification (v0.1.12)#1
nitinstp23 merged 2 commits into
mainfrom
release/v0.1.12

Conversation

@nitinstp23

@nitinstp23 nitinstp23 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Brings scout-react to parity with scout-flutter 0.1.23's production-hardening work, per scout-react-prd-0.1.12. All 13 PRD requirements (P0+P1+P2). No API removals — only defaults change.

⚠️ Action required before merging

  • Dashboards filtering crash.type = 'exit_info' will silently return zero rows. Exit-info records now carry the real reason in crash.type (jvm_crash / native_crash / anr / low_memory); the detection path moved to a new crash.source attribute. The dashboard owner needs to migrate those panels.
  • Upgrading without config changes turns things off. Vitals metrics, export retries and offline buffering all default off now, and trace/log cadence goes 5s → 30s. Every one is opt-in-able; see CHANGELOG for the snippets.

Why the defaults changed

Four problems, all measured in production on the flutter side:

  1. Duplicate spans. The stock @opentelemetry/exporter-*-otlp-http exporters wrap their transport in RetryingTransport, which re-sends up to five more times on 429/502/503/504 and on network errors — stacked under this SDK's own retry wrapper, one batch could reach the collector ~20 times. Retrying an ambiguous failure (a timeout the collector may already have ingested) re-delivers identical span IDs.
  2. Telemetry volume. Frame/memory/CPU metrics defaulted on and are the highest-volume signal the SDK produces.
  3. Connection churn. One TCP+TLS handshake per beacon inflates ALB LCU cost.
  4. Crash-count inflation. Android reported swipe-from-recents, Force Stop and self-exit as crashes.

Main changes

Export path. Replaced the three stock OTLP exporters with a fetch-based OTLP/JSON exporter (src/core/otlp-exporter.ts) where one export is exactly one request. Retry policy now lives in one place and is off by default. Reuses the existing @opentelemetry/otlp-transformer serializers and the POST shape already proven in offline-buffer.ts; metrics keep CUMULATIVE temporality so backend semantics are unchanged.

Defaults. Vitals metrics off; exportRetry.maxRetries 0; offlineBuffer.enabled false; 30s cadence for all signals via a new unified exportIntervalSeconds; 60s vitals sampling via vitalsCollectionIntervalSeconds. Per-signal *Ms knobs still win when set explicitly.

Android. Exit-info classification narrowed to the four genuine crash reasons. ANR watchdog polls every 100ms instead of threshold/10, so a hang is reported at threshold + ~0.1s.

scout.react.version resource attribute on every span, metric and log, pinned to package.json by a contract test and placed after the integrator's resourceAttributes spread so it can't be shadowed.

Crash quality. app_crash is now attributed to the session that actually died (session.id, session.start_time, and a crash.timestamp of when the app was last known alive rather than when the crash was noticed on relaunch). Breadcrumbs are session-scoped, so a relaunched session no longer inherits the dead one's trail — those crumbs go to the crash report instead.

Bugs found along the way (not in the PRD)

  • Offline buffering was silently dead at zero retries. wrapWithRetry returned the exporter untouched when maxRetries <= 0, so the hook feeding the offline buffer never installed. Flipping the default to 0 would have killed buffering even for integrators who explicitly re-enabled it.
  • Web crash.started_at was the marker's write time, not the session's start time.
  • Console-capture feedback loop. With captureConsole + debug, the SDK's own [scout] diagnostics were captured as logs, producing more exports, which logged again.

Packaging fixes (publint + arethetypeswrong, now gating in CI)

Added both tools as make check-exports, wired into make ci. They pack the real tarball, so the exports map is checked as consumers see it. They found four live bugs:

  • Subpath types were unresolvable under moduleResolution: "node" — which covers most React Native tsconfigs. @base-14/scout-react/native silently resolved to any. Fixed with typesVersions.
  • CJS consumers got ESM types — one top-level types condition was reused for require. Now split per condition against the .d.cts files tsup was already emitting but nothing pointed at.
  • ./native could not be loaded from Node at all. tsconfig.native.json emits CommonJS but the root package is "type": "module", so Node read dist/native/** and dist/core/** as ESM and every require threw. Both directories now carry a {"type":"commonjs"} marker — not a rename to .cjs, which would move Metro's entry point. Verified against a real installed tarball.
  • ./babel-plugin shipped no type declarations at all.

One known caveat left deliberately: import Scout from '@base-14/scout-react/native' (the README's snippet) resolves to the module namespace under Node ESM, because tsc's CJS output sets __esModule without module.exports. Metro and Babel handle it correctly, so app code is unaffected. Fixing it means dropping the documented default export or patching tsc's output; README now points Node users at the named export.

Build / CI

  • make test-android never worked — it called a bare gradle, which isn't installed by default (exit 127). Added a checked-in Gradle wrapper with a pinned, checksum-verified distribution, so a JDK is the only prerequisite. CI validates the wrapper.
  • CI's coverage artifact was always empty. make ci runs vitest run, not --coverage, and if-no-files-found: ignore hid it. Now produced explicitly and set to error.
  • publish.yml had no tag↔version guard. npm publishes what package.json says, ignoring the tag. Combined with the new SCOPE_VERSION pin, the chain is now complete: git tag → package.json → the version stamped on every span. Publish also ran only npm test (no lint, no typecheck) despite tags being pushable from any commit; it now runs make ci.
  • make build delegates to the npm script so it can't drift from prepare.

Verification

  • make ci green: prettier, eslint (0 errors, 0 warnings — removed a dead eslint-disable for a rule that was never enabled), typecheck, 188 tests / 24 files, build, publint + attw (No problems found).
  • 11 Kotlin tests via make test-android, covering the exit-info crash-class and benign sets and the ANR hang-detection rules. Real JUnit execution, not just CI config.
  • Duplicate-delivery is test-locked: exactly one request on 500/429/502/503/504 and on timeout-then-recover.

On-device — iOS 26.4 simulator

Built from this branch with npm pack and run in the pokedex-react-native
example against the live oteldemo2 RUM ingest endpoint. Data confirmed landing
in ClickHouse and rendering in the RUM Grafana app.

export cycles:   20 OK,  0 failures,  0 retries
spans exported:  42        logs: 2
  • At-most-once export confirmed on real hardware. Every batch delivered on
    attempt 1; the retry path never fired once.
  • screen.name on every emit. Span mix: http.request x14,
    user_interaction x5, screen_load x5, app_vital x5, view_session x4,
    plus error, app_startup, app_paused, app_lifecycle.changed and
    screen_view root spans.
  • Metro resolves dist/native with the new {"type":"commonjs"} markers.
    This was previously verified only against Node, with Metro's resolver argued
    rather than tested — it now bundles and runs for real. ScoutNativeCrash 0.1.12 autolinked through Expo modules alongside KSCrash 2.5.1.
  • Ingest auth validated independently before the app ran: a probe POST to
    /v1/traces returned 200 with the token and 403 without.

App-side harness fixes are in base-14/scout-rum-example-apps#2. Note that
Scout.attachNavigationContainer had never been called in that app, so screen
tracking was entirely dead — worth checking for in other integrations, since the
SDK gives no warning when the navigation tracker is simply never installed.

Not done

  • On-device Android verification. Still outstanding, and it is the half that matters most for this PR — anr is Android-only and the exit-info reclassification cannot be exercised on iOS at all. Needs an emulator + collector: a 500-returning collector receives exactly one POST per batch; Force Stop and swipe-kill produce no crash span while a real NDK SIGABRT produces crash.type = native_crash + crash.source = exit_info; ANR within ~5.1s; socket reuse across three consecutive 30s exports (adb shell ss -tp). No Android SDK/NDK is installed on the machine used for the iOS run.
  • "Default init emits zero metrics" is not confirmed at the data-point level. The iOS run shows the metrics pipeline exporting one ResourceMetrics envelope per cycle, which is the expected shape whether or not it carries data points — the debug log counts envelopes, not points. The three metric flags do default to false and every native emitter is gated on them, so this is very likely fine, but it was not directly observed.
  • P1-3 connection reuse is unverified. I deliberately did not set keepalive: true on fetch — it's for unload beacons and caps bodies at 64 KB. If the on-device check shows sockets closing, a keep-alive transport goes into otlp-exporter.ts before release.
  • core/docs/docs/instrument/mobile/react-native.md in the base14 repo — that path isn't in the local checkout. The SDK's own docs/configuration.md is updated.
  • 7 moderate CVEs in prod deps (@opentelemetry/core <2.8.0, GHSA-8988-4f7v-96qf). CI's --audit-level=high gate passes so this is invisible today; the fix pulls in a breaking sdk-logs bump, which I wouldn't do in this PR.

@nitinstp23 nitinstp23 self-assigned this Aug 4, 2026
@nitinstp23
nitinstp23 requested a review from nimishgj August 4, 2026 05:22
@nitinstp23
nitinstp23 merged commit 3614a52 into main Aug 4, 2026
4 checks passed
@nitinstp23
nitinstp23 deleted the release/v0.1.12 branch August 4, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant