react-native-otel 0.1.4 → 0.1.6
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.
- package/README.md +1133 -13
- package/lib/module/core/meter.js +28 -2
- package/lib/module/core/meter.js.map +1 -1
- package/lib/module/core/processor.js +28 -0
- package/lib/module/core/processor.js.map +1 -0
- package/lib/module/core/resource.js +1 -0
- package/lib/module/core/resource.js.map +1 -1
- package/lib/module/core/sampler.js +55 -0
- package/lib/module/core/sampler.js.map +1 -0
- package/lib/module/core/span.js +15 -1
- package/lib/module/core/span.js.map +1 -1
- package/lib/module/core/tracer.js +24 -4
- package/lib/module/core/tracer.js.map +1 -1
- package/lib/module/exporters/multi-exporter.js +57 -0
- package/lib/module/exporters/multi-exporter.js.map +1 -0
- package/lib/module/exporters/otlp-http-exporter.js +69 -9
- package/lib/module/exporters/otlp-http-exporter.js.map +1 -1
- package/lib/module/exporters/wal.js +59 -3
- package/lib/module/exporters/wal.js.map +1 -1
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/instrumentation/expo-router.js +76 -0
- package/lib/module/instrumentation/expo-router.js.map +1 -0
- package/lib/module/instrumentation/fetch.js +99 -0
- package/lib/module/instrumentation/fetch.js.map +1 -0
- package/lib/module/instrumentation/lifecycle.js +6 -1
- package/lib/module/instrumentation/lifecycle.js.map +1 -1
- package/lib/module/instrumentation/linking.js +65 -0
- package/lib/module/instrumentation/linking.js.map +1 -0
- package/lib/module/instrumentation/network.js +28 -4
- package/lib/module/instrumentation/network.js.map +1 -1
- package/lib/module/instrumentation/startup.js +46 -0
- package/lib/module/instrumentation/startup.js.map +1 -0
- package/lib/module/sdk.js +65 -5
- package/lib/module/sdk.js.map +1 -1
- package/lib/module/version.js +7 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/src/core/meter.d.ts +2 -0
- package/lib/typescript/src/core/meter.d.ts.map +1 -1
- package/lib/typescript/src/core/processor.d.ts +29 -0
- package/lib/typescript/src/core/processor.d.ts.map +1 -0
- package/lib/typescript/src/core/resource.d.ts +3 -0
- package/lib/typescript/src/core/resource.d.ts.map +1 -1
- package/lib/typescript/src/core/sampler.d.ts +31 -0
- package/lib/typescript/src/core/sampler.d.ts.map +1 -0
- package/lib/typescript/src/core/span.d.ts +16 -0
- package/lib/typescript/src/core/span.d.ts.map +1 -1
- package/lib/typescript/src/core/tracer.d.ts +8 -3
- package/lib/typescript/src/core/tracer.d.ts.map +1 -1
- package/lib/typescript/src/exporters/multi-exporter.d.ts +28 -0
- package/lib/typescript/src/exporters/multi-exporter.d.ts.map +1 -0
- package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +9 -0
- package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -1
- package/lib/typescript/src/exporters/wal.d.ts +2 -0
- package/lib/typescript/src/exporters/wal.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/expo-router.d.ts +31 -0
- package/lib/typescript/src/instrumentation/expo-router.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/fetch.d.ts +18 -0
- package/lib/typescript/src/instrumentation/fetch.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/lifecycle.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/linking.d.ts +23 -0
- package/lib/typescript/src/instrumentation/linking.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/network.d.ts.map +1 -1
- package/lib/typescript/src/instrumentation/startup.d.ts +16 -0
- package/lib/typescript/src/instrumentation/startup.d.ts.map +1 -0
- package/lib/typescript/src/sdk.d.ts +34 -0
- package/lib/typescript/src/sdk.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +2 -0
- package/lib/typescript/src/version.d.ts.map +1 -0
- package/package.json +6 -1
- package/src/core/meter.ts +33 -2
- package/src/core/processor.ts +33 -0
- package/src/core/resource.ts +6 -0
- package/src/core/sampler.ts +65 -0
- package/src/core/span.ts +28 -1
- package/src/core/tracer.ts +42 -7
- package/src/exporters/multi-exporter.ts +59 -0
- package/src/exporters/otlp-http-exporter.ts +79 -10
- package/src/exporters/wal.ts +62 -3
- package/src/index.ts +34 -1
- package/src/instrumentation/expo-router.ts +94 -0
- package/src/instrumentation/fetch.ts +134 -0
- package/src/instrumentation/lifecycle.ts +7 -1
- package/src/instrumentation/linking.ts +83 -0
- package/src/instrumentation/network.ts +33 -4
- package/src/instrumentation/startup.ts +49 -0
- package/src/sdk.ts +86 -4
- package/src/version.ts +6 -0
package/README.md
CHANGED
|
@@ -1,37 +1,1157 @@
|
|
|
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
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/react-native-otel)
|
|
6
|
+
[](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. `fetch`, error, and lifecycle instrumentation are installed automatically. Navigation and Axios instrumentation require explicit setup (see below).
|
|
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());
|
|
335
|
+
```
|
|
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 |
|
|
343
|
+
|
|
344
|
+
Import `react-native-otel` as early as possible in your entry file to maximise the accuracy of `module_load_ms`.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
### Deep Links & Push Notifications
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { otel, createLinkingInstrumentation, recordPushNotification } from 'react-native-otel';
|
|
352
|
+
|
|
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
|
+
});
|
|
365
|
+
```
|
|
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
|
+
}
|
|
10
429
|
```
|
|
11
430
|
|
|
431
|
+
#### Span API
|
|
12
432
|
|
|
13
|
-
|
|
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. |
|
|
14
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.
|
|
15
475
|
|
|
16
|
-
```
|
|
17
|
-
|
|
476
|
+
```ts
|
|
477
|
+
const screenSpan = tracer.startSpan('screen.Dashboard');
|
|
18
478
|
|
|
19
|
-
|
|
479
|
+
tracer.withSpan(screenSpan, () => {
|
|
480
|
+
tracer.startActiveSpan('load.widgets', async (span) => {
|
|
481
|
+
await loadWidgets();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
20
484
|
|
|
21
|
-
|
|
485
|
+
screenSpan.end();
|
|
22
486
|
```
|
|
23
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
|
+
---
|
|
24
1098
|
|
|
25
1099
|
## Contributing
|
|
26
1100
|
|
|
27
|
-
|
|
28
|
-
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
29
|
-
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
1101
|
+
Contributions are welcome. This project is a Yarn workspace monorepo containing the library (root) and an example app (`example/`).
|
|
30
1102
|
|
|
31
|
-
|
|
1103
|
+
### Prerequisites
|
|
1104
|
+
|
|
1105
|
+
- Node.js — see [`.nvmrc`](./.nvmrc) for the required version
|
|
1106
|
+
- Yarn 4 — `corepack enable && corepack prepare yarn@4.11.0 --activate`
|
|
1107
|
+
|
|
1108
|
+
### Setup
|
|
1109
|
+
|
|
1110
|
+
```sh
|
|
1111
|
+
git clone https://github.com/03balogun/react-native-otel.git
|
|
1112
|
+
cd react-native-otel
|
|
1113
|
+
yarn
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
### Common commands
|
|
32
1117
|
|
|
33
|
-
|
|
1118
|
+
| Command | Description |
|
|
1119
|
+
|---|---|
|
|
1120
|
+
| `yarn test` | Run the Jest test suite |
|
|
1121
|
+
| `yarn test --watch` | Run tests in watch mode |
|
|
1122
|
+
| `yarn typecheck` | Type-check with TypeScript |
|
|
1123
|
+
| `yarn lint` | Lint with ESLint + Prettier |
|
|
1124
|
+
| `yarn lint --fix` | Auto-fix lint and formatting errors |
|
|
1125
|
+
| `yarn prepare` | Build the library (outputs to `lib/`) |
|
|
1126
|
+
| `yarn example start` | Start the Metro bundler for the example app |
|
|
1127
|
+
| `yarn example ios` | Run the example app on iOS |
|
|
1128
|
+
| `yarn example android` | Run the example app on Android |
|
|
1129
|
+
|
|
1130
|
+
### Running tests
|
|
1131
|
+
|
|
1132
|
+
```sh
|
|
1133
|
+
yarn test # all tests
|
|
1134
|
+
yarn test --watch # watch mode
|
|
1135
|
+
yarn test --ci # CI mode
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
Tests live in `src/__tests__/`. New features should include corresponding tests. Aim to test behaviour, not implementation details.
|
|
1139
|
+
|
|
1140
|
+
### Sending a pull request
|
|
1141
|
+
|
|
1142
|
+
1. **Open an issue first** for any change that affects the public API or architecture.
|
|
1143
|
+
2. **Fork** the repo and create a branch from `main`.
|
|
1144
|
+
3. **Write tests** and ensure the full suite passes (`yarn test`).
|
|
1145
|
+
4. **Pass all checks** — `yarn typecheck`, `yarn lint`.
|
|
1146
|
+
5. **Keep PRs small and focused** on a single concern.
|
|
1147
|
+
6. Submit against `main`. CI runs tests and a build check automatically.
|
|
1148
|
+
|
|
1149
|
+
### Releasing
|
|
1150
|
+
|
|
1151
|
+
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
1152
|
|
|
35
1153
|
---
|
|
36
1154
|
|
|
37
|
-
|
|
1155
|
+
## License
|
|
1156
|
+
|
|
1157
|
+
MIT © [Wahab Balogun](https://github.com/03balogun)
|