react-native-otel 0.1.0

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 (105) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +37 -0
  3. package/lib/module/context/span-context.js +14 -0
  4. package/lib/module/context/span-context.js.map +1 -0
  5. package/lib/module/core/attributes.js +43 -0
  6. package/lib/module/core/attributes.js.map +1 -0
  7. package/lib/module/core/clock.js +8 -0
  8. package/lib/module/core/clock.js.map +1 -0
  9. package/lib/module/core/ids.js +16 -0
  10. package/lib/module/core/ids.js.map +1 -0
  11. package/lib/module/core/log-record.js +41 -0
  12. package/lib/module/core/log-record.js.map +1 -0
  13. package/lib/module/core/meter.js +70 -0
  14. package/lib/module/core/meter.js.map +1 -0
  15. package/lib/module/core/resource.js +20 -0
  16. package/lib/module/core/resource.js.map +1 -0
  17. package/lib/module/core/span.js +96 -0
  18. package/lib/module/core/span.js.map +1 -0
  19. package/lib/module/core/tracer.js +48 -0
  20. package/lib/module/core/tracer.js.map +1 -0
  21. package/lib/module/exporters/console-exporter.js +53 -0
  22. package/lib/module/exporters/console-exporter.js.map +1 -0
  23. package/lib/module/exporters/otlp-http-exporter.js +317 -0
  24. package/lib/module/exporters/otlp-http-exporter.js.map +1 -0
  25. package/lib/module/exporters/types.js +4 -0
  26. package/lib/module/exporters/types.js.map +1 -0
  27. package/lib/module/index.js +24 -0
  28. package/lib/module/index.js.map +1 -0
  29. package/lib/module/instrumentation/errors.js +63 -0
  30. package/lib/module/instrumentation/errors.js.map +1 -0
  31. package/lib/module/instrumentation/lifecycle.js +15 -0
  32. package/lib/module/instrumentation/lifecycle.js.map +1 -0
  33. package/lib/module/instrumentation/navigation.js +51 -0
  34. package/lib/module/instrumentation/navigation.js.map +1 -0
  35. package/lib/module/instrumentation/network.js +183 -0
  36. package/lib/module/instrumentation/network.js.map +1 -0
  37. package/lib/module/package.json +1 -0
  38. package/lib/module/react/OtelProvider.js +57 -0
  39. package/lib/module/react/OtelProvider.js.map +1 -0
  40. package/lib/module/react/useOtel.js +12 -0
  41. package/lib/module/react/useOtel.js.map +1 -0
  42. package/lib/module/sdk.js +127 -0
  43. package/lib/module/sdk.js.map +1 -0
  44. package/lib/typescript/package.json +1 -0
  45. package/lib/typescript/src/context/span-context.d.ts +9 -0
  46. package/lib/typescript/src/context/span-context.d.ts.map +1 -0
  47. package/lib/typescript/src/core/attributes.d.ts +6 -0
  48. package/lib/typescript/src/core/attributes.d.ts.map +1 -0
  49. package/lib/typescript/src/core/clock.d.ts +2 -0
  50. package/lib/typescript/src/core/clock.d.ts.map +1 -0
  51. package/lib/typescript/src/core/ids.d.ts +3 -0
  52. package/lib/typescript/src/core/ids.d.ts.map +1 -0
  53. package/lib/typescript/src/core/log-record.d.ts +15 -0
  54. package/lib/typescript/src/core/log-record.d.ts.map +1 -0
  55. package/lib/typescript/src/core/meter.d.ts +30 -0
  56. package/lib/typescript/src/core/meter.d.ts.map +1 -0
  57. package/lib/typescript/src/core/resource.d.ts +25 -0
  58. package/lib/typescript/src/core/resource.d.ts.map +1 -0
  59. package/lib/typescript/src/core/span.d.ts +77 -0
  60. package/lib/typescript/src/core/span.d.ts.map +1 -0
  61. package/lib/typescript/src/core/tracer.d.ts +21 -0
  62. package/lib/typescript/src/core/tracer.d.ts.map +1 -0
  63. package/lib/typescript/src/exporters/console-exporter.d.ts +17 -0
  64. package/lib/typescript/src/exporters/console-exporter.d.ts.map +1 -0
  65. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +58 -0
  66. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -0
  67. package/lib/typescript/src/exporters/types.d.ts +25 -0
  68. package/lib/typescript/src/exporters/types.d.ts.map +1 -0
  69. package/lib/typescript/src/index.d.ts +25 -0
  70. package/lib/typescript/src/index.d.ts.map +1 -0
  71. package/lib/typescript/src/instrumentation/errors.d.ts +15 -0
  72. package/lib/typescript/src/instrumentation/errors.d.ts.map +1 -0
  73. package/lib/typescript/src/instrumentation/lifecycle.d.ts +3 -0
  74. package/lib/typescript/src/instrumentation/lifecycle.d.ts.map +1 -0
  75. package/lib/typescript/src/instrumentation/navigation.d.ts +7 -0
  76. package/lib/typescript/src/instrumentation/navigation.d.ts.map +1 -0
  77. package/lib/typescript/src/instrumentation/network.d.ts +33 -0
  78. package/lib/typescript/src/instrumentation/network.d.ts.map +1 -0
  79. package/lib/typescript/src/react/OtelProvider.d.ts +21 -0
  80. package/lib/typescript/src/react/OtelProvider.d.ts.map +1 -0
  81. package/lib/typescript/src/react/useOtel.d.ts +3 -0
  82. package/lib/typescript/src/react/useOtel.d.ts.map +1 -0
  83. package/lib/typescript/src/sdk.d.ts +50 -0
  84. package/lib/typescript/src/sdk.d.ts.map +1 -0
  85. package/package.json +125 -0
  86. package/src/context/span-context.ts +17 -0
  87. package/src/core/attributes.ts +61 -0
  88. package/src/core/clock.ts +5 -0
  89. package/src/core/ids.ts +15 -0
  90. package/src/core/log-record.ts +58 -0
  91. package/src/core/meter.ts +82 -0
  92. package/src/core/resource.ts +50 -0
  93. package/src/core/span.ts +161 -0
  94. package/src/core/tracer.ts +75 -0
  95. package/src/exporters/console-exporter.ts +89 -0
  96. package/src/exporters/otlp-http-exporter.ts +377 -0
  97. package/src/exporters/types.ts +29 -0
  98. package/src/index.ts +63 -0
  99. package/src/instrumentation/errors.ts +95 -0
  100. package/src/instrumentation/lifecycle.ts +17 -0
  101. package/src/instrumentation/navigation.ts +61 -0
  102. package/src/instrumentation/network.ts +253 -0
  103. package/src/react/OtelProvider.tsx +98 -0
  104. package/src/react/useOtel.ts +14 -0
  105. package/src/sdk.ts +179 -0
@@ -0,0 +1,377 @@
1
+ import type {
2
+ LogExporter,
3
+ LogRecord,
4
+ MetricExporter,
5
+ MetricRecord,
6
+ } from './types';
7
+ import type { Attributes } from '../core/attributes';
8
+ import type { Resource } from '../core/resource';
9
+ import type { ReadonlySpan, SpanExporter } from '../core/span';
10
+
11
+ // ─── OTLP attribute value serialization ──────────────────────────────────────
12
+
13
+ type OtlpAnyValue =
14
+ | { stringValue: string }
15
+ | { intValue: string }
16
+ | { doubleValue: number }
17
+ | { boolValue: boolean }
18
+ | { arrayValue: { values: OtlpAnyValue[] } };
19
+
20
+ function toOtlpValue(value: unknown): OtlpAnyValue {
21
+ if (typeof value === 'boolean') return { boolValue: value };
22
+ if (typeof value === 'string') return { stringValue: value };
23
+ if (typeof value === 'number') {
24
+ return Number.isInteger(value)
25
+ ? { intValue: String(value) }
26
+ : { doubleValue: value };
27
+ }
28
+ if (Array.isArray(value)) {
29
+ return { arrayValue: { values: value.map(toOtlpValue) } };
30
+ }
31
+ return { stringValue: String(value) };
32
+ }
33
+
34
+ function toOtlpAttributes(attrs: Attributes | Record<string, unknown>) {
35
+ return Object.entries(attrs).map(([key, value]) => ({
36
+ key,
37
+ value: toOtlpValue(value),
38
+ }));
39
+ }
40
+
41
+ // Milliseconds → nanoseconds as string (exceeds JS safe integer range).
42
+ function msToNano(ms: number): string {
43
+ return String(ms * 1_000_000);
44
+ }
45
+
46
+ // ─── SpanKind + SpanStatus mappings ──────────────────────────────────────────
47
+
48
+ const SPAN_KIND: Record<string, number> = {
49
+ INTERNAL: 1,
50
+ SERVER: 2,
51
+ CLIENT: 3,
52
+ PRODUCER: 4,
53
+ CONSUMER: 5,
54
+ };
55
+
56
+ const SPAN_STATUS_CODE: Record<string, number> = {
57
+ UNSET: 0,
58
+ OK: 1,
59
+ ERROR: 2,
60
+ };
61
+
62
+ // ─── Exporter ─────────────────────────────────────────────────────────────────
63
+
64
+ export interface OtlpHttpExporterOptions {
65
+ // Full OTLP traces endpoint, e.g. 'https://in-otel.hyperdx.io/v1/traces'
66
+ endpoint: string;
67
+ // Additional headers, e.g. { authorization: '<api-key>' }
68
+ headers?: Record<string, string>;
69
+ // Max spans to buffer before flushing immediately. Default: 50.
70
+ batchSize?: number;
71
+ // How often to auto-flush buffered spans in ms. Default: 30_000.
72
+ flushIntervalMs?: number;
73
+ }
74
+
75
+ export class OtlpHttpExporter implements SpanExporter {
76
+ private readonly tracesEndpoint: string;
77
+ private readonly headers: Record<string, string>;
78
+ private readonly batchSize: number;
79
+ private buffer: ReadonlySpan[] = [];
80
+ private resource_: Readonly<Resource> | undefined;
81
+ private timer_: ReturnType<typeof setInterval> | undefined;
82
+
83
+ constructor(options: OtlpHttpExporterOptions) {
84
+ this.tracesEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/traces';
85
+ this.headers = {
86
+ 'Content-Type': 'application/json',
87
+ ...options.headers,
88
+ };
89
+ this.batchSize = options.batchSize ?? 50;
90
+
91
+ const interval = options.flushIntervalMs ?? 30_000;
92
+ this.timer_ = setInterval(() => {
93
+ this.flush();
94
+ }, interval);
95
+ }
96
+
97
+ // Called by OtelSDK.init() after buildResource() — not part of SpanExporter.
98
+ setResource(resource: Readonly<Resource>): void {
99
+ this.resource_ = resource;
100
+ }
101
+
102
+ export(spans: ReadonlySpan[]): void {
103
+ this.buffer.push(...spans);
104
+ if (this.buffer.length >= this.batchSize) {
105
+ this.flush();
106
+ }
107
+ }
108
+
109
+ flush(): void {
110
+ if (this.buffer.length === 0) return;
111
+ const batch = this.buffer.splice(0);
112
+ this.send(batch);
113
+ }
114
+
115
+ // Clear the flush timer and send any remaining buffered spans.
116
+ destroy(): void {
117
+ if (this.timer_ !== undefined) {
118
+ clearInterval(this.timer_);
119
+ this.timer_ = undefined;
120
+ }
121
+ this.flush();
122
+ }
123
+
124
+ private send(spans: ReadonlySpan[]): void {
125
+ const resourceAttrs = this.resource_
126
+ ? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
127
+ : [];
128
+
129
+ const body = JSON.stringify({
130
+ resourceSpans: [
131
+ {
132
+ resource: { attributes: resourceAttrs },
133
+ scopeSpans: [
134
+ {
135
+ scope: { name: 'react-native-otel', version: '0.1.0' },
136
+ spans: spans.map((s) => this.toOtlpSpan(s)),
137
+ },
138
+ ],
139
+ },
140
+ ],
141
+ });
142
+
143
+ fetch(this.tracesEndpoint, {
144
+ method: 'POST',
145
+ headers: this.headers,
146
+ body,
147
+ }).catch(() => {});
148
+ }
149
+
150
+ private toOtlpSpan(span: ReadonlySpan) {
151
+ return {
152
+ traceId: span.traceId,
153
+ spanId: span.spanId,
154
+ // Root spans must omit parentSpanId — empty string breaks trace tree assembly.
155
+ ...(span.parentSpanId ? { parentSpanId: span.parentSpanId } : {}),
156
+ name: span.name,
157
+ kind: SPAN_KIND[span.kind] ?? 1,
158
+ startTimeUnixNano: msToNano(span.startTimeMs),
159
+ endTimeUnixNano: msToNano(span.endTimeMs ?? span.startTimeMs),
160
+ attributes: toOtlpAttributes(span.attributes as Attributes),
161
+ events: span.events.map((event) => ({
162
+ name: event.name,
163
+ timeUnixNano: msToNano(event.timestampMs),
164
+ attributes: toOtlpAttributes(event.attributes),
165
+ })),
166
+ droppedEventsCount: span.droppedEventsCount,
167
+ status: {
168
+ code: SPAN_STATUS_CODE[span.status] ?? 0,
169
+ // Omit message when empty — some parsers reject the empty string.
170
+ ...(span.statusMessage ? { message: span.statusMessage } : {}),
171
+ },
172
+ };
173
+ }
174
+ }
175
+
176
+ // ─── Metric exporter ──────────────────────────────────────────────────────────
177
+
178
+ // OTLP aggregation temporality: 2 = CUMULATIVE
179
+ const AGGREGATION_TEMPORALITY_CUMULATIVE = 2;
180
+
181
+ export interface OtlpHttpMetricExporterOptions {
182
+ endpoint: string;
183
+ headers?: Record<string, string>;
184
+ }
185
+
186
+ export class OtlpHttpMetricExporter implements MetricExporter {
187
+ private readonly metricsEndpoint: string;
188
+ private readonly headers: Record<string, string>;
189
+ private resource_: Readonly<Resource> | undefined;
190
+
191
+ constructor(options: OtlpHttpMetricExporterOptions) {
192
+ this.metricsEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/metrics';
193
+ this.headers = {
194
+ 'Content-Type': 'application/json',
195
+ ...options.headers,
196
+ };
197
+ }
198
+
199
+ setResource(resource: Readonly<Resource>): void {
200
+ this.resource_ = resource;
201
+ }
202
+
203
+ export(metrics: MetricRecord[]): void {
204
+ if (metrics.length === 0) return;
205
+ this.send(metrics);
206
+ }
207
+
208
+ private send(metrics: MetricRecord[]): void {
209
+ const resourceAttrs = this.resource_
210
+ ? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
211
+ : [];
212
+
213
+ // Group records by name so each unique metric name becomes one OTLP metric.
214
+ const byName = new Map<string, MetricRecord[]>();
215
+ for (const record of metrics) {
216
+ const group = byName.get(record.name);
217
+ if (group) {
218
+ group.push(record);
219
+ } else {
220
+ byName.set(record.name, [record]);
221
+ }
222
+ }
223
+
224
+ const otlpMetrics = Array.from(byName.entries()).map(([name, records]) => {
225
+ const type = records[0]?.type;
226
+
227
+ // Counters → sum; histograms + gauges → gauge (no bucket data available).
228
+ if (type === 'counter') {
229
+ return {
230
+ name,
231
+ sum: {
232
+ dataPoints: records.map((r) => ({
233
+ asDouble: r.value,
234
+ startTimeUnixNano: msToNano(r.timestampMs),
235
+ timeUnixNano: msToNano(r.timestampMs),
236
+ attributes: toOtlpAttributes(r.attributes),
237
+ })),
238
+ aggregationTemporality: AGGREGATION_TEMPORALITY_CUMULATIVE,
239
+ isMonotonic: true,
240
+ },
241
+ };
242
+ }
243
+
244
+ return {
245
+ name,
246
+ gauge: {
247
+ dataPoints: records.map((r) => ({
248
+ asDouble: r.value,
249
+ timeUnixNano: msToNano(r.timestampMs),
250
+ attributes: toOtlpAttributes(r.attributes),
251
+ })),
252
+ },
253
+ };
254
+ });
255
+
256
+ const body = JSON.stringify({
257
+ resourceMetrics: [
258
+ {
259
+ resource: { attributes: resourceAttrs },
260
+ scopeMetrics: [
261
+ {
262
+ scope: { name: 'react-native-otel', version: '0.1.0' },
263
+ metrics: otlpMetrics,
264
+ },
265
+ ],
266
+ },
267
+ ],
268
+ });
269
+
270
+ fetch(this.metricsEndpoint, {
271
+ method: 'POST',
272
+ headers: this.headers,
273
+ body,
274
+ }).catch(() => {});
275
+ }
276
+ }
277
+
278
+ // ─── Log exporter ─────────────────────────────────────────────────────────────
279
+
280
+ // OTLP severity number mapping (spec: https://opentelemetry.io/docs/specs/otel/logs/data-model/)
281
+ const LOG_SEVERITY_NUMBER: Record<string, number> = {
282
+ TRACE: 1,
283
+ DEBUG: 5,
284
+ INFO: 9,
285
+ WARN: 13,
286
+ ERROR: 17,
287
+ FATAL: 21,
288
+ };
289
+
290
+ export interface OtlpHttpLogExporterOptions {
291
+ endpoint: string;
292
+ headers?: Record<string, string>;
293
+ batchSize?: number;
294
+ flushIntervalMs?: number;
295
+ }
296
+
297
+ export class OtlpHttpLogExporter implements LogExporter {
298
+ private readonly logsEndpoint: string;
299
+ private readonly headers: Record<string, string>;
300
+ private readonly batchSize: number;
301
+ private buffer: LogRecord[] = [];
302
+ private resource_: Readonly<Resource> | undefined;
303
+ private timer_: ReturnType<typeof setInterval> | undefined;
304
+
305
+ constructor(options: OtlpHttpLogExporterOptions) {
306
+ this.logsEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/logs';
307
+ this.headers = {
308
+ 'Content-Type': 'application/json',
309
+ ...options.headers,
310
+ };
311
+ this.batchSize = options.batchSize ?? 50;
312
+
313
+ const interval = options.flushIntervalMs ?? 30_000;
314
+ this.timer_ = setInterval(() => {
315
+ this.flush();
316
+ }, interval);
317
+ }
318
+
319
+ setResource(resource: Readonly<Resource>): void {
320
+ this.resource_ = resource;
321
+ }
322
+
323
+ export(logs: LogRecord[]): void {
324
+ this.buffer.push(...logs);
325
+ if (this.buffer.length >= this.batchSize) {
326
+ this.flush();
327
+ }
328
+ }
329
+
330
+ flush(): void {
331
+ if (this.buffer.length === 0) return;
332
+ const batch = this.buffer.splice(0);
333
+ this.send(batch);
334
+ }
335
+
336
+ destroy(): void {
337
+ if (this.timer_ !== undefined) {
338
+ clearInterval(this.timer_);
339
+ this.timer_ = undefined;
340
+ }
341
+ this.flush();
342
+ }
343
+
344
+ private send(logs: LogRecord[]): void {
345
+ const resourceAttrs = this.resource_
346
+ ? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
347
+ : [];
348
+
349
+ const body = JSON.stringify({
350
+ resourceLogs: [
351
+ {
352
+ resource: { attributes: resourceAttrs },
353
+ scopeLogs: [
354
+ {
355
+ scope: { name: 'react-native-otel', version: '0.1.0' },
356
+ logRecords: logs.map((log) => ({
357
+ timeUnixNano: msToNano(log.timestampMs),
358
+ severityNumber: LOG_SEVERITY_NUMBER[log.severity] ?? 9,
359
+ severityText: log.severity,
360
+ body: { stringValue: log.body },
361
+ ...(log.traceId ? { traceId: log.traceId } : {}),
362
+ ...(log.spanId ? { spanId: log.spanId } : {}),
363
+ attributes: toOtlpAttributes(log.attributes),
364
+ })),
365
+ },
366
+ ],
367
+ },
368
+ ],
369
+ });
370
+
371
+ fetch(this.logsEndpoint, {
372
+ method: 'POST',
373
+ headers: this.headers,
374
+ body,
375
+ }).catch(() => {});
376
+ }
377
+ }
@@ -0,0 +1,29 @@
1
+ import type { Attributes } from '../core/attributes';
2
+ import type { ReadonlySpan, SpanExporter } from '../core/span';
3
+
4
+ export type { ReadonlySpan, SpanExporter };
5
+
6
+ export interface MetricRecord {
7
+ type: 'counter' | 'histogram' | 'gauge';
8
+ name: string;
9
+ value: number;
10
+ timestampMs: number;
11
+ attributes: Attributes;
12
+ }
13
+
14
+ export interface LogRecord {
15
+ timestampMs: number;
16
+ severity: string;
17
+ body: string;
18
+ traceId: string | undefined;
19
+ spanId: string | undefined;
20
+ attributes: Attributes;
21
+ }
22
+
23
+ export interface MetricExporter {
24
+ export(metrics: MetricRecord[]): void;
25
+ }
26
+
27
+ export interface LogExporter {
28
+ export(logs: LogRecord[]): void;
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ // SDK
2
+ export { otel } from './sdk';
3
+ export type { OtelConfig } from './sdk';
4
+
5
+ // Core
6
+ export { Span, NoopSpan } from './core/span';
7
+ export type {
8
+ SpanKind,
9
+ SpanStatus,
10
+ SpanEvent,
11
+ ReadonlySpan,
12
+ } from './core/span';
13
+ export { Tracer } from './core/tracer';
14
+ export { Meter, Counter, Histogram, Gauge } from './core/meter';
15
+ export { OtelLogger } from './core/log-record';
16
+ export type { LogSeverity } from './core/log-record';
17
+ export type { Attributes, AttributeValue } from './core/attributes';
18
+ export type { Resource } from './core/resource';
19
+
20
+ // Context
21
+ export { spanContext } from './context/span-context';
22
+
23
+ // Exporters
24
+ export type {
25
+ SpanExporter,
26
+ MetricExporter,
27
+ LogExporter,
28
+ MetricRecord,
29
+ LogRecord,
30
+ } from './exporters/types';
31
+ export {
32
+ ConsoleSpanExporter,
33
+ ConsoleMetricExporter,
34
+ ConsoleLogExporter,
35
+ } from './exporters/console-exporter';
36
+ export {
37
+ OtlpHttpExporter,
38
+ OtlpHttpMetricExporter,
39
+ OtlpHttpLogExporter,
40
+ } from './exporters/otlp-http-exporter';
41
+ export type {
42
+ OtlpHttpExporterOptions,
43
+ OtlpHttpMetricExporterOptions,
44
+ OtlpHttpLogExporterOptions,
45
+ } from './exporters/otlp-http-exporter';
46
+
47
+ // Instrumentation
48
+ export { createNavigationInstrumentation } from './instrumentation/navigation';
49
+ export type { NavigationInstrumentation } from './instrumentation/navigation';
50
+ export { createAxiosInstrumentation } from './instrumentation/network';
51
+ export type {
52
+ AxiosInstrumentation,
53
+ AxiosInstrumentationOptions,
54
+ AxiosRequestConfig as OtelAxiosRequestConfig,
55
+ AxiosResponse as OtelAxiosResponse,
56
+ } from './instrumentation/network';
57
+ export { installErrorInstrumentation } from './instrumentation/errors';
58
+ export type { StorageAdapter } from './instrumentation/errors';
59
+
60
+ // React
61
+ export { OtelProvider, OtelContext } from './react/OtelProvider';
62
+ export type { OtelContextValue, OtelProviderProps } from './react/OtelProvider';
63
+ export { useOtel } from './react/useOtel';
@@ -0,0 +1,95 @@
1
+ import {
2
+ ATTR_EXCEPTION_MESSAGE,
3
+ ATTR_EXCEPTION_STACKTRACE,
4
+ ATTR_EXCEPTION_TYPE,
5
+ } from '@opentelemetry/semantic-conventions';
6
+
7
+ import { Span } from '../core/span';
8
+ import { Tracer } from '../core/tracer';
9
+ import type { ReadonlySpan } from '../exporters/types';
10
+
11
+ const CRASH_KEY = '@react-native-otel/pending-crash';
12
+
13
+ export interface StorageAdapter {
14
+ setSync(key: string, value: string): void;
15
+ getSync(key: string): string | null;
16
+ deleteSync(key: string): void;
17
+ }
18
+
19
+ type GlobalErrorHandler = (error: Error, isFatal?: boolean) => void;
20
+
21
+ // Serialized crash span shape for storage
22
+ interface CrashSpanRecord {
23
+ traceId: string;
24
+ spanId: string;
25
+ name: string;
26
+ startTimeMs: number;
27
+ endTimeMs: number;
28
+ attributes: Record<string, unknown>;
29
+ events: {
30
+ name: string;
31
+ timestampMs: number;
32
+ attributes: Record<string, unknown>;
33
+ }[];
34
+ status: string;
35
+ statusMessage: string | undefined;
36
+ }
37
+
38
+ export function installErrorInstrumentation(params: {
39
+ tracer: Tracer;
40
+ storage?: StorageAdapter;
41
+ exporter?: { export(spans: ReadonlySpan[]): void };
42
+ }): void {
43
+ const { tracer, storage, exporter } = params;
44
+
45
+ // Flush any pending crash span from previous session
46
+ if (storage && exporter) {
47
+ const pending = storage.getSync(CRASH_KEY);
48
+ if (pending) {
49
+ try {
50
+ const crashRecord = JSON.parse(pending) as CrashSpanRecord;
51
+ exporter.export([crashRecord as unknown as ReadonlySpan]);
52
+ } catch {
53
+ // Ignore parse errors
54
+ }
55
+ storage.deleteSync(CRASH_KEY);
56
+ }
57
+ }
58
+
59
+ // Wrap the global JS error handler
60
+ const originalHandler = (
61
+ ErrorUtils as { getGlobalHandler?(): GlobalErrorHandler }
62
+ ).getGlobalHandler?.();
63
+
64
+ ErrorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => {
65
+ const span = tracer.startSpan(`crash.${error.name}`, {
66
+ kind: 'INTERNAL',
67
+ attributes: {
68
+ [ATTR_EXCEPTION_TYPE]: error.name,
69
+ [ATTR_EXCEPTION_MESSAGE]: error.message,
70
+ [ATTR_EXCEPTION_STACKTRACE]: error.stack ?? '',
71
+ 'crash.is_fatal': isFatal ?? false, // custom — no OTel equivalent
72
+ },
73
+ });
74
+ span.setStatus('ERROR', error.message);
75
+ span.end();
76
+
77
+ // Persist crash span synchronously for next session retrieval
78
+ if (isFatal && storage && span instanceof Span) {
79
+ const record: CrashSpanRecord = {
80
+ traceId: span.traceId,
81
+ spanId: span.spanId,
82
+ name: span.name,
83
+ startTimeMs: span.startTimeMs,
84
+ endTimeMs: span.endTimeMs ?? Date.now(),
85
+ attributes: span.attributes as Record<string, unknown>,
86
+ events: span.events,
87
+ status: span.status,
88
+ statusMessage: span.statusMessage,
89
+ };
90
+ storage.setSync(CRASH_KEY, JSON.stringify(record));
91
+ }
92
+
93
+ originalHandler?.(error, isFatal);
94
+ });
95
+ }
@@ -0,0 +1,17 @@
1
+ import type { AppStateStatus } from 'react-native';
2
+ import { AppState } from 'react-native';
3
+
4
+ import { spanContext } from '../context/span-context';
5
+ import { Meter } from '../core/meter';
6
+
7
+ export function installLifecycleInstrumentation(meter: Meter): void {
8
+ AppState.addEventListener('change', (state: AppStateStatus) => {
9
+ spanContext.current()?.addEvent(`app.lifecycle.${state}`, {
10
+ 'app.state': state,
11
+ });
12
+
13
+ if (state === 'background') {
14
+ meter.flush();
15
+ }
16
+ });
17
+ }
@@ -0,0 +1,61 @@
1
+ import { ATTR_APP_SCREEN_NAME } from '@opentelemetry/semantic-conventions/incubating';
2
+
3
+ import { spanContext } from '../context/span-context';
4
+ import { Span, NoopSpan } from '../core/span';
5
+ import { Tracer } from '../core/tracer';
6
+
7
+ // Keyed by React Navigation route key — handles modals + tabs coexisting
8
+ const screenSpans = new Map<string, Span | NoopSpan>();
9
+
10
+ export function createNavigationInstrumentation(tracer: Tracer) {
11
+ return {
12
+ onRouteChange(
13
+ currentName: string,
14
+ previousName: string | undefined,
15
+ currentKey: string,
16
+ previousKey: string | undefined,
17
+ params?: Record<string, unknown>
18
+ ): void {
19
+ // End previous screen span looked up by key (not stack pop)
20
+ if (previousKey) {
21
+ const prevSpan = screenSpans.get(previousKey);
22
+ if (prevSpan) {
23
+ prevSpan.end();
24
+ screenSpans.delete(previousKey);
25
+ }
26
+ }
27
+
28
+ // Start new screen span
29
+ const span = tracer.startSpan(`screen.${currentName}`, {
30
+ kind: 'INTERNAL',
31
+ attributes: {
32
+ [ATTR_APP_SCREEN_NAME]: currentName, // 'app.screen.name'
33
+ 'app.screen.previous_name': previousName ?? '', // custom
34
+ ...(params ? { 'app.screen.params': JSON.stringify(params) } : {}), // custom
35
+ },
36
+ });
37
+
38
+ screenSpans.set(currentKey, span);
39
+ spanContext.setCurrent(span);
40
+ },
41
+
42
+ endCurrentScreen(): void {
43
+ const current = spanContext.current();
44
+ if (current) {
45
+ current.end();
46
+ // Remove from map
47
+ for (const [key, span] of screenSpans.entries()) {
48
+ if (span === current) {
49
+ screenSpans.delete(key);
50
+ break;
51
+ }
52
+ }
53
+ spanContext.setCurrent(undefined);
54
+ }
55
+ },
56
+ };
57
+ }
58
+
59
+ export type NavigationInstrumentation = ReturnType<
60
+ typeof createNavigationInstrumentation
61
+ >;