react-native-otel 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +1137 -13
  2. package/lib/module/context/span-context.js +56 -5
  3. package/lib/module/context/span-context.js.map +1 -1
  4. package/lib/module/core/ids.js +21 -7
  5. package/lib/module/core/ids.js.map +1 -1
  6. package/lib/module/core/meter.js +101 -12
  7. package/lib/module/core/meter.js.map +1 -1
  8. package/lib/module/core/processor.js +28 -0
  9. package/lib/module/core/processor.js.map +1 -0
  10. package/lib/module/core/resource.js +1 -0
  11. package/lib/module/core/resource.js.map +1 -1
  12. package/lib/module/core/sampler.js +55 -0
  13. package/lib/module/core/sampler.js.map +1 -0
  14. package/lib/module/core/span.js +15 -1
  15. package/lib/module/core/span.js.map +1 -1
  16. package/lib/module/core/tracer.js +94 -5
  17. package/lib/module/core/tracer.js.map +1 -1
  18. package/lib/module/exporters/console-exporter.js +8 -1
  19. package/lib/module/exporters/console-exporter.js.map +1 -1
  20. package/lib/module/exporters/multi-exporter.js +57 -0
  21. package/lib/module/exporters/multi-exporter.js.map +1 -0
  22. package/lib/module/exporters/otlp-http-exporter.js +159 -25
  23. package/lib/module/exporters/otlp-http-exporter.js.map +1 -1
  24. package/lib/module/exporters/wal.js +129 -0
  25. package/lib/module/exporters/wal.js.map +1 -0
  26. package/lib/module/index.js +16 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/instrumentation/errors.js +21 -0
  29. package/lib/module/instrumentation/errors.js.map +1 -1
  30. package/lib/module/instrumentation/expo-router.js +76 -0
  31. package/lib/module/instrumentation/expo-router.js.map +1 -0
  32. package/lib/module/instrumentation/fetch.js +99 -0
  33. package/lib/module/instrumentation/fetch.js.map +1 -0
  34. package/lib/module/instrumentation/lifecycle.js +6 -1
  35. package/lib/module/instrumentation/lifecycle.js.map +1 -1
  36. package/lib/module/instrumentation/linking.js +65 -0
  37. package/lib/module/instrumentation/linking.js.map +1 -0
  38. package/lib/module/instrumentation/network.js +35 -0
  39. package/lib/module/instrumentation/network.js.map +1 -1
  40. package/lib/module/instrumentation/startup.js +46 -0
  41. package/lib/module/instrumentation/startup.js.map +1 -0
  42. package/lib/module/sdk.js +87 -5
  43. package/lib/module/sdk.js.map +1 -1
  44. package/lib/module/version.js +7 -0
  45. package/lib/module/version.js.map +1 -0
  46. package/lib/typescript/src/context/span-context.d.ts +14 -4
  47. package/lib/typescript/src/context/span-context.d.ts.map +1 -1
  48. package/lib/typescript/src/core/ids.d.ts.map +1 -1
  49. package/lib/typescript/src/core/meter.d.ts +12 -2
  50. package/lib/typescript/src/core/meter.d.ts.map +1 -1
  51. package/lib/typescript/src/core/processor.d.ts +29 -0
  52. package/lib/typescript/src/core/processor.d.ts.map +1 -0
  53. package/lib/typescript/src/core/resource.d.ts +3 -0
  54. package/lib/typescript/src/core/resource.d.ts.map +1 -1
  55. package/lib/typescript/src/core/sampler.d.ts +31 -0
  56. package/lib/typescript/src/core/sampler.d.ts.map +1 -0
  57. package/lib/typescript/src/core/span.d.ts +16 -0
  58. package/lib/typescript/src/core/span.d.ts.map +1 -1
  59. package/lib/typescript/src/core/tracer.d.ts +16 -6
  60. package/lib/typescript/src/core/tracer.d.ts.map +1 -1
  61. package/lib/typescript/src/exporters/console-exporter.d.ts.map +1 -1
  62. package/lib/typescript/src/exporters/multi-exporter.d.ts +28 -0
  63. package/lib/typescript/src/exporters/multi-exporter.d.ts.map +1 -0
  64. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +20 -2
  65. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -1
  66. package/lib/typescript/src/exporters/types.d.ts +17 -3
  67. package/lib/typescript/src/exporters/types.d.ts.map +1 -1
  68. package/lib/typescript/src/exporters/wal.d.ts +21 -0
  69. package/lib/typescript/src/exporters/wal.d.ts.map +1 -0
  70. package/lib/typescript/src/index.d.ts +15 -2
  71. package/lib/typescript/src/index.d.ts.map +1 -1
  72. package/lib/typescript/src/instrumentation/errors.d.ts.map +1 -1
  73. package/lib/typescript/src/instrumentation/expo-router.d.ts +31 -0
  74. package/lib/typescript/src/instrumentation/expo-router.d.ts.map +1 -0
  75. package/lib/typescript/src/instrumentation/fetch.d.ts +18 -0
  76. package/lib/typescript/src/instrumentation/fetch.d.ts.map +1 -0
  77. package/lib/typescript/src/instrumentation/lifecycle.d.ts.map +1 -1
  78. package/lib/typescript/src/instrumentation/linking.d.ts +23 -0
  79. package/lib/typescript/src/instrumentation/linking.d.ts.map +1 -0
  80. package/lib/typescript/src/instrumentation/network.d.ts.map +1 -1
  81. package/lib/typescript/src/instrumentation/startup.d.ts +16 -0
  82. package/lib/typescript/src/instrumentation/startup.d.ts.map +1 -0
  83. package/lib/typescript/src/sdk.d.ts +35 -0
  84. package/lib/typescript/src/sdk.d.ts.map +1 -1
  85. package/lib/typescript/src/version.d.ts +2 -0
  86. package/lib/typescript/src/version.d.ts.map +1 -0
  87. package/package.json +12 -2
  88. package/src/context/span-context.ts +61 -8
  89. package/src/core/ids.ts +21 -7
  90. package/src/core/meter.ts +136 -14
  91. package/src/core/processor.ts +33 -0
  92. package/src/core/resource.ts +6 -0
  93. package/src/core/sampler.ts +65 -0
  94. package/src/core/span.ts +28 -1
  95. package/src/core/tracer.ts +140 -19
  96. package/src/exporters/console-exporter.ts +18 -4
  97. package/src/exporters/multi-exporter.ts +59 -0
  98. package/src/exporters/otlp-http-exporter.ts +191 -29
  99. package/src/exporters/types.ts +24 -3
  100. package/src/exporters/wal.ts +145 -0
  101. package/src/index.ts +36 -1
  102. package/src/instrumentation/errors.ts +27 -0
  103. package/src/instrumentation/expo-router.ts +94 -0
  104. package/src/instrumentation/fetch.ts +134 -0
  105. package/src/instrumentation/lifecycle.ts +7 -1
  106. package/src/instrumentation/linking.ts +83 -0
  107. package/src/instrumentation/network.ts +39 -0
  108. package/src/instrumentation/startup.ts +49 -0
  109. package/src/sdk.ts +115 -4
  110. package/src/version.ts +6 -0
package/README.md CHANGED
@@ -1,37 +1,1161 @@
1
1
  # react-native-otel
2
2
 
3
- Lightweight OpenTelemetry SDK for React Native
3
+ Lightweight [OpenTelemetry](https://opentelemetry.io/) SDK for React Native. Zero native dependencies — works in Expo managed workflow, bare React Native, and any Hermes-powered app.
4
4
 
5
- ## Installation
5
+ [![npm version](https://img.shields.io/npm/v/react-native-otel)](https://www.npmjs.com/package/react-native-otel)
6
+ [![license](https://img.shields.io/npm/l/react-native-otel)](LICENSE)
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ - [Features](#features)
13
+ - [Installation](#installation)
14
+ - [Quick Start](#quick-start)
15
+ - [SDK Configuration](#sdk-configuration)
16
+ - [Instrumentation](#instrumentation)
17
+ - [Navigation (React Navigation)](#navigation-react-navigation)
18
+ - [Network (Axios)](#network-axios)
19
+ - [Network (fetch)](#network-fetch)
20
+ - [App Startup](#app-startup)
21
+ - [Deep Links & Push Notifications](#deep-links--push-notifications)
22
+ - [Expo Router](#expo-router)
23
+ - [Error & Crash Reporting](#error--crash-reporting)
24
+ - [Tracing](#tracing)
25
+ - [startSpan](#startspan)
26
+ - [startActiveSpan](#startactivespan)
27
+ - [withSpan](#withspan)
28
+ - [Span Links](#span-links)
29
+ - [recordEvent](#recordevent)
30
+ - [recordException](#recordexception)
31
+ - [Sampling](#sampling)
32
+ - [Span Processors](#span-processors)
33
+ - [Metrics](#metrics)
34
+ - [Counter](#counter)
35
+ - [Histogram](#histogram)
36
+ - [Gauge](#gauge)
37
+ - [Logging](#logging)
38
+ - [Exporters](#exporters)
39
+ - [OtlpHttpExporter (Spans)](#otlphttpexporter-spans)
40
+ - [OtlpHttpMetricExporter (Metrics)](#otlphttpmetricexporter-metrics)
41
+ - [OtlpHttpLogExporter (Logs)](#otlphttplogexporter-logs)
42
+ - [Multi-Exporter (Fan-out)](#multi-exporter-fan-out)
43
+ - [Console Exporters (Development)](#console-exporters-development)
44
+ - [Custom Exporters](#custom-exporters)
45
+ - [React Integration](#react-integration)
46
+ - [OtelProvider](#otelprovider)
47
+ - [useOtel](#useotel)
48
+ - [Persistence & Crash Recovery](#persistence--crash-recovery)
49
+ - [Connectivity-Aware Flushing](#connectivity-aware-flushing)
50
+ - [User Identification](#user-identification)
51
+ - [Flush & Shutdown](#flush--shutdown)
52
+ - [TypeScript](#typescript)
53
+ - [Limitations](#limitations)
54
+ - [Contributing](#contributing)
55
+
56
+ ---
57
+
58
+ ## Features
59
+
60
+ - **Distributed tracing** — W3C `traceparent`, `tracestate`, and `baggage` header injection; parent/child span linking across screens and network requests
61
+ - **Span links** — link a span to spans in other traces (batch jobs, fan-in workflows)
62
+ - **Sampling** — pluggable `Sampler` interface with `AlwaysOn`, `AlwaysOff`, and `TraceIdRatio` built-ins
63
+ - **Span processors** — `SpanProcessor` pipeline for custom enrichment or filtering before export
64
+ - **Metrics** — Counter, Histogram (explicit bucket boundaries), and Gauge, exported as real OTLP data
65
+ - **Structured logging** — TRACE / DEBUG / INFO / WARN / ERROR / FATAL with automatic trace/span correlation
66
+ - **Navigation instrumentation** — automatic screen-level span lifecycle for React Navigation
67
+ - **Network instrumentation** — Axios interceptors and global `fetch` patching with W3C context propagation and sensitive field redaction
68
+ - **App startup span** — cold-start duration from module-load time to first render
69
+ - **Deep link & push notification spans** — `Linking` adapter + manual push-notification recording
70
+ - **Expo Router support** — optional hook adapter for file-based navigation (peer dep)
71
+ - **App lifecycle metrics** — automatic `app.foreground_count` and `app.background_count` counters
72
+ - **Error & crash instrumentation** — JS fatal errors, non-fatal exceptions, and unhandled Promise rejections
73
+ - **Multi-exporter fan-out** — send the same telemetry to multiple backends simultaneously
74
+ - **Persistence & retry** — Write-Ahead Log (WAL) for spans, metrics, and logs; jitter + exponential backoff; circuit breaker after 5 consecutive failures
75
+ - **Connectivity-aware flushing** — plug in any `NetInfo`-compatible adapter to pause delivery while offline
76
+ - **Auto platform detection** — `Platform.OS` / `Platform.Version` used as default `osName` / `osVersion`
77
+ - **Custom resource attributes** — merge arbitrary key/value pairs into the OTLP resource
78
+ - **Cryptographic IDs** — 128-bit trace IDs and 64-bit span IDs via `crypto.getRandomValues()`
79
+ - **React integration** — `OtelProvider`, `useOtel` hook, optional error boundary
80
+ - **Zero native code** — pure TypeScript, no linking required
6
81
 
82
+ ---
83
+
84
+ ## Installation
7
85
 
8
86
  ```sh
87
+ # npm
9
88
  npm install react-native-otel
89
+
90
+ # yarn
91
+ yarn add react-native-otel
92
+ ```
93
+
94
+ **Peer dependencies** (already in your project):
95
+
96
+ ```sh
97
+ react
98
+ react-native
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Quick Start
104
+
105
+ ```ts
106
+ // app/_layout.tsx (or App.tsx)
107
+ import { otel, OtlpHttpExporter, OtlpHttpMetricExporter } from 'react-native-otel';
108
+
109
+ otel.init({
110
+ serviceName: 'my-app',
111
+ serviceVersion: '1.0.0',
112
+ environment: 'production',
113
+ exporter: new OtlpHttpExporter({
114
+ endpoint: 'https://your-otel-collector',
115
+ headers: { authorization: 'Bearer YOUR_API_KEY' },
116
+ }),
117
+ metricExporter: new OtlpHttpMetricExporter({
118
+ endpoint: 'https://your-otel-collector',
119
+ headers: { authorization: 'Bearer YOUR_API_KEY' },
120
+ }),
121
+ });
122
+ ```
123
+
124
+ `osName` and `osVersion` are auto-detected from `Platform.OS` / `Platform.Version` when omitted. Navigation, network, and error instrumentation are wired up separately (see below) for full control over what gets traced.
125
+
126
+ ---
127
+
128
+ ## SDK Configuration
129
+
130
+ Pass an `OtelConfig` object to `otel.init()`. Call it once, as early as possible in your app entry point. Subsequent calls are no-ops.
131
+
132
+ ```ts
133
+ otel.init({
134
+ // ─── Required ───────────────────────────────────────────────
135
+ serviceName: 'my-app',
136
+
137
+ // ─── Service metadata ────────────────────────────────────────
138
+ serviceVersion: '1.2.3', // Default: '0.0.0'
139
+ environment: 'production', // Default: 'production'
140
+ appBuild: '42',
141
+
142
+ // ─── Device metadata ─────────────────────────────────────────
143
+ // osName and osVersion are auto-detected from Platform when omitted.
144
+ osName: 'ios', // Default: Platform.OS
145
+ osVersion: '17.4', // Default: String(Platform.Version)
146
+ deviceBrand: 'Apple',
147
+ deviceModel: 'iPhone 15 Pro',
148
+ deviceType: 'handset',
149
+
150
+ // ─── Extra resource attributes ───────────────────────────────
151
+ // Merged into the OTLP resource alongside the standard fields above.
152
+ resourceAttributes: {
153
+ 'team': 'mobile',
154
+ 'region': 'us-east-1',
155
+ },
156
+
157
+ // ─── Exporters ───────────────────────────────────────────────
158
+ exporter: new OtlpHttpExporter({ endpoint: '...' }),
159
+ metricExporter: new OtlpHttpMetricExporter({ endpoint: '...' }),
160
+ logExporter: new OtlpHttpLogExporter({ endpoint: '...' }),
161
+
162
+ // ─── Sampling ────────────────────────────────────────────────
163
+ // Legacy: 1.0 = 100%, 0.1 = 10%. Ignored when sampler is set.
164
+ sampleRate: 1.0,
165
+ // Pluggable sampler (takes precedence over sampleRate when set).
166
+ sampler: new TraceIdRatioSampler(0.25), // sample 25% of traces
167
+
168
+ // ─── Span processors ─────────────────────────────────────────
169
+ // Custom processors run on every span before it is exported.
170
+ processors: [new SimpleSpanProcessor(myExporter)],
171
+
172
+ // ─── Connectivity ─────────────────────────────────────────────
173
+ // Pause flushing when offline (no native dep required).
174
+ networkAdapter: {
175
+ addListener(cb) {
176
+ const unsub = NetInfo.addEventListener(s => cb(!!s.isConnected));
177
+ return unsub;
178
+ },
179
+ },
180
+
181
+ // ─── Attributes ──────────────────────────────────────────────
182
+ maxAttributeStringLength: 1024,
183
+
184
+ // ─── Network redaction ───────────────────────────────────────
185
+ sensitiveKeys: [
186
+ 'header.authorization',
187
+ 'header.cookie',
188
+ 'body.password',
189
+ 'response.token',
190
+ ],
191
+
192
+ // ─── Persistence ─────────────────────────────────────────────
193
+ storage: {
194
+ setSync: (key, value) => MMKVStorage.set(key, value),
195
+ getSync: (key) => MMKVStorage.getString(key) ?? null,
196
+ deleteSync: (key) => MMKVStorage.delete(key),
197
+ },
198
+ });
199
+ ```
200
+
201
+ ### OtelConfig reference
202
+
203
+ | Property | Type | Default | Description |
204
+ |---|---|---|---|
205
+ | `serviceName` | `string` | — | **Required.** Identifies your service in all telemetry. |
206
+ | `serviceVersion` | `string` | `'0.0.0'` | Service version string. |
207
+ | `environment` | `string` | `'production'` | Deployment environment. |
208
+ | `appBuild` | `string` | `''` | Build number or commit SHA. |
209
+ | `osName` | `string` | `Platform.OS` | Operating system name. Auto-detected when omitted. |
210
+ | `osVersion` | `string` | `Platform.Version` | OS version string. Auto-detected when omitted. |
211
+ | `deviceBrand` | `string` | `''` | Device manufacturer. |
212
+ | `deviceModel` | `string` | `''` | Device model name. |
213
+ | `deviceType` | `string \| number` | `''` | Device form factor. |
214
+ | `resourceAttributes` | `Attributes` | `undefined` | Extra key/value pairs merged into the OTLP resource. |
215
+ | `exporter` | `SpanExporter` | `undefined` | Span destination. Omit to discard spans. |
216
+ | `metricExporter` | `MetricExporter` | `undefined` | Metric destination. |
217
+ | `logExporter` | `LogExporter` | `undefined` | Log destination. |
218
+ | `sampleRate` | `number` | `1.0` | Fraction of traces to capture (0–1). Ignored when `sampler` is set. |
219
+ | `sampler` | `Sampler` | `undefined` | Pluggable sampler. Takes precedence over `sampleRate`. |
220
+ | `processors` | `SpanProcessor[]` | `[]` | Span processor pipeline. |
221
+ | `networkAdapter` | `NetworkAdapter` | `undefined` | Connectivity adapter for pause-on-offline flushing. |
222
+ | `maxAttributeStringLength` | `number` | `1024` | Truncate attribute strings longer than this. |
223
+ | `sensitiveKeys` | `string[]` | `[]` | Dot-notation paths to redact from network captures. |
224
+ | `storage` | `StorageAdapter` | `undefined` | Synchronous key/value store for WAL and crash persistence. |
225
+
226
+ ---
227
+
228
+ ## Instrumentation
229
+
230
+ ### Navigation (React Navigation)
231
+
232
+ Wire up screen-level spans by connecting the instrumentation to your navigation state change handler. Each screen navigation starts a new root span and ends the previous one.
233
+
234
+ ```tsx
235
+ import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
236
+ import { otel, createNavigationInstrumentation } from 'react-native-otel';
237
+
238
+ const navigationRef = createNavigationContainerRef();
239
+ const navInstrumentation = createNavigationInstrumentation(otel.getTracer());
240
+
241
+ export default function App() {
242
+ return (
243
+ <NavigationContainer
244
+ ref={navigationRef}
245
+ onReady={() => {
246
+ const route = navigationRef.getCurrentRoute();
247
+ if (route) {
248
+ navInstrumentation.onRouteChange(
249
+ route.name, undefined, route.key, undefined, route.params as Record<string, unknown>
250
+ );
251
+ }
252
+ }}
253
+ onStateChange={() => {
254
+ const current = navigationRef.getCurrentRoute();
255
+ const previous = navigationRef.getPreviousRoute?.();
256
+ if (current) {
257
+ navInstrumentation.onRouteChange(
258
+ current.name, previous?.name,
259
+ current.key, previous?.key,
260
+ current.params as Record<string, unknown>
261
+ );
262
+ }
263
+ }}
264
+ >
265
+ {/* ... */}
266
+ </NavigationContainer>
267
+ );
268
+ }
269
+ ```
270
+
271
+ #### NavigationInstrumentation API
272
+
273
+ | Method | Signature | Description |
274
+ |---|---|---|
275
+ | `onRouteChange` | `(currentName, previousName, currentKey, previousKey, params?) => void` | Call on every navigation state change. Ends the previous screen span and starts a new one. |
276
+ | `endCurrentScreen` | `() => void` | Manually end the active screen span. |
277
+
278
+ ---
279
+
280
+ ### Network (Axios)
281
+
282
+ Creates Axios interceptors that wrap each HTTP request in a CLIENT span and inject W3C `traceparent`, `tracestate`, and `baggage` headers.
283
+
284
+ ```ts
285
+ import axios from 'axios';
286
+ import { otel, createAxiosInstrumentation } from 'react-native-otel';
287
+
288
+ const axiosInstrumentation = createAxiosInstrumentation(otel.getTracer(), {
289
+ sensitiveKeys: otel.getSensitiveKeys(),
290
+ });
291
+
292
+ const api = axios.create({ baseURL: 'https://api.example.com' });
293
+ api.interceptors.request.use(axiosInstrumentation.onRequest);
294
+ api.interceptors.response.use(
295
+ axiosInstrumentation.onResponse,
296
+ axiosInstrumentation.onError
297
+ );
298
+ ```
299
+
300
+ The following W3C headers are injected automatically on every sampled request:
301
+
302
+ | Header | Purpose |
303
+ |---|---|
304
+ | `traceparent` | Continues the trace in your backend (`00-{traceId}-{spanId}-01`) |
305
+ | `tracestate` | Forwarded from the active span's `tracestate` attribute when present |
306
+ | `baggage` | Built from any span attributes prefixed with `baggage.` |
307
+
308
+ ---
309
+
310
+ ### Network (fetch)
311
+
312
+ `otel.init()` automatically patches `globalThis.fetch` to create a CLIENT span for every HTTP request. No extra setup is required.
313
+
314
+ The OTLP exporter's own delivery calls are immune — the SDK snapshots the original `fetch` before installing the instrumentation, so there is no infinite recursion.
315
+
316
+ If you need to opt out:
317
+
318
+ ```ts
319
+ import { uninstallFetchInstrumentation } from 'react-native-otel';
320
+
321
+ uninstallFetchInstrumentation(); // restores the original fetch
322
+ ```
323
+
324
+ ---
325
+
326
+ ### App Startup
327
+
328
+ Records a single `app.startup` span whose duration covers the period from module-load time to the point when this function is called. Call it once, immediately after `otel.init()`, before rendering the first screen.
329
+
330
+ ```ts
331
+ import { otel, installStartupInstrumentation } from 'react-native-otel';
332
+
333
+ otel.init({ serviceName: 'my-app', ... });
334
+ installStartupInstrumentation(otel.getTracer());
10
335
  ```
11
336
 
337
+ The span carries:
338
+
339
+ | Attribute | Description |
340
+ |---|---|
341
+ | `app.startup.module_load_ms` | Timestamp when the JS module was loaded |
342
+ | `app.startup.sdk_init_ms` | Timestamp when `otel.init()` completed |
12
343
 
13
- ## Usage
344
+ Import `react-native-otel` as early as possible in your entry file to maximise the accuracy of `module_load_ms`.
14
345
 
346
+ ---
15
347
 
16
- ```js
17
- import { multiply } from 'react-native-otel';
348
+ ### Deep Links & Push Notifications
18
349
 
19
- // ...
350
+ ```ts
351
+ import { otel, createLinkingInstrumentation, recordPushNotification } from 'react-native-otel';
20
352
 
21
- const result = await multiply(3, 7);
353
+ // Creates an app.deep_link span for every incoming URL.
354
+ // Also checks getInitialURL() for links that launched the app.
355
+ const linking = createLinkingInstrumentation(otel.getTracer());
356
+
357
+ // Remove the listener when shutting down.
358
+ linking.uninstall();
359
+
360
+ // Record a push notification payload as a standalone span.
361
+ recordPushNotification(otel.getTracer(), {
362
+ title: 'New message',
363
+ 'notification.id': 'n_123',
364
+ });
22
365
  ```
23
366
 
367
+ ---
368
+
369
+ ### Expo Router
370
+
371
+ An optional hook adapter for apps using [Expo Router](https://expo.github.io/router/). It requires `expo-router` as a peer dependency and is published under a dedicated sub-path export to keep it out of the main bundle for apps that don't use Expo Router.
372
+
373
+ ```tsx
374
+ // app/_layout.tsx
375
+ import { useExpoRouterInstrumentation } from 'react-native-otel/expo-router';
376
+ import { otel } from 'react-native-otel';
377
+
378
+ export default function RootLayout() {
379
+ useExpoRouterInstrumentation(otel.getTracer());
380
+ return <Slot />;
381
+ }
382
+ ```
383
+
384
+ On every route change the hook ends the previous screen span and starts a new `screen.{pathname}` span with `screen.name` and `screen.segments` attributes.
385
+
386
+ ---
387
+
388
+ ### Error & Crash Reporting
389
+
390
+ Automatically installed by `otel.init()`. No additional setup required.
391
+
392
+ | Signal | Span name | Key attributes |
393
+ |---|---|---|
394
+ | Fatal JS error | `crash.{Error.name}` | `exception.type`, `exception.message`, `exception.stacktrace`, `crash.is_fatal: true` |
395
+ | Non-fatal JS error | `crash.{Error.name}` | same, `crash.is_fatal: false` |
396
+ | Unhandled Promise rejection | `unhandled_rejection.{Error.name}` | `exception.type`, `exception.message`, `exception.stacktrace`, `exception.unhandled_rejection: true` |
397
+
398
+ **Crash persistence:** Fatal error spans are written synchronously to the `StorageAdapter` before the process terminates and exported on the next app launch.
399
+
400
+ ---
401
+
402
+ ## Tracing
403
+
404
+ Access the tracer via `otel.getTracer()` or the `useOtel()` hook.
405
+
406
+ ### startSpan
407
+
408
+ Creates a span without making it the active context.
409
+
410
+ ```ts
411
+ const tracer = otel.getTracer();
412
+
413
+ const span = tracer.startSpan('checkout.process', {
414
+ kind: 'INTERNAL',
415
+ attributes: { 'order.id': orderId, 'order.total': total },
416
+ // parent: pass a SpanContext, or null to force a new root trace.
417
+ // Omit to inherit the current active span automatically.
418
+ });
419
+
420
+ try {
421
+ await processOrder(orderId);
422
+ span.setStatus('OK');
423
+ } catch (err) {
424
+ span.recordException(err as Error);
425
+ span.setStatus('ERROR', (err as Error).message);
426
+ } finally {
427
+ span.end();
428
+ }
429
+ ```
430
+
431
+ #### Span API
432
+
433
+ | Method | Signature | Description |
434
+ |---|---|---|
435
+ | `setAttribute` | `(key: string, value: AttributeValue) => void` | Set a single attribute. No-op after `end()`. |
436
+ | `addEvent` | `(name: string, attrs?: Attributes) => void` | Add a timed event. Capped at 128; excess are dropped and counted. |
437
+ | `recordException` | `(error: Error, attrs?: Attributes) => void` | Attach exception details as a span event and set status ERROR. |
438
+ | `setStatus` | `(status: SpanStatus, message?: string) => void` | Set the span outcome. |
439
+ | `end` | `() => void` | Finalize and export the span. Idempotent. |
440
+
441
+ ---
442
+
443
+ ### startActiveSpan
444
+
445
+ Creates a span and makes it the active context for the duration of the callback.
446
+
447
+ ```ts
448
+ // Synchronous
449
+ tracer.startActiveSpan('render.catalog', (span) => {
450
+ span.setAttribute('item.count', items.length);
451
+ renderItems(items);
452
+ });
453
+
454
+ // Async
455
+ await tracer.startActiveSpan('fetch.user', async (span) => {
456
+ const user = await api.get('/me'); // network span parents automatically
457
+ span.setAttribute('user.id', user.id);
458
+ });
459
+
460
+ // With options
461
+ await tracer.startActiveSpan(
462
+ 'payment.authorize',
463
+ { kind: 'CLIENT', attributes: { 'payment.provider': 'stripe' } },
464
+ async (span) => { await stripe.confirmPayment(intent); }
465
+ );
466
+ ```
467
+
468
+ > **Concurrency note:** `startActiveSpan` uses a shared context stack. For concurrent `Promise.all`-style work, use `startSpan` with explicit parents instead.
469
+
470
+ ---
471
+
472
+ ### withSpan
473
+
474
+ Makes an existing span the active context without ending it.
475
+
476
+ ```ts
477
+ const screenSpan = tracer.startSpan('screen.Dashboard');
478
+
479
+ tracer.withSpan(screenSpan, () => {
480
+ tracer.startActiveSpan('load.widgets', async (span) => {
481
+ await loadWidgets();
482
+ });
483
+ });
484
+
485
+ screenSpan.end();
486
+ ```
487
+
488
+ ---
489
+
490
+ ### Span Links
491
+
492
+ Link a span to one or more spans in other (or the same) traces. Useful for batch processing, fan-in workflows, and message queues.
493
+
494
+ ```ts
495
+ import type { SpanLink } from 'react-native-otel';
496
+
497
+ const links: SpanLink[] = [
498
+ {
499
+ traceId: upstreamSpan.traceId,
500
+ spanId: upstreamSpan.spanId,
501
+ attributes: { 'link.reason': 'triggered_by' },
502
+ },
503
+ ];
504
+
505
+ const span = tracer.startSpan('batch.process', { links });
506
+ // Links are serialized in OTLP as the `links` array on the span.
507
+ span.end();
508
+ ```
509
+
510
+ ---
511
+
512
+ ### recordEvent
513
+
514
+ Records a named event on the currently active span.
515
+
516
+ ```ts
517
+ otel.recordEvent('button.tapped', { button: 'checkout', screen: 'Cart' });
518
+ tracer.recordEvent('video.paused', { position_ms: 32500 });
519
+ ```
520
+
521
+ ---
522
+
523
+ ### recordException
524
+
525
+ Records an error as a child span of the current active span.
526
+
527
+ ```ts
528
+ try {
529
+ await riskyOperation();
530
+ } catch (err) {
531
+ tracer.recordException(err as Error, { component: 'PaymentForm' });
532
+ }
533
+ ```
534
+
535
+ ---
536
+
537
+ ## Sampling
538
+
539
+ The SDK ships three built-in samplers and accepts any custom implementation of the `Sampler` interface.
540
+
541
+ ```ts
542
+ import {
543
+ AlwaysOnSampler,
544
+ AlwaysOffSampler,
545
+ TraceIdRatioSampler,
546
+ } from 'react-native-otel';
547
+
548
+ otel.init({
549
+ serviceName: 'my-app',
550
+ // Sample 10% of traces deterministically by trace ID:
551
+ sampler: new TraceIdRatioSampler(0.1),
552
+ });
553
+ ```
554
+
555
+ | Sampler | Behaviour |
556
+ |---|---|
557
+ | `AlwaysOnSampler` | Records every span (default when no sampler is set). |
558
+ | `AlwaysOffSampler` | Drops every span. Useful to disable tracing in specific environments. |
559
+ | `TraceIdRatioSampler(ratio)` | Samples a deterministic fraction of traces (0–1) using the trace ID. |
560
+
561
+ `TraceIdRatioSampler` makes sampling decisions based on the first 8 bytes of the trace ID, matching the W3C spec intent. Root spans (no parent) fall back to a random decision at the same ratio.
562
+
563
+ **Custom sampler:**
564
+
565
+ ```ts
566
+ import type { Sampler } from 'react-native-otel';
567
+ import type { SpanContext, Attributes } from 'react-native-otel';
568
+
569
+ class MySampler implements Sampler {
570
+ shouldSample(name: string, parent?: SpanContext, attributes?: Attributes): boolean {
571
+ // Drop health-check spans.
572
+ return name !== 'health.check';
573
+ }
574
+ }
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Span Processors
580
+
581
+ Span processors run synchronously at span start and end. Use them to enrich spans with extra attributes, filter spans, or forward to a custom exporter without going through the SDK exporter chain.
582
+
583
+ ```ts
584
+ import {
585
+ SimpleSpanProcessor,
586
+ NoopSpanProcessor,
587
+ } from 'react-native-otel';
588
+ import type { SpanProcessor, ReadonlySpan } from 'react-native-otel';
589
+ import type { Span } from 'react-native-otel';
590
+
591
+ // SimpleSpanProcessor: wraps an exporter — calls export() immediately on end().
592
+ otel.init({
593
+ serviceName: 'my-app',
594
+ processors: [new SimpleSpanProcessor(myCustomExporter)],
595
+ });
596
+
597
+ // Custom processor: enrich every span with a 'session.id' attribute.
598
+ class SessionProcessor implements SpanProcessor {
599
+ onStart(span: Span): void {
600
+ span.setAttribute('session.id', currentSessionId());
601
+ }
602
+ onEnd(_span: ReadonlySpan): void {}
603
+ }
604
+
605
+ otel.init({
606
+ serviceName: 'my-app',
607
+ exporter: new OtlpHttpExporter({ endpoint: '...' }),
608
+ processors: [new SessionProcessor()],
609
+ });
610
+ ```
611
+
612
+ When `processors` is set, each span's `end()` calls the processors in order instead of calling the exporter directly. To both enrich and export, use a `SimpleSpanProcessor` wrapping your exporter as the last processor in the array.
613
+
614
+ ---
615
+
616
+ ## Metrics
617
+
618
+ Access the meter via `otel.getMeter()` or the `useOtel()` hook.
619
+
620
+ ### Counter
621
+
622
+ A monotonically increasing value. Exported as an OTLP cumulative sum.
623
+
624
+ ```ts
625
+ const meter = otel.getMeter();
626
+ const apiCallCounter = meter.createCounter('api.calls');
627
+
628
+ apiCallCounter.add(1);
629
+ apiCallCounter.add(1, { endpoint: '/checkout', status: '200' });
630
+ ```
631
+
632
+ ---
633
+
634
+ ### Histogram
635
+
636
+ Records a distribution of values. Aggregates per unique attribute set and exports as real OTLP explicit bucket histogram data. Default boundaries cover typical mobile latencies in milliseconds.
637
+
638
+ ```ts
639
+ const requestDuration = meter.createHistogram('http.client.duration', {
640
+ // Default: [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000]
641
+ boundaries: [0, 10, 50, 100, 500, 1000, 5000],
642
+ });
643
+
644
+ const start = Date.now();
645
+ await api.get('/products');
646
+ requestDuration.record(Date.now() - start, { endpoint: '/products' });
647
+ ```
648
+
649
+ Histograms flush via `meter.flush()` which is called automatically on app background, `otel.flush()`, and `otel.shutdown()`. Each flush window uses DELTA temporality — buckets are cleared after each flush.
650
+
651
+ ---
652
+
653
+ ### Gauge
654
+
655
+ Records an instantaneous value (last-write-wins).
656
+
657
+ ```ts
658
+ const memoryGauge = meter.createGauge('app.memory.used_mb');
659
+
660
+ setInterval(() => {
661
+ memoryGauge.set(getCurrentMemoryUsageMB(), { unit: 'mb' });
662
+ }, 10_000);
663
+ ```
664
+
665
+ ---
666
+
667
+ ### Built-in lifecycle metrics
668
+
669
+ `otel.init()` automatically installs two counters that track app lifecycle transitions:
670
+
671
+ | Metric | Description |
672
+ |---|---|
673
+ | `app.foreground_count` | Incremented each time the app moves to the foreground (`active` state). |
674
+ | `app.background_count` | Incremented each time the app moves to the background. Also triggers a metric flush. |
675
+
676
+ ---
677
+
678
+ ## Logging
679
+
680
+ Access the logger via `otel.getLogger()` or the `useOtel()` hook.
681
+
682
+ ```ts
683
+ const logger = otel.getLogger();
684
+
685
+ logger.trace('Entering render cycle', { component: 'ProductList' });
686
+ logger.debug('Cache hit', { key: 'products:page:1' });
687
+ logger.info('User signed in', { 'user.id': userId });
688
+ logger.warn('API rate limit approaching', { remaining: 10 });
689
+ logger.error('Payment failed', { code: 'CARD_DECLINED' });
690
+ logger.fatal('Out of memory — terminating');
691
+ ```
692
+
693
+ All log records include `severity`, `body`, `traceId`, `spanId`, `attributes`, and `timestampMs`.
694
+
695
+ ---
696
+
697
+ ## Exporters
698
+
699
+ ### OtlpHttpExporter (Spans)
700
+
701
+ Sends spans to any OTLP/HTTP-compatible backend. `/v1/traces` is appended to the endpoint automatically.
702
+
703
+ ```ts
704
+ import { OtlpHttpExporter } from 'react-native-otel';
705
+
706
+ new OtlpHttpExporter({
707
+ endpoint: 'https://in-otel.hyperdx.io',
708
+ headers: { authorization: 'Bearer YOUR_API_KEY' },
709
+ batchSize: 50, // Flush when buffer hits this size. Default: 50.
710
+ flushIntervalMs: 30_000, // Auto-flush interval in ms. Default: 30 s.
711
+ })
712
+ ```
713
+
714
+ ---
715
+
716
+ ### OtlpHttpMetricExporter (Metrics)
717
+
718
+ Sends metrics to any OTLP/HTTP-compatible backend. `/v1/metrics` is appended automatically. Metrics are buffered and flushed on the interval or when `flush()` / `destroy()` is called.
719
+
720
+ ```ts
721
+ import { OtlpHttpMetricExporter } from 'react-native-otel';
722
+
723
+ new OtlpHttpMetricExporter({
724
+ endpoint: 'https://your-collector',
725
+ headers: { authorization: 'Bearer YOUR_API_KEY' },
726
+ flushIntervalMs: 30_000, // Default: 30 s.
727
+ })
728
+ ```
729
+
730
+ Exported metric types:
731
+ - **Counter** → OTLP `sum` (cumulative, monotonic)
732
+ - **Histogram** → OTLP `histogram` (explicit buckets, delta temporality)
733
+ - **Gauge** → OTLP `gauge`
734
+
735
+ ---
736
+
737
+ ### OtlpHttpLogExporter (Logs)
738
+
739
+ Sends logs to any OTLP/HTTP-compatible backend. `/v1/logs` is appended automatically. Supports WAL persistence (pass `storage` in `otel.init()`).
740
+
741
+ ```ts
742
+ import { OtlpHttpLogExporter } from 'react-native-otel';
743
+
744
+ new OtlpHttpLogExporter({
745
+ endpoint: 'https://your-collector',
746
+ headers: { authorization: 'Bearer YOUR_API_KEY' },
747
+ batchSize: 50, // Default: 50
748
+ flushIntervalMs: 30_000, // Default: 30 s
749
+ })
750
+ ```
751
+
752
+ ---
753
+
754
+ ### Multi-Exporter (Fan-out)
755
+
756
+ Send the same signal to multiple backends simultaneously. Each exporter is called independently — a failure in one does not affect the others.
757
+
758
+ ```ts
759
+ import {
760
+ MultiSpanExporter,
761
+ MultiMetricExporter,
762
+ MultiLogExporter,
763
+ OtlpHttpExporter,
764
+ ConsoleSpanExporter,
765
+ } from 'react-native-otel';
766
+
767
+ otel.init({
768
+ serviceName: 'my-app',
769
+ exporter: new MultiSpanExporter([
770
+ new OtlpHttpExporter({ endpoint: 'https://grafana-cloud...' }),
771
+ new OtlpHttpExporter({ endpoint: 'https://hyperdx...' }),
772
+ new ConsoleSpanExporter(), // also log to console in dev
773
+ ]),
774
+ });
775
+ ```
776
+
777
+ ---
778
+
779
+ ### Console Exporters (Development)
780
+
781
+ Pretty-print telemetry to the React Native console.
782
+
783
+ ```ts
784
+ import {
785
+ ConsoleSpanExporter,
786
+ ConsoleMetricExporter,
787
+ ConsoleLogExporter,
788
+ } from 'react-native-otel';
789
+
790
+ otel.init({
791
+ serviceName: 'my-app',
792
+ exporter: new ConsoleSpanExporter(),
793
+ metricExporter: new ConsoleMetricExporter(),
794
+ logExporter: new ConsoleLogExporter(),
795
+ });
796
+ ```
797
+
798
+ ---
799
+
800
+ ### Custom Exporters
801
+
802
+ ```ts
803
+ import type {
804
+ SpanExporter, MetricExporter, LogExporter,
805
+ ReadonlySpan, MetricRecord, LogRecord,
806
+ } from 'react-native-otel';
807
+
808
+ class MySpanExporter implements SpanExporter {
809
+ export(spans: ReadonlySpan[]): void {
810
+ for (const span of spans) {
811
+ sendToMyBackend(span);
812
+ }
813
+ }
814
+ }
815
+ ```
816
+
817
+ ---
818
+
819
+ ## React Integration
820
+
821
+ ### OtelProvider
822
+
823
+ ```tsx
824
+ import { OtelProvider } from 'react-native-otel';
825
+
826
+ export default function App() {
827
+ return (
828
+ <OtelProvider withErrorBoundary>
829
+ <RootNavigator />
830
+ </OtelProvider>
831
+ );
832
+ }
833
+ ```
834
+
835
+ `withErrorBoundary` wraps children in a React error boundary that calls `tracer.recordException()` on render errors.
836
+
837
+ ---
838
+
839
+ ### useOtel
840
+
841
+ ```tsx
842
+ import { useOtel } from 'react-native-otel';
843
+
844
+ function CheckoutButton() {
845
+ const { tracer, meter, logger, recordEvent, setUser } = useOtel();
846
+ const checkoutCounter = meter.createCounter('checkout.attempts');
847
+
848
+ const handlePress = async () => {
849
+ recordEvent('checkout.button.tapped');
850
+ checkoutCounter.add(1, { source: 'cart_screen' });
851
+
852
+ await tracer.startActiveSpan('checkout.submit', async (span) => {
853
+ try {
854
+ const order = await api.post('/orders', cartItems);
855
+ span.setAttribute('order.id', order.id);
856
+ logger.info('Order placed', { 'order.id': order.id });
857
+ } catch (err) {
858
+ logger.error('Checkout failed', { reason: (err as Error).message });
859
+ throw err;
860
+ }
861
+ });
862
+ };
863
+
864
+ return <Button onPress={handlePress} title="Check Out" />;
865
+ }
866
+ ```
867
+
868
+ ---
869
+
870
+ ## Persistence & Crash Recovery
871
+
872
+ When you provide a `StorageAdapter`, the SDK enables WAL persistence for spans, metrics, and logs.
873
+
874
+ ### Write-Ahead Log (WAL)
875
+
876
+ Before each network export attempt, batches are serialized to storage. If the app crashes or loses connectivity, the data survives. On the next `otel.init()`, undelivered batches are replayed automatically.
877
+
878
+ WAL limits:
879
+ - **Max 3 batches** per signal type. Oldest are evicted to prevent unbounded growth.
880
+ - **Max 500 items** per batch.
881
+ - **Exponential backoff** with jitter — up to 3 retries per batch (base delay: 500 ms). 4xx responses are not retried.
882
+ - **Circuit breaker** — after 5 consecutive delivery failures for an endpoint, attempts are paused for 60 seconds to avoid hammering an unavailable backend.
883
+
884
+ ### Crash span persistence
885
+
886
+ Fatal JS errors are written synchronously to storage and exported on the next app launch.
887
+
888
+ ### StorageAdapter interface
889
+
890
+ ```ts
891
+ interface StorageAdapter {
892
+ setSync(key: string, value: string): void;
893
+ getSync(key: string): string | null;
894
+ deleteSync(key: string): void;
895
+ }
896
+ ```
897
+
898
+ **MMKV example** (recommended):
899
+
900
+ ```ts
901
+ import { MMKV } from 'react-native-mmkv';
902
+ const storage = new MMKV();
903
+
904
+ otel.init({
905
+ serviceName: 'my-app',
906
+ storage: {
907
+ setSync: (key, value) => storage.set(key, value),
908
+ getSync: (key) => storage.getString(key) ?? null,
909
+ deleteSync: (key) => storage.delete(key),
910
+ },
911
+ });
912
+ ```
913
+
914
+ > `AsyncStorage` is not compatible — the adapter must be synchronous.
915
+
916
+ ---
917
+
918
+ ## Connectivity-Aware Flushing
919
+
920
+ Provide a `NetworkAdapter` to automatically pause telemetry delivery while the device is offline and resume immediately when connectivity is restored.
921
+
922
+ ```ts
923
+ import NetInfo from '@react-native-community/netinfo';
924
+ import type { NetworkAdapter } from 'react-native-otel';
925
+
926
+ const networkAdapter: NetworkAdapter = {
927
+ addListener(cb) {
928
+ const unsub = NetInfo.addEventListener((state) => cb(!!state.isConnected));
929
+ return unsub; // called on otel.shutdown()
930
+ },
931
+ };
932
+
933
+ otel.init({
934
+ serviceName: 'my-app',
935
+ networkAdapter,
936
+ ...
937
+ });
938
+ ```
939
+
940
+ `NetworkAdapter` is a plain interface — any connectivity library works. `@react-native-community/netinfo` is **not** a dependency of `react-native-otel`.
941
+
942
+ ---
943
+
944
+ ## User Identification
945
+
946
+ ```ts
947
+ // After login
948
+ otel.setUser({ id: '42', email: 'user@example.com' });
949
+
950
+ // Via context hook
951
+ const { setUser } = useOtel();
952
+ setUser({ id: currentUser.id });
953
+
954
+ // Clear on logout
955
+ otel.setUser({});
956
+ ```
957
+
958
+ User attributes are attached as `user.id` and `user.email` to all spans created after the call.
959
+
960
+ ---
961
+
962
+ ## Flush & Shutdown
963
+
964
+ ### otel.flush()
965
+
966
+ Sends all buffered spans and metrics. When a `NetworkAdapter` is configured and the device is offline, `flush()` is a no-op — data stays buffered until connectivity is restored.
967
+
968
+ ```ts
969
+ await api.logout();
970
+ otel.flush();
971
+ navigation.reset({ routes: [{ name: 'Login' }] });
972
+ ```
973
+
974
+ ### otel.shutdown()
975
+
976
+ Ends the active screen span, flushes all buffers, clears flush timers, and removes the network listener.
977
+
978
+ ```ts
979
+ AppState.addEventListener('change', (state) => {
980
+ if (state === 'background') otel.shutdown();
981
+ });
982
+ ```
983
+
984
+ ---
985
+
986
+ ## TypeScript
987
+
988
+ All public types are exported from the package root:
989
+
990
+ ```ts
991
+ import type {
992
+ // Config & adapters
993
+ OtelConfig,
994
+ NetworkAdapter,
995
+
996
+ // Core
997
+ SpanKind,
998
+ SpanStatus,
999
+ SpanEvent,
1000
+ SpanLink,
1001
+ SpanOptions,
1002
+ ReadonlySpan,
1003
+ SpanProcessor,
1004
+ Attributes,
1005
+ AttributeValue,
1006
+ Resource,
1007
+
1008
+ // Sampling
1009
+ Sampler,
1010
+
1011
+ // Metrics
1012
+ HistogramOptions,
1013
+
1014
+ // Logging
1015
+ LogSeverity,
1016
+
1017
+ // Context
1018
+ SpanContextManagerPublic,
1019
+
1020
+ // Exporters
1021
+ SpanExporter,
1022
+ MetricExporter,
1023
+ LogExporter,
1024
+ MetricRecord,
1025
+ LogRecord,
1026
+
1027
+ // Instrumentation
1028
+ NavigationInstrumentation,
1029
+ AxiosInstrumentation,
1030
+ AxiosInstrumentationOptions,
1031
+ OtelAxiosRequestConfig,
1032
+ OtelAxiosResponse,
1033
+ FetchInstrumentationOptions,
1034
+ LinkingInstrumentation,
1035
+ StorageAdapter,
1036
+
1037
+ // React
1038
+ OtelContextValue,
1039
+ OtelProviderProps,
1040
+ } from 'react-native-otel';
1041
+ ```
1042
+
1043
+ The current SDK version is also exported:
1044
+
1045
+ ```ts
1046
+ import { SDK_VERSION } from 'react-native-otel';
1047
+ // e.g. '0.1.4'
1048
+ ```
1049
+
1050
+ ---
1051
+
1052
+ ## Limitations
1053
+
1054
+ ### No concurrent async context propagation
1055
+
1056
+ React Native runs on a single JS thread without `AsyncLocalStorage`. The context stack is shared across the event loop — interleaved `await` calls can corrupt active span tracking:
1057
+
1058
+ ```ts
1059
+ // BAD — concurrent spans racing on the shared context stack
1060
+ await Promise.all([
1061
+ tracer.startActiveSpan('fetch.a', async () => { await fetchA(); }),
1062
+ tracer.startActiveSpan('fetch.b', async () => { await fetchB(); }),
1063
+ ]);
1064
+
1065
+ // GOOD — pass parents explicitly for concurrent work
1066
+ const parent = spanContext.current();
1067
+ await Promise.all([
1068
+ (async () => {
1069
+ const span = tracer.startSpan('fetch.a', { parent });
1070
+ try { await fetchA(); span.setStatus('OK'); }
1071
+ finally { span.end(); }
1072
+ })(),
1073
+ (async () => {
1074
+ const span = tracer.startSpan('fetch.b', { parent });
1075
+ try { await fetchB(); span.setStatus('OK'); }
1076
+ finally { span.end(); }
1077
+ })(),
1078
+ ]);
1079
+ ```
1080
+
1081
+ ### OTLP/HTTP JSON only
1082
+
1083
+ The built-in exporters speak OTLP over HTTP using JSON encoding. OTLP/gRPC and OTLP/HTTP protobuf are not currently supported. Most SaaS observability platforms accept OTLP/HTTP JSON natively.
1084
+
1085
+ ### Head-based sampling only
1086
+
1087
+ The `sampleRate` and `Sampler` options make decisions at span creation time. Tail-based sampling (deciding after the full trace completes) is not supported.
1088
+
1089
+ ### StorageAdapter must be synchronous
1090
+
1091
+ The `StorageAdapter` interface requires synchronous `get`/`set`/`delete`. `AsyncStorage` and other async stores are incompatible. Use [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) or a similar synchronous store.
1092
+
1093
+ ### WAL storage limits
1094
+
1095
+ The WAL caps at **3 batches per signal type** with a maximum of **500 items per batch**. Data beyond these limits is evicted (oldest first). Very high-volume apps could lose telemetry during extended offline periods.
1096
+
1097
+ ### Expo Router instrumentation is opt-in
1098
+
1099
+ `useExpoRouterInstrumentation` is available via the `react-native-otel/expo-router` sub-path export. Ensure `expo-router` is installed before using it.
1100
+
1101
+ ---
24
1102
 
25
1103
  ## Contributing
26
1104
 
27
- - [Development workflow](CONTRIBUTING.md#development-workflow)
28
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
29
- - [Code of conduct](CODE_OF_CONDUCT.md)
1105
+ Contributions are welcome. This project is a Yarn workspace monorepo containing the library (root) and an example app (`example/`).
30
1106
 
31
- ## License
1107
+ ### Prerequisites
1108
+
1109
+ - Node.js — see [`.nvmrc`](./.nvmrc) for the required version
1110
+ - Yarn 4 — `corepack enable && corepack prepare yarn@4.11.0 --activate`
1111
+
1112
+ ### Setup
1113
+
1114
+ ```sh
1115
+ git clone https://github.com/03balogun/react-native-otel.git
1116
+ cd react-native-otel
1117
+ yarn
1118
+ ```
1119
+
1120
+ ### Common commands
32
1121
 
33
- MIT
1122
+ | Command | Description |
1123
+ |---|---|
1124
+ | `yarn test` | Run the Jest test suite |
1125
+ | `yarn test --watch` | Run tests in watch mode |
1126
+ | `yarn typecheck` | Type-check with TypeScript |
1127
+ | `yarn lint` | Lint with ESLint + Prettier |
1128
+ | `yarn lint --fix` | Auto-fix lint and formatting errors |
1129
+ | `yarn prepare` | Build the library (outputs to `lib/`) |
1130
+ | `yarn example start` | Start the Metro bundler for the example app |
1131
+ | `yarn example ios` | Run the example app on iOS |
1132
+ | `yarn example android` | Run the example app on Android |
1133
+
1134
+ ### Running tests
1135
+
1136
+ ```sh
1137
+ yarn test # all tests
1138
+ yarn test --watch # watch mode
1139
+ yarn test --ci # CI mode
1140
+ ```
1141
+
1142
+ Tests live in `src/__tests__/`. New features should include corresponding tests. Aim to test behaviour, not implementation details.
1143
+
1144
+ ### Sending a pull request
1145
+
1146
+ 1. **Open an issue first** for any change that affects the public API or architecture.
1147
+ 2. **Fork** the repo and create a branch from `main`.
1148
+ 3. **Write tests** and ensure the full suite passes (`yarn test`).
1149
+ 4. **Pass all checks** — `yarn typecheck`, `yarn lint`.
1150
+ 5. **Keep PRs small and focused** on a single concern.
1151
+ 6. Submit against `main`. CI runs tests and a build check automatically.
1152
+
1153
+ ### Releasing
1154
+
1155
+ Releases are fully automated. Every push to `main` that is not a bot commit triggers the [Release workflow](.github/workflows/release.yml), which bumps the patch version, publishes to npm, and creates a GitHub release.
34
1156
 
35
1157
  ---
36
1158
 
37
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
1159
+ ## License
1160
+
1161
+ MIT © [Wahab Balogun](https://github.com/03balogun)