@raccoon.ninja/otel-react 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 raccoon.ninja
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,433 @@
1
+ # @raccoon.ninja/otel-react
2
+
3
+ Drop-in OpenTelemetry instrumentation for React, React Native, and Next.js.
4
+
5
+ One component. Zero code changes. Traces, logs, and Web Vitals metrics exported via OTLP to any compatible collector.
6
+
7
+ ## Features
8
+
9
+ - **One-line setup** via `<OtelProvider>` or `initOtel()`
10
+ - **Auto-instrumentation** for `fetch`, `XMLHttpRequest`, document load, and user interactions
11
+ - **Web Vitals** (LCP, CLS, INP, TTFB, FCP) exported as OTel metrics
12
+ - **Error boundary** that records unhandled errors as spans and logs
13
+ - **Custom spans** via the `useTracer()` hook
14
+ - **OTLP HTTP/JSON** export to any OpenTelemetry-compatible backend
15
+ - **React Native** and **Next.js** support via subpath imports
16
+ - **Vendor-neutral** -- works with Grafana, Jaeger, Datadog, Honeycomb, or any OTLP collector
17
+ - **Tree-shakeable** with `sideEffects: false`, ESM + CJS dual format
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @raccoon.ninja/otel-react
23
+ ```
24
+
25
+ React is a peer dependency -- you need React 17, 18, or 19 installed in your project.
26
+
27
+ ## Quick Start
28
+
29
+ Wrap your app with `<OtelProvider>`:
30
+
31
+ ```tsx
32
+ import { OtelProvider } from '@raccoon.ninja/otel-react';
33
+
34
+ function App() {
35
+ return (
36
+ <OtelProvider serviceName="my-app">
37
+ <RestOfTheApp />
38
+ </OtelProvider>
39
+ );
40
+ }
41
+ ```
42
+
43
+ That's it. Your app now exports:
44
+
45
+ - Traces for every `fetch()` and `XMLHttpRequest` call
46
+ - Document load timing spans (DNS, TCP, TLS, DOM processing)
47
+ - User interaction spans (clicks)
48
+ - Web Vitals metrics (LCP, CLS, INP, TTFB, FCP)
49
+
50
+ All telemetry is sent to `http://localhost:4318` by default (the standard OTLP HTTP port).
51
+
52
+ ## Configuration
53
+
54
+ ### OtelProvider Props
55
+
56
+ | Prop | Type | Default | Description |
57
+ |------|------|---------|-------------|
58
+ | `serviceName` | `string` | **(required)** | Sets the `service.name` resource attribute |
59
+ | `endpoint` | `string` | `http://localhost:4318` | OTLP HTTP collector endpoint |
60
+ | `serviceVersion` | `string` | -- | Sets `service.version` resource attribute |
61
+ | `environment` | `string` | -- | Sets `deployment.environment` resource attribute |
62
+ | `headers` | `Record<string, string>` | -- | Custom headers for OTLP requests (e.g., auth tokens) |
63
+ | `exportTimeout` | `number` | `30000` | Export timeout in milliseconds |
64
+ | `ignoreUrls` | `(string \| RegExp)[]` | -- | URLs to exclude from fetch/XHR instrumentation |
65
+ | `instrumentations` | `InstrumentationConfig` | all enabled | Enable/disable specific auto-instrumentations |
66
+ | `resourceAttributes` | `Record<string, string>` | -- | Additional OTel resource attributes |
67
+ | `extensions` | `OtelExtension[]` | -- | Opt-in extensions (e.g., `withReactRouter()`) |
68
+ | `configureTracing` | `(provider) => void` | -- | Escape hatch to configure the TracerProvider |
69
+ | `configureExporter` | `(exporter) => void` | -- | Escape hatch to configure the OTLP exporter |
70
+
71
+ ### Pointing to a Collector
72
+
73
+ ```tsx
74
+ <OtelProvider
75
+ serviceName="my-app"
76
+ endpoint="https://otel-collector.example.com:4318"
77
+ headers={{ 'x-api-key': 'your-token' }}
78
+ >
79
+ <App />
80
+ </OtelProvider>
81
+ ```
82
+
83
+ ### Disabling Specific Instrumentations
84
+
85
+ ```tsx
86
+ <OtelProvider
87
+ serviceName="my-app"
88
+ instrumentations={{
89
+ fetch: true, // default: true
90
+ xhr: false, // disable XHR instrumentation
91
+ documentLoad: true, // default: true
92
+ userInteraction: false, // disable click tracking
93
+ webVitals: true, // default: true
94
+ }}
95
+ >
96
+ <App />
97
+ </OtelProvider>
98
+ ```
99
+
100
+ ### Excluding URLs from Instrumentation
101
+
102
+ Useful for health checks, analytics endpoints, or any URL that shouldn't generate spans:
103
+
104
+ ```tsx
105
+ <OtelProvider
106
+ serviceName="my-app"
107
+ ignoreUrls={[
108
+ /\/health$/,
109
+ /\/analytics/,
110
+ 'https://cdn.example.com',
111
+ ]}
112
+ >
113
+ <App />
114
+ </OtelProvider>
115
+ ```
116
+
117
+ ## Imperative API
118
+
119
+ For non-component contexts (testing, scripts, early initialization), use `initOtel()` directly:
120
+
121
+ ```typescript
122
+ import { initOtel } from '@raccoon.ninja/otel-react';
123
+
124
+ const otel = initOtel({
125
+ serviceName: 'my-app',
126
+ endpoint: 'https://otel-collector.example.com:4318',
127
+ });
128
+
129
+ // Later, during cleanup:
130
+ await otel.shutdown();
131
+ ```
132
+
133
+ `<OtelProvider>` calls `initOtel()` internally and handles shutdown on unmount.
134
+
135
+ ## Error Boundary
136
+
137
+ `TracedErrorBoundary` catches unhandled errors in its subtree and records them as both OTel spans and log records:
138
+
139
+ ```tsx
140
+ import { OtelProvider, TracedErrorBoundary } from '@raccoon.ninja/otel-react';
141
+
142
+ function App() {
143
+ return (
144
+ <OtelProvider serviceName="my-app">
145
+ <TracedErrorBoundary fallback={<div>Something went wrong</div>}>
146
+ <MainContent />
147
+ </TracedErrorBoundary>
148
+ </OtelProvider>
149
+ );
150
+ }
151
+ ```
152
+
153
+ The fallback can also be a render function that receives the error and a reset callback:
154
+
155
+ ```tsx
156
+ <TracedErrorBoundary
157
+ fallback={(error, reset) => (
158
+ <div>
159
+ <p>Error: {error.message}</p>
160
+ <button onClick={reset}>Try Again</button>
161
+ </div>
162
+ )}
163
+ onError={(error, errorInfo) => {
164
+ // Optional: additional error reporting
165
+ console.error('Caught by boundary:', error);
166
+ }}
167
+ >
168
+ <RiskyComponent />
169
+ </TracedErrorBoundary>
170
+ ```
171
+
172
+ Each caught error generates:
173
+ - A span with `error.type`, `error.message`, `error.stack`, and `error.component_stack` attributes
174
+ - A log record at `ERROR` severity with the same details
175
+
176
+ ## Custom Spans
177
+
178
+ Use the `useTracer()` hook to create custom spans for business-critical operations:
179
+
180
+ ```tsx
181
+ import { useTracer } from '@raccoon.ninja/otel-react';
182
+
183
+ function CheckoutForm() {
184
+ const tracer = useTracer();
185
+
186
+ const handleSubmit = async (data: FormData) => {
187
+ const span = tracer.startSpan('checkout.submit');
188
+ span.setAttribute('cart.item_count', data.items.length);
189
+
190
+ try {
191
+ await submitOrder(data);
192
+ span.setStatus({ code: 1 }); // SpanStatusCode.OK
193
+ } catch (error) {
194
+ span.setStatus({ code: 2, message: String(error) }); // SpanStatusCode.ERROR
195
+ span.recordException(error as Error);
196
+ throw error;
197
+ } finally {
198
+ span.end();
199
+ }
200
+ };
201
+
202
+ return <form onSubmit={handleSubmit}>{/* ... */}</form>;
203
+ }
204
+ ```
205
+
206
+ You can provide a custom tracer name and version:
207
+
208
+ ```tsx
209
+ const tracer = useTracer('my-feature', '1.0.0');
210
+ ```
211
+
212
+ ## Extensions
213
+
214
+ ### React Router
215
+
216
+ Opt-in route change tracing for React Router v6/v7:
217
+
218
+ ```tsx
219
+ import { OtelProvider, withReactRouter } from '@raccoon.ninja/otel-react';
220
+
221
+ function App() {
222
+ return (
223
+ <OtelProvider
224
+ serviceName="my-app"
225
+ extensions={[withReactRouter()]}
226
+ >
227
+ <RouterProvider router={router} />
228
+ </OtelProvider>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ## What Gets Collected
234
+
235
+ ### Traces (Automatic)
236
+
237
+ | Signal | Source | What It Captures |
238
+ |--------|--------|-----------------|
239
+ | `fetch()` calls | `@opentelemetry/instrumentation-fetch` | URL, method, status, duration |
240
+ | `XMLHttpRequest` | `@opentelemetry/instrumentation-xml-http-request` | URL, method, status, duration |
241
+ | Document load | `@opentelemetry/instrumentation-document-load` | DNS, TCP, TLS, DOM processing |
242
+ | User interactions | `@opentelemetry/instrumentation-user-interaction` | Click events on DOM elements |
243
+ | Error boundary | `TracedErrorBoundary` | Uncaught errors with component stack |
244
+
245
+ ### Metrics (Automatic)
246
+
247
+ | Metric | Type | Description |
248
+ |--------|------|-------------|
249
+ | `web_vitals.lcp` | Histogram | Largest Contentful Paint (ms) |
250
+ | `web_vitals.cls` | Histogram | Cumulative Layout Shift |
251
+ | `web_vitals.inp` | Histogram | Interaction to Next Paint (ms) |
252
+ | `web_vitals.ttfb` | Histogram | Time to First Byte (ms) |
253
+ | `web_vitals.fcp` | Histogram | First Contentful Paint (ms) |
254
+
255
+ Each metric includes `web_vitals.rating` (`good`, `needs-improvement`, `poor`) and `page.url` attributes.
256
+
257
+ ### Resource Attributes (Automatic)
258
+
259
+ | Attribute | Source |
260
+ |-----------|--------|
261
+ | `service.name` | Config (required) |
262
+ | `service.version` | Config (optional) |
263
+ | `deployment.environment` | Config (optional) |
264
+ | `telemetry.sdk.name` | `@raccoon.ninja/otel-react` |
265
+ | `telemetry.sdk.version` | Package version |
266
+ | `browser.language` | `navigator.language` |
267
+ | `browser.user_agent` | `navigator.userAgent` |
268
+ | `browser.platform` | `navigator.platform` |
269
+
270
+ Custom attributes can be added via the `resourceAttributes` prop.
271
+
272
+ ## React Native
273
+
274
+ Import from `@raccoon.ninja/otel-react/native`:
275
+
276
+ ```typescript
277
+ import { initOtel } from '@raccoon.ninja/otel-react/native';
278
+
279
+ const otel = await initOtel({
280
+ serviceName: 'my-rn-app',
281
+ endpoint: 'https://otel-collector.example.com:4318',
282
+ });
283
+ ```
284
+
285
+ Key differences from the browser entry:
286
+ - Uses `BasicTracerProvider` instead of `WebTracerProvider` (no DOM APIs)
287
+ - Only `fetch` instrumentation is enabled (no document load, user interaction, or Web Vitals)
288
+ - XHR is disabled by default to avoid duplicate spans (React Native's `fetch` polyfills over XHR)
289
+ - Uses `AppState` change listener for flush instead of `visibilitychange`
290
+
291
+ ### React Navigation
292
+
293
+ Track navigation state changes as spans:
294
+
295
+ ```typescript
296
+ import { initOtel, withReactNavigation, createNavigationTracker } from '@raccoon.ninja/otel-react/native';
297
+
298
+ const otel = await initOtel({
299
+ serviceName: 'my-rn-app',
300
+ extensions: [withReactNavigation()],
301
+ });
302
+
303
+ const onStateChange = createNavigationTracker();
304
+
305
+ function App() {
306
+ return (
307
+ <NavigationContainer onStateChange={onStateChange}>
308
+ {/* screens */}
309
+ </NavigationContainer>
310
+ );
311
+ }
312
+ ```
313
+
314
+ ## Next.js
315
+
316
+ Next.js requires a two-part setup (server + client).
317
+
318
+ ### Server-Side (`instrumentation.ts`)
319
+
320
+ ```typescript
321
+ import { initOtelServer } from '@raccoon.ninja/otel-react/nextjs';
322
+
323
+ export async function register() {
324
+ await initOtelServer({
325
+ serviceName: 'my-nextjs-app',
326
+ endpoint: 'https://otel-collector.example.com:4318',
327
+ });
328
+ }
329
+ ```
330
+
331
+ ### Client-Side (Root Layout)
332
+
333
+ ```tsx
334
+ // app/providers.tsx
335
+ 'use client';
336
+
337
+ import { OtelProvider } from '@raccoon.ninja/otel-react';
338
+
339
+ export function Providers({ children }: { children: React.ReactNode }) {
340
+ return (
341
+ <OtelProvider serviceName="my-nextjs-app">
342
+ {children}
343
+ </OtelProvider>
344
+ );
345
+ }
346
+
347
+ // app/layout.tsx
348
+ import { Providers } from './providers';
349
+
350
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
351
+ return (
352
+ <html>
353
+ <body>
354
+ <Providers>{children}</Providers>
355
+ </body>
356
+ </html>
357
+ );
358
+ }
359
+ ```
360
+
361
+ ## CORS Configuration
362
+
363
+ If your app and collector are on different origins, configure CORS on the collector. Example for the OpenTelemetry Collector:
364
+
365
+ ```yaml
366
+ # otel-collector-config.yaml
367
+ receivers:
368
+ otlp:
369
+ protocols:
370
+ http:
371
+ endpoint: 0.0.0.0:4318
372
+ cors:
373
+ allowed_origins:
374
+ - "https://your-app.com"
375
+ - "http://localhost:3000"
376
+ allowed_headers:
377
+ - "Content-Type"
378
+ - "X-Requested-With"
379
+ max_age: 7200
380
+ ```
381
+
382
+ ## Graceful Shutdown
383
+
384
+ The package automatically flushes pending telemetry:
385
+
386
+ - On `visibilitychange` (when the tab becomes hidden)
387
+ - On `beforeunload` (when the page is about to close)
388
+ - On `<OtelProvider>` unmount
389
+ - When `otel.shutdown()` is called (imperative API)
390
+
391
+ This ensures telemetry is not lost during page navigation or tab close.
392
+
393
+ ## API Reference
394
+
395
+ ### Exports from `@raccoon.ninja/otel-react`
396
+
397
+ | Export | Type | Description |
398
+ |--------|------|-------------|
399
+ | `OtelProvider` | Component | React provider that initializes OTel on mount |
400
+ | `TracedErrorBoundary` | Component | Error boundary that records errors as spans/logs |
401
+ | `initOtel` | Function | Imperative initialization, returns `OtelHandle` |
402
+ | `useTracer` | Hook | Get an OTel `Tracer` for custom spans |
403
+ | `withReactRouter` | Function | Extension for React Router route tracing |
404
+ | `OtelOptions` | Type | Configuration interface |
405
+ | `OtelHandle` | Type | Handle with `shutdown()` method |
406
+ | `OtelExtension` | Type | Extension function signature |
407
+ | `InstrumentationConfig` | Type | Instrumentation toggle config |
408
+ | `OtelProviderProps` | Type | Props for `<OtelProvider>` |
409
+ | `TracedErrorBoundaryProps` | Type | Props for `<TracedErrorBoundary>` |
410
+
411
+ ### Exports from `@raccoon.ninja/otel-react/native`
412
+
413
+ | Export | Type | Description |
414
+ |--------|------|-------------|
415
+ | `initOtel` | Function | RN-specific initialization (async) |
416
+ | `withReactNavigation` | Function | Extension for React Navigation |
417
+ | `createNavigationTracker` | Function | Creates `onStateChange` handler |
418
+
419
+ ### Exports from `@raccoon.ninja/otel-react/nextjs`
420
+
421
+ | Export | Type | Description |
422
+ |--------|------|-------------|
423
+ | `initOtelServer` | Function | Server-side initialization for `instrumentation.ts` |
424
+
425
+ ## Requirements
426
+
427
+ - **Node.js** >= 18
428
+ - **React** 17, 18, or 19
429
+ - An OTLP-compatible collector (e.g., [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), [Grafana Alloy](https://grafana.com/docs/alloy/), or any vendor's OTLP endpoint)
430
+
431
+ ## License
432
+
433
+ MIT