@base-14/scout-react

中文文档
0.1.22发布于 2026/11/05MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2488

Zero-config OpenTelemetry RUM for React and React Native. Auto-captures clicks, navigation, errors, lifecycle, network, performance, and web vitals.

原始使用文档
文档目录23 节
  1. Install
  2. Bare React Native (no Expo)
  3. Quick start — React (browser)
  4. Quick start — React Native
  5. What's captured
  6. Cross-platform (web + RN)
  7. Web only
  8. React Native only
  9. Resource attributes (shared across all signals)
  10. InstrumentationScope
  11. Distributed tracing
  12. Identity and breadcrumbs
  13. Filtering and PII scrubbing — beforeSend
  14. Sampling — defaults, and how errors stay loud
  15. User-based sampling — let your app decide per user
  16. Full configuration
  17. Background flush
  18. Embedded in a native app? — the WebView bridge
  19. Browser support
  20. Out of scope (for now)
  21. Runnable samples
  22. Development
  23. License

Scout React

npm version license

Zero-config OpenTelemetry RUM (Real User Monitoring) for React (browser) and React Native. Install the package, call Scout.initialize() — that's it.

Auto-captures clicks, navigation, errors, lifecycle, network, performance, web vitals, and device context. Exports over OTLP/HTTP.

import Scout from '@base-14/scout-react';

await Scout.initialize({
  serviceName: 'my-app',
  endpoint: 'https://otlp.example.com:4318',
});

That's all the code you write.


Install

npm install @base-14/scout-react

Peer deps are installed by the host app on demand (none of them are required for the web entry):

Use casePeer deps
Web (browser)react (only if you use /react helpers)
React Nativereact-native, @react-native-async-storage/async-storage
RN + navigation tracking@react-navigation/native
RN + connectivity tracking@react-native-community/netinfo
RN + device model/manufacturerreact-native-device-info
RN + battery levelexpo-battery (preferred) or react-native-device-info

Bare React Native (no Expo)

The native layer (ScoutCrash: ANR / UI-hang detection, native crash capture, accurate cold-start timing) is built on the Expo Modules API. That's the only part of Expo it uses — no managed workflow, Expo CLI, or Expo Go. Bare apps need Expo's module infrastructure installed once:

npx install-expo-modules@latest
npx pod-install

This adds the expo package and wires up autolinking in MainApplication / AppDelegate. Installing expo-modules-core on its own is not enough — the module won't be linked.

Native capture behaves identically in bare and Expo apps. If the module isn't linked, the SDK doesn't fail: it falls back to JS-only capture, which means no ANR, hang, or native-crash spans, and cold start measured from JS bundle load instead of process start. To confirm it's linked, check in a release build on both platforms:

import { requireOptionalNativeModule } from 'expo-modules-core';

const mod = requireOptionalNativeModule('ScoutCrash');
console.log('ScoutCrash linked:', !!mod && (await mod.isInstalled()));

Quick start — React (browser)

import Scout from '@base-14/scout-react';

await Scout.initialize({
  serviceName: 'my-app',
  endpoint: 'https://otlp.example.com:4318',
});

That's it. Clicks, route changes, errors, fetch + XHR, lifecycle, web vitals, long tasks, memory, and crashes are all auto-captured. No per-button instrumentation.

Optional React helpers from the /react subpath:

import { ScoutProvider, useScout, ScoutErrorBoundary, ScoutAction } from '@base-14/scout-react/react';

<ScoutProvider config={{ serviceName: 'my-app', endpoint: '...' }}>
  <ScoutErrorBoundary fallback={(e) => <ErrorPage error={e} />}>
    <App />
  </ScoutErrorBoundary>
</ScoutProvider>;

// Override the tap label for a widget the SDK can't auto-name:
<ScoutAction description="Add to cart">
  <MyFancyButton />
</ScoutAction>

Quick start — React Native

Two lines in your entry file. Nothing in your component tree.

// index.ts
import Scout from '@base-14/scout-react/native';
import App from './App';

Scout.registerRootComponent(App);

registerRootComponent installs a root-level error + touch boundary via RN's AppRegistry.setWrapperComponentProvider. Every tap becomes a user_interaction span, every render error becomes an error span (with error.component_stack), automatically.

Running the native entry under plain Node (Jest without a React Native preset, SSR, scripts) — use the named export, import { Scout } from '@base-14/scout-react/native'. dist/native is CommonJS so Metro can see literal require()s; Node's ESM interop does not honour its __esModule marker, so the default import resolves to the module namespace rather than the class. Metro and Babel handle the default import correctly, so app code is unaffected.

Then in your App.tsx:

import Scout from '@base-14/scout-react/native';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';

export default function App() {
  const navRef = useNavigationContainerRef();
  useEffect(() => {
    Scout.initialize({
      serviceName: 'my-app',
      endpoint: 'https://otlp.example.com:4318',
    });
  }, []);
  return (
    <NavigationContainer
      ref={navRef}
      onReady={() => Scout.attachNavigationContainer(navRef)}>
      {/* ... */}
    </NavigationContainer>
  );
}

On Android USB devices, the OTLP endpoint runs on your dev machine — point it at http://localhost:<port> and run adb reverse tcp:<port> tcp:<port> so the phone forwards through USB. iOS simulator shares the host network so localhost works directly.


What's captured

Cross-platform (web + RN)

SignalSpan / metricNotes
Clicks / tapsuser_interactionuser_interaction.target, target.type
Value changes, submits, text editsuser_interactionWeb only. user_interaction.type is click | change | submit | input; see interactionEvents
Navigationscreen_view, view_sessionscreen_view becomes the root span — all spans on that screen share its trace id
Screen load timescreen_loadscreen.load_time in seconds
App startupapp_startupcold + warm
Lifecycleapp_paused, app_resumedWeb: visibilitychange; RN: AppState
Errors (uncaught)error span + error.count metricerror.handled=false
Errors (manual / boundary)error spanerror.handled=true, error.component_stack if from a React boundary
Unhandled rejectionserror spanWeb: window event; RN: HermesInternal
Long taskslong_taskDefault threshold 100ms
Frozen framesfrozen_frameLong task ≥ 700ms; background / suspended time excluded, capped at frozenFrameMaxMs
ANRanrWeb: worker watchdog. RN: timer-drift watchdog.
HTTP (fetch + XHR)http.requestMethod, URL, status, duration, content-length
Unclean exit (OOM / force-kill / tab discard)app_unclean_exit on next launchPersistent session marker (localStorage on web, AsyncStorage on RN). Not a crash — pagehide also fails to fire on ordinary closes, so it never touches crash-free rate. Off by default inside embedded WebViews (enableUncleanExitDetection)
Native crash (RN)native_crash on next launchiOS: KSCrash 2.5+ (mach exceptions, POSIX signals, C++, NSException, main-thread deadlock) + MetricKit (MXCrashDiagnostic, MXHangDiagnostic) on iOS 14+. Android: uncaught Java/Kotlin (Thread.setDefaultUncaughtExceptionHandler) + NDK signal handler for native crashes + ApplicationExitInfo (API 30+) for OS-recorded process deaths including ANR (low-memory kills are emitted as app_exit, not as crashes). Reports persisted to disk and emitted on next launch with full register / stack / binary-image dumps, prior breadcrumbs, and crash.type / crash.reason / crash.stack_trace
LogsOTLP logsScout.logDebug/Info/Warning/Error and (opt-in) console.* capture

Web only

SignalNotes
Web vitalsLCP, INP, CLS, TTFB, FCP via web-vitals (with sub-parts — INP input_delay_ms/processing_duration_ms/presentation_delay_ms, LCP load_delay_ms/load_time_ms/render_delay_ms, CLS layout-shift rects)
Memoryweb.memory.usage gauge (Chromium only)
Frame metricsweb.frame.build_time histogram via Long Animation Frame API
Batterydevice.battery.level, device.battery.state via navigator.getBattery (Chromium)
Console captureOpt-in via captureConsole: true

React Native only

SignalNotes
Batterydevice.battery.level, device.battery.state via expo-battery or react-native-device-info
Devicemodel, manufacturer, brand, is_physical, screen dimensions via react-native-device-info + RN.Dimensions
Network connectivity typevia @react-native-community/netinfo
OS name / versionvia Platform.OS, Platform.Version
Memoryreact_native.memory.usage gauge — polls react-native-device-info.getUsedMemory() every 10s
Frame metricsreact_native.frame.build_time histogram + react_native.frame.dropped gauge sampled via requestAnimationFrame
Long tasks / frozen frameslong_task and frozen_frame spans derived from the same rAF loop (≥ longTaskThresholdMs, ≥ 700ms respectively)
Console captureOpt-in via captureConsole: true

Resource attributes (shared across all signals)

Attached to every ResourceSpans / ResourceMetrics / ResourceLogs batch:

  • service.name, service.version, environment
  • os.name, os.version
  • device.model.name, device.manufacturer, device.brand, device.is_physical
  • screen.width, screen.height, screen.pixel_ratio
  • device.battery.level, device.battery.state
  • Anything you pass via config.resourceAttributes (e.g. deployment.region, team)

InstrumentationScope

Every span, metric, and log emitted by this SDK carries a single OpenTelemetry InstrumentationScope:

name:    base14.scout.react
version: <package.json version>

This is enforced by a CI guard test — backends can filter on scope.name to identify all Scout-originated telemetry regardless of which signal or which auto-instrumentation produced it.


Distributed tracing

Hosts listed in firstPartyHosts receive a W3C traceparent header on outgoing fetch / XHR. The header uses the trace + span ID of the SDK's http.request span, so your backend's spans parent under the browser's request and the entire flow appears in one trace.

firstPartyHosts: [
  'api.acme.com',         // exact host
  '*.acme.com',           // matches all subdomains AND the apex
  /^api\d+\.acme\.com$/,  // regex tested against the URL host
],

Third-party hosts (Stripe, Google Fonts, Segment, etc.) are still captured as http.request spans locally — they just don't get the header.


Identity and breadcrumbs

Scout.setUser('user-123', { email: '[email protected]', plan: 'pro' });
Scout.clearUser();
// With user-based sampling in play, switching to a different user (or
// clearUser) starts a new session. See "User-based sampling" below.

Scout.addBreadcrumb('checkout', 'added item to cart');
Scout.addBreadcrumb('checkout', 'entered payment details');
// The last 20 breadcrumbs are JSON-serialized onto every `error` and
// `app_crash` span so you can see what the user did before the crash.

Scout.reportError(new Error('payment failed'));
Scout.logEvent('purchase_completed', { sku: 'SKU-1', amount: '49.99' });

Filtering and PII scrubbing — beforeSend

Runs on every span / metric / log before export. Return null to drop, return the event to send modified.

beforeSend: (event) => {
  // Drop health checks
  if (String(event['http.url'] ?? '').includes('/health')) return null;
  // Scrub PII
  delete event['enduser.email'];
  return event;
},

Sampling — defaults, and how errors stay loud

Scout defaults to 1% session sampling (sessionSampleRate: 1). Out of every ~100 user sessions, only one keeps the full instrumentation trail; the other 99 emit nothing. This keeps telemetry volume (and collector cost) bounded as your app scales.

Errors are never sampled out by default. When alwaysCaptureErrors: true (the default), these signals bypass the session-sample gate and are always exported, regardless of whether the session was sampled in:

SignalSpan / logWhy it bypasses
Caught error reported via Scout.reportError(...)error spanYou always want failures visible.
Uncaught JS exception, unhandled rejection, React error-boundary catcherror spanSame — drop on the floor isn't acceptable.
Scout.logError(...) / ERROR-severity logslog recordHigh-severity logs treated like errors.
Native crash marker (RN app_crash)app_crash on next launchCrashes that lose the prior session must reach you. (app_unclean_exit is not crash-class: it is reported only when the session it describes was sampled.)
Native crash (iOS Mach exception / Android NDK signal / JVM uncaught)native_crash on next launchSame — fatal-class signal.
ANR (app not responding)anr spanFatal-class UX failure.

If you set alwaysCaptureErrors: false, errors get the same 1% treatment as everything else. Most teams should leave it at the default.

Everything else — user_interaction, screen_view, http.request, long_task, frozen_frame, web vitals, performance metrics — respects sessionSampleRate. Crank it to 100 for dev, leave it at 1 for production high-traffic apps, or set it to something in between (e.g. 10) for a less aggressive sample.

Scout.initialize({
  serviceName: 'my-app',
  endpoint: 'https://otel.example.com:4318',

  sessionSampleRate: 1,           // default — 1% of sessions
  alwaysCaptureErrors: true,      // default — errors always export

  // For local dev / staging:
  // sessionSampleRate: 100,      // capture every session
});

The sampling decision is made once per session, then frozen for that session's lifetime. A session is either fully sampled in (every signal flows) or sampled out (only error-class signals flow). This keeps individual sessions coherent — you never get half-a-trace.

User-based sampling — let your app decide per user

Per-session sampling means one user ends up with some sessions kept and others dropped. To decide per user instead, for example by cohort, plan or geography, pass a value from your own remote config or feature-flag service. Scout makes no network call for this; the targeting is entirely yours.

// At startup: the last-fetched remote-config value, read synchronously from its cache.
Scout.initialize({ serviceName: 'my-app', endpoint, userSampling: remoteConfig.cached('scout_user_sampling') });

// When the value arrives or changes (fetch completes, user logs in):
Scout.setUserSampling(true);  // keep this user's sessions
Scout.setUserSampling(false); // drop this user's spans; logs and metrics still flow
Scout.setUserSampling(null);  // no opinion: fall back to sessionSampleRate
Value at session startSpansLogs and metrics
trueall sentsent
falsenone, except error-class spans while alwaysCaptureErrors is onstill sent
null / never setsessionSampleRate roll, as aboveas above
  • Applies at the next session start, never mid-session. A new session starts after idle timeout, after max duration, on a reload after the session expired, or on a user switch. A reload within the session keeps its decision.
  • Persisted. The latest value is stored, so a load that passes nothing still honours it. A non-null userSampling at init wins over the stored value. A setUserSampling call made before initialize() resolves is held and decides the first session.
  • User switches. While user sampling is in play, clearUser() (which also resets the value), and setUser() with an id different from the previous one, start a new session. On an account switch, call setUserSampling(newValue) before setUser(newId). Going from anonymous to the first login keeps the current session.
  • Verify in Scout. Every span, log and metric carries session.sampling_source = user | fallback.
  • Not a kill switch. The SDK keeps running for a "no" user; only their spans stop.

Full configuration

await Scout.initialize({
  // Required
  serviceName: 'my-app',
  endpoint: 'https://otlp.example.com:4318',

  // Identity / pipeline
  serviceVersion: '1.0.0',
  environment: 'production',
  secure: true,                              // https when scheme omitted
  headers: { Authorization: 'Bearer ...' },  // sent on every OTLP export

  // Auto-instrumentation toggles (all default true)
  enableAutoTapTracking: true,
  enableErrorTracking: true,
  enableLifecycleTracking: true,
  enableStartupTracking: true,
  enableConnectivityTracking: true,
  enablePerformanceMetrics: true,
  enableLongTaskDetection: true,
  enableAnrDetection: true,
  enableFrameMetrics: true,                  // web + RN
  enableMemoryMetrics: true,                 // web + RN
  enableWebVitals: true,                     // web
  enableBatteryTracking: true,
  enableNetworkTracking: true,
  enableLogging: true,
  captureConsole: false,
  capturePrintStatements: false,             // alias for captureConsole

  // Thresholds
  longTaskThresholdMs: 100,
  anrThresholdMs: 5000,

  // Sessions
  sessionSampleRate: 1,                      // 0..100, default 1 (1% of sessions)
  alwaysCaptureErrors: true,                 // errors bypass sessionSampleRate (default true)
  sessionTimeoutMinutes: 30,

  // Network
  firstPartyHosts: ['api.example.com', '*.example.com'],
  ignoreUrlPatterns: [/\/health$/],

  // Storage
  maxOfflineStorageMb: 5,

  // Filtering
  beforeSend: (event) => event,

  // Custom resource attributes
  resourceAttributes: {
    'deployment.region': 'us-east-1',
    team: 'mobile',
  },

  // Export pacing (defaults match production needs)
  metricExportIntervalMs: 30000,
  logExportScheduledDelayMs: 5000,

  // Retry on retryable failures (network errors, 408, 429, 5xx)
  exportRetry: {
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
  },

  // On-disk offline buffer — replayed on next init / foreground / online
  offlineBuffer: {
    enabled: true,
    maxItems: { traces: 5000, metrics: 2000, logs: 5000 },
  },

  // Debug
  debug: false,
});

Background flush

On RN, the SDK calls Scout.flush() when AppState transitions from active to background / inactive. On web, it flushes on visibilitychange = hidden and pagehide. This drains the BatchSpanProcessor, metric reader, and log processor before the OS suspends / kills the process — without it, events emitted in the last few seconds (the ones leading up to a crash) would die with the in-memory batch queue.

If your in-memory exporter still doesn't deliver in time (e.g. the OS kills us mid-POST), the offline buffer persists the batch to disk and replays it on next Scout.initialize(). See Configuration → Offline buffer.


Embedded in a native app? — the WebView bridge

When this page runs inside a native app that also reports RUM (a Flutter app using scout_flutter, say), the two SDKs would otherwise open two unrelated sessions for one user flow. The host fixes that by handing the page its identity:

Scout.setWebViewBridge({
  sessionId: 'native-session-id',
  anonymousId: 'native-anonymous-id',
});

Safe to call before initialize() — the call is parked and applied once the SDK is up, which is what lets a host inject it as the page starts loading.

Three modes, selected by which fields you pass:

ModeFieldsBehaviour
Session adoption (recommended)sessionId, anonymousIdPage keeps exporting to the collector, tagged with the host's session. One copy of every signal, full fidelity.
Relay+ send, relay: truePage stops POSTing spans; the host delivers them. Use when the WebView can't reach the collector. Spans only — logs and metrics still go over HTTP.
Mirror+ sendPage exports and hands the host a copy. Both reach the backend, so expect duplicate spans. Only useful if the host needs to observe web events locally.

Scout.isExportingSpans reports whether the page still owns delivery — worth asserting in a smoke test, since a mis-wired relay is silently lossy.

Prefer session adoption unless the WebView genuinely can't reach the collector; it needs no host-side relay code and loses nothing.


Browser support

Web builds emit ES2020 syntax and are tested down to Chrome / Android System WebView 87 (Android 8.1) and Safari 14. The SDK calls no prototype method newer than that floor — make check-compat fails the build if dist/ does — and gates dependency features behind runtime checks: web-vitals' CLS and INP tracking needs Array.prototype.at (Chrome 92) and is skipped below it, while LCP, FCP and TTFB still report. No polyfills are shipped. An error thrown from inside the SDK bundle is recorded with error.origin: sdk / error.category: sdk_internal, once per distinct failure per page, and does not count towards error.count.


Out of scope (for now)

SignalWhy not
Session Replay (DOM / view-tree recording)Heavyweight payload + privacy implications. Not on the roadmap.
Profiling (continuous CPU sampling)No reliable cross-platform sampler. Not on the roadmap.
Frame raster time (web)Browsers expose only whole-frame durations via Long Animation Frame. We emit web.frame.build_time covering the full frame.
CPU usage (web)No browser API.

Runnable samples

SampleStackHow to run
examples/platform-design-webVite + Reactcd examples/platform-design-web && npm run dev
examples/platform-design-mobileExpo + React Nativecd examples/platform-design-mobile && npx expo run:ios (or run:android). The mobile example bundles the ScoutCrash Expo module — npx expo prebuild is run automatically the first time.

Both default to the local OTel collector on :34318. The repo includes a sample collector config in dev/ (gitignored) — point your collector at it or use your own.


Development

make install            # npm ci
make build              # tsup for web entries, tsc for native (preserves literal require())
make typecheck
make lint
make fmt                # prettier --write
make test               # 90 unit + jsdom integration tests
make test-coverage      # core/** ≥ 75% lines, ≥ 70% functions
make audit              # npm audit prod + all
make ci                 # fmt-check → lint → typecheck → test → build

See CHANGELOG.md for release history.


License

MIT

版本与发布记录

@base-14/scout-react 共发布 17 个版本,首次发布于 2026/05/22;过去 12 个月发布了 17 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.22latest2026/11/052 天前
0.1.212026/10/011 个月前
0.1.202026/09/221 个月前
0.1.192026/09/211 个月前
0.1.172026/09/091 个月前
0.1.162026/08/122 个月前

关于 @base-14/scout-react 的常见问题

@base-14/scout-react 是什么?

Zero-config OpenTelemetry RUM for React and React Native. Auto-captures clicks, navigation, errors, lifecycle, network, performance, and web vitals.

如何安装 @base-14/scout-react?

在项目目录运行 npm install @base-14/scout-react,或使用 pnpm add / yarn add / bun add。

@base-14/scout-react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@base-14/scout-react 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@base-14/scout-react 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@base-14/scout-react 可以商用吗?

@base-14/scout-react 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@base-14/scout-react 的下载量有多少?

根据 npm 官方下载统计,@base-14/scout-react 在 2026/10/08 – 2026/11/06 这 30 天内被下载 2,488 次,日均约 83 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@base-14/scout-react 还在维护吗?

最新版本 0.1.22 发布于 2026/11/05(2 天前);至今共发布 17 个版本,其中过去 12 个月发布了 17 个;首次发布于 2026/05/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。