feat: flutter parity — at-most-once export, minimal defaults, exit-info crash classification (v0.1.12) - #1
Merged
Merged
Conversation
…fo crash classification (v0.1.12)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
crash.type = 'exit_info'will silently return zero rows. Exit-info records now carry the real reason incrash.type(jvm_crash/native_crash/anr/low_memory); the detection path moved to a newcrash.sourceattribute. The dashboard owner needs to migrate those panels.Why the defaults changed
Four problems, all measured in production on the flutter side:
@opentelemetry/exporter-*-otlp-httpexporters wrap their transport inRetryingTransport, 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.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-transformerserializers and the POST shape already proven inoffline-buffer.ts; metrics keep CUMULATIVE temporality so backend semantics are unchanged.Defaults. Vitals metrics off;
exportRetry.maxRetries0;offlineBuffer.enabledfalse; 30s cadence for all signals via a new unifiedexportIntervalSeconds; 60s vitals sampling viavitalsCollectionIntervalSeconds. Per-signal*Msknobs 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.versionresource attribute on every span, metric and log, pinned topackage.jsonby a contract test and placed after the integrator'sresourceAttributesspread so it can't be shadowed.Crash quality.
app_crashis now attributed to the session that actually died (session.id,session.start_time, and acrash.timestampof 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)
wrapWithRetryreturned the exporter untouched whenmaxRetries <= 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.crash.started_atwas the marker's write time, not the session's start time.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 intomake ci. They pack the real tarball, so the exports map is checked as consumers see it. They found four live bugs:moduleResolution: "node"— which covers most React Native tsconfigs.@base-14/scout-react/nativesilently resolved toany. Fixed withtypesVersions.typescondition was reused forrequire. Now split per condition against the.d.ctsfiles tsup was already emitting but nothing pointed at../nativecould not be loaded from Node at all.tsconfig.native.jsonemits CommonJS but the root package is"type": "module", so Node readdist/native/**anddist/core/**as ESM and everyrequirethrew. 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-pluginshipped 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__esModulewithoutmodule.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-androidnever worked — it called a baregradle, 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.make cirunsvitest run, not--coverage, andif-no-files-found: ignorehid it. Now produced explicitly and set toerror.publish.ymlhad no tag↔version guard. npm publishes whatpackage.jsonsays, ignoring the tag. Combined with the newSCOPE_VERSIONpin, the chain is now complete: git tag → package.json → the version stamped on every span. Publish also ran onlynpm test(no lint, no typecheck) despite tags being pushable from any commit; it now runsmake ci.make builddelegates to the npm script so it can't drift fromprepare.Verification
make cigreen: prettier, eslint (0 errors, 0 warnings — removed a deadeslint-disablefor a rule that was never enabled), typecheck, 188 tests / 24 files, build, publint + attw (No problems found).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.On-device — iOS 26.4 simulator
Built from this branch with
npm packand run in thepokedex-react-nativeexample against the live
oteldemo2RUM ingest endpoint. Data confirmed landingin ClickHouse and rendering in the RUM Grafana app.
attempt 1; the retry path never fired once.screen.nameon every emit. Span mix:http.requestx14,user_interactionx5,screen_loadx5,app_vitalx5,view_sessionx4,plus
error,app_startup,app_paused,app_lifecycle.changedandscreen_viewroot spans.dist/nativewith 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.12autolinked through Expo modules alongside KSCrash 2.5.1./v1/tracesreturned 200 with the token and 403 without.App-side harness fixes are in base-14/scout-rum-example-apps#2. Note that
Scout.attachNavigationContainerhad never been called in that app, so screentracking 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
anris 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 producescrash.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.ResourceMetricsenvelope 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 tofalseand every native emitter is gated on them, so this is very likely fine, but it was not directly observed.keepalive: trueon 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 intootlp-exporter.tsbefore release.core/docs/docs/instrument/mobile/react-native.mdin the base14 repo — that path isn't in the local checkout. The SDK's owndocs/configuration.mdis updated.@opentelemetry/core <2.8.0, GHSA-8988-4f7v-96qf). CI's--audit-level=highgate passes so this is invisible today; the fix pulls in a breakingsdk-logsbump, which I wouldn't do in this PR.