react-native-otel 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +1137 -13
  2. package/lib/module/context/span-context.js +56 -5
  3. package/lib/module/context/span-context.js.map +1 -1
  4. package/lib/module/core/ids.js +21 -7
  5. package/lib/module/core/ids.js.map +1 -1
  6. package/lib/module/core/meter.js +101 -12
  7. package/lib/module/core/meter.js.map +1 -1
  8. package/lib/module/core/processor.js +28 -0
  9. package/lib/module/core/processor.js.map +1 -0
  10. package/lib/module/core/resource.js +1 -0
  11. package/lib/module/core/resource.js.map +1 -1
  12. package/lib/module/core/sampler.js +55 -0
  13. package/lib/module/core/sampler.js.map +1 -0
  14. package/lib/module/core/span.js +15 -1
  15. package/lib/module/core/span.js.map +1 -1
  16. package/lib/module/core/tracer.js +94 -5
  17. package/lib/module/core/tracer.js.map +1 -1
  18. package/lib/module/exporters/console-exporter.js +8 -1
  19. package/lib/module/exporters/console-exporter.js.map +1 -1
  20. package/lib/module/exporters/multi-exporter.js +57 -0
  21. package/lib/module/exporters/multi-exporter.js.map +1 -0
  22. package/lib/module/exporters/otlp-http-exporter.js +159 -25
  23. package/lib/module/exporters/otlp-http-exporter.js.map +1 -1
  24. package/lib/module/exporters/wal.js +129 -0
  25. package/lib/module/exporters/wal.js.map +1 -0
  26. package/lib/module/index.js +16 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/instrumentation/errors.js +21 -0
  29. package/lib/module/instrumentation/errors.js.map +1 -1
  30. package/lib/module/instrumentation/expo-router.js +76 -0
  31. package/lib/module/instrumentation/expo-router.js.map +1 -0
  32. package/lib/module/instrumentation/fetch.js +99 -0
  33. package/lib/module/instrumentation/fetch.js.map +1 -0
  34. package/lib/module/instrumentation/lifecycle.js +6 -1
  35. package/lib/module/instrumentation/lifecycle.js.map +1 -1
  36. package/lib/module/instrumentation/linking.js +65 -0
  37. package/lib/module/instrumentation/linking.js.map +1 -0
  38. package/lib/module/instrumentation/network.js +35 -0
  39. package/lib/module/instrumentation/network.js.map +1 -1
  40. package/lib/module/instrumentation/startup.js +46 -0
  41. package/lib/module/instrumentation/startup.js.map +1 -0
  42. package/lib/module/sdk.js +87 -5
  43. package/lib/module/sdk.js.map +1 -1
  44. package/lib/module/version.js +7 -0
  45. package/lib/module/version.js.map +1 -0
  46. package/lib/typescript/src/context/span-context.d.ts +14 -4
  47. package/lib/typescript/src/context/span-context.d.ts.map +1 -1
  48. package/lib/typescript/src/core/ids.d.ts.map +1 -1
  49. package/lib/typescript/src/core/meter.d.ts +12 -2
  50. package/lib/typescript/src/core/meter.d.ts.map +1 -1
  51. package/lib/typescript/src/core/processor.d.ts +29 -0
  52. package/lib/typescript/src/core/processor.d.ts.map +1 -0
  53. package/lib/typescript/src/core/resource.d.ts +3 -0
  54. package/lib/typescript/src/core/resource.d.ts.map +1 -1
  55. package/lib/typescript/src/core/sampler.d.ts +31 -0
  56. package/lib/typescript/src/core/sampler.d.ts.map +1 -0
  57. package/lib/typescript/src/core/span.d.ts +16 -0
  58. package/lib/typescript/src/core/span.d.ts.map +1 -1
  59. package/lib/typescript/src/core/tracer.d.ts +16 -6
  60. package/lib/typescript/src/core/tracer.d.ts.map +1 -1
  61. package/lib/typescript/src/exporters/console-exporter.d.ts.map +1 -1
  62. package/lib/typescript/src/exporters/multi-exporter.d.ts +28 -0
  63. package/lib/typescript/src/exporters/multi-exporter.d.ts.map +1 -0
  64. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +20 -2
  65. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -1
  66. package/lib/typescript/src/exporters/types.d.ts +17 -3
  67. package/lib/typescript/src/exporters/types.d.ts.map +1 -1
  68. package/lib/typescript/src/exporters/wal.d.ts +21 -0
  69. package/lib/typescript/src/exporters/wal.d.ts.map +1 -0
  70. package/lib/typescript/src/index.d.ts +15 -2
  71. package/lib/typescript/src/index.d.ts.map +1 -1
  72. package/lib/typescript/src/instrumentation/errors.d.ts.map +1 -1
  73. package/lib/typescript/src/instrumentation/expo-router.d.ts +31 -0
  74. package/lib/typescript/src/instrumentation/expo-router.d.ts.map +1 -0
  75. package/lib/typescript/src/instrumentation/fetch.d.ts +18 -0
  76. package/lib/typescript/src/instrumentation/fetch.d.ts.map +1 -0
  77. package/lib/typescript/src/instrumentation/lifecycle.d.ts.map +1 -1
  78. package/lib/typescript/src/instrumentation/linking.d.ts +23 -0
  79. package/lib/typescript/src/instrumentation/linking.d.ts.map +1 -0
  80. package/lib/typescript/src/instrumentation/network.d.ts.map +1 -1
  81. package/lib/typescript/src/instrumentation/startup.d.ts +16 -0
  82. package/lib/typescript/src/instrumentation/startup.d.ts.map +1 -0
  83. package/lib/typescript/src/sdk.d.ts +35 -0
  84. package/lib/typescript/src/sdk.d.ts.map +1 -1
  85. package/lib/typescript/src/version.d.ts +2 -0
  86. package/lib/typescript/src/version.d.ts.map +1 -0
  87. package/package.json +12 -2
  88. package/src/context/span-context.ts +61 -8
  89. package/src/core/ids.ts +21 -7
  90. package/src/core/meter.ts +136 -14
  91. package/src/core/processor.ts +33 -0
  92. package/src/core/resource.ts +6 -0
  93. package/src/core/sampler.ts +65 -0
  94. package/src/core/span.ts +28 -1
  95. package/src/core/tracer.ts +140 -19
  96. package/src/exporters/console-exporter.ts +18 -4
  97. package/src/exporters/multi-exporter.ts +59 -0
  98. package/src/exporters/otlp-http-exporter.ts +191 -29
  99. package/src/exporters/types.ts +24 -3
  100. package/src/exporters/wal.ts +145 -0
  101. package/src/index.ts +36 -1
  102. package/src/instrumentation/errors.ts +27 -0
  103. package/src/instrumentation/expo-router.ts +94 -0
  104. package/src/instrumentation/fetch.ts +134 -0
  105. package/src/instrumentation/lifecycle.ts +7 -1
  106. package/src/instrumentation/linking.ts +83 -0
  107. package/src/instrumentation/network.ts +39 -0
  108. package/src/instrumentation/startup.ts +49 -0
  109. package/src/sdk.ts +115 -4
  110. package/src/version.ts +6 -0
@@ -3,10 +3,16 @@ import type {
3
3
  LogRecord,
4
4
  MetricExporter,
5
5
  MetricRecord,
6
+ CounterRecord,
7
+ HistogramRecord,
8
+ GaugeRecord,
6
9
  } from './types';
7
10
  import type { Attributes } from '../core/attributes';
8
11
  import type { Resource } from '../core/resource';
9
12
  import type { ReadonlySpan, SpanExporter } from '../core/span';
13
+ import type { StorageAdapter } from '../instrumentation/errors';
14
+ import { Wal, fetchWithRetry } from './wal';
15
+ import { SDK_VERSION } from '../version';
10
16
 
11
17
  // ─── OTLP attribute value serialization ──────────────────────────────────────
12
18
 
@@ -79,6 +85,7 @@ export class OtlpHttpExporter implements SpanExporter {
79
85
  private buffer: ReadonlySpan[] = [];
80
86
  private resource_: Readonly<Resource> | undefined;
81
87
  private timer_: ReturnType<typeof setInterval> | undefined;
88
+ private wal_: Wal<ReadonlySpan> | undefined;
82
89
 
83
90
  constructor(options: OtlpHttpExporterOptions) {
84
91
  this.tracesEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/traces';
@@ -92,6 +99,8 @@ export class OtlpHttpExporter implements SpanExporter {
92
99
  this.timer_ = setInterval(() => {
93
100
  this.flush();
94
101
  }, interval);
102
+ // Allow Node.js to exit cleanly in test environments without calling destroy().
103
+ if (typeof this.timer_.unref === 'function') this.timer_.unref();
95
104
  }
96
105
 
97
106
  // Called by OtelSDK.init() after buildResource() — not part of SpanExporter.
@@ -99,6 +108,20 @@ export class OtlpHttpExporter implements SpanExporter {
99
108
  this.resource_ = resource;
100
109
  }
101
110
 
111
+ // Called by OtelSDK.init() when a StorageAdapter is configured.
112
+ // Initialises the WAL and replays any undelivered batches from the previous session.
113
+ setStorage(storage: StorageAdapter): void {
114
+ this.wal_ = new Wal<ReadonlySpan>(storage, '@react-native-otel/wal/spans');
115
+ this.replayWal();
116
+ }
117
+
118
+ private replayWal(): void {
119
+ if (!this.wal_) return;
120
+ for (const batch of this.wal_.readAll()) {
121
+ this.deliverBatch(batch.data as ReadonlySpan[], batch.id);
122
+ }
123
+ }
124
+
102
125
  export(spans: ReadonlySpan[]): void {
103
126
  this.buffer.push(...spans);
104
127
  if (this.buffer.length >= this.batchSize) {
@@ -109,7 +132,12 @@ export class OtlpHttpExporter implements SpanExporter {
109
132
  flush(): void {
110
133
  if (this.buffer.length === 0) return;
111
134
  const batch = this.buffer.splice(0);
112
- this.send(batch);
135
+ if (this.wal_) {
136
+ const id = this.wal_.write(batch);
137
+ this.deliverBatch(batch, id);
138
+ } else {
139
+ this.deliverBatch(batch, undefined);
140
+ }
113
141
  }
114
142
 
115
143
  // Clear the flush timer and send any remaining buffered spans.
@@ -121,30 +149,41 @@ export class OtlpHttpExporter implements SpanExporter {
121
149
  this.flush();
122
150
  }
123
151
 
124
- private send(spans: ReadonlySpan[]): void {
152
+ private deliverBatch(spans: ReadonlySpan[], walId: string | undefined): void {
153
+ const body = this.buildBody(spans);
154
+ fetchWithRetry(this.tracesEndpoint, {
155
+ method: 'POST',
156
+ headers: this.headers,
157
+ body,
158
+ })
159
+ .then((success) => {
160
+ if (success && walId !== undefined) {
161
+ this.wal_?.delete(walId);
162
+ }
163
+ })
164
+ .catch(() => {
165
+ // Leave in WAL for next session
166
+ });
167
+ }
168
+
169
+ private buildBody(spans: ReadonlySpan[]): string {
125
170
  const resourceAttrs = this.resource_
126
171
  ? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
127
172
  : [];
128
173
 
129
- const body = JSON.stringify({
174
+ return JSON.stringify({
130
175
  resourceSpans: [
131
176
  {
132
177
  resource: { attributes: resourceAttrs },
133
178
  scopeSpans: [
134
179
  {
135
- scope: { name: 'react-native-otel', version: '0.1.0' },
180
+ scope: { name: 'react-native-otel', version: SDK_VERSION },
136
181
  spans: spans.map((s) => this.toOtlpSpan(s)),
137
182
  },
138
183
  ],
139
184
  },
140
185
  ],
141
186
  });
142
-
143
- fetch(this.tracesEndpoint, {
144
- method: 'POST',
145
- headers: this.headers,
146
- body,
147
- }).catch(() => {});
148
187
  }
149
188
 
150
189
  private toOtlpSpan(span: ReadonlySpan) {
@@ -163,6 +202,11 @@ export class OtlpHttpExporter implements SpanExporter {
163
202
  timeUnixNano: msToNano(event.timestampMs),
164
203
  attributes: toOtlpAttributes(event.attributes),
165
204
  })),
205
+ links: span.links.map((link) => ({
206
+ traceId: link.traceId,
207
+ spanId: link.spanId,
208
+ attributes: link.attributes ? toOtlpAttributes(link.attributes) : [],
209
+ })),
166
210
  droppedEventsCount: span.droppedEventsCount,
167
211
  status: {
168
212
  code: SPAN_STATUS_CODE[span.status] ?? 0,
@@ -175,18 +219,24 @@ export class OtlpHttpExporter implements SpanExporter {
175
219
 
176
220
  // ─── Metric exporter ──────────────────────────────────────────────────────────
177
221
 
178
- // OTLP aggregation temporality: 2 = CUMULATIVE
222
+ // OTLP aggregation temporality constants
223
+ const AGGREGATION_TEMPORALITY_DELTA = 1;
179
224
  const AGGREGATION_TEMPORALITY_CUMULATIVE = 2;
180
225
 
181
226
  export interface OtlpHttpMetricExporterOptions {
182
227
  endpoint: string;
183
228
  headers?: Record<string, string>;
229
+ // How often to auto-flush buffered metrics in ms. Default: 30_000.
230
+ flushIntervalMs?: number;
184
231
  }
185
232
 
186
233
  export class OtlpHttpMetricExporter implements MetricExporter {
187
234
  private readonly metricsEndpoint: string;
188
235
  private readonly headers: Record<string, string>;
189
236
  private resource_: Readonly<Resource> | undefined;
237
+ private wal_: Wal<MetricRecord> | undefined;
238
+ private timer_: ReturnType<typeof setInterval> | undefined;
239
+ private buffer_: MetricRecord[] = [];
190
240
 
191
241
  constructor(options: OtlpHttpMetricExporterOptions) {
192
242
  this.metricsEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/metrics';
@@ -194,18 +244,82 @@ export class OtlpHttpMetricExporter implements MetricExporter {
194
244
  'Content-Type': 'application/json',
195
245
  ...options.headers,
196
246
  };
247
+
248
+ const interval = options.flushIntervalMs ?? 30_000;
249
+ this.timer_ = setInterval(() => {
250
+ this.flush();
251
+ }, interval);
252
+ // Allow Node.js to exit cleanly in test environments without calling destroy().
253
+ if (typeof this.timer_.unref === 'function') this.timer_.unref();
197
254
  }
198
255
 
199
256
  setResource(resource: Readonly<Resource>): void {
200
257
  this.resource_ = resource;
201
258
  }
202
259
 
260
+ setStorage(storage: StorageAdapter): void {
261
+ this.wal_ = new Wal<MetricRecord>(
262
+ storage,
263
+ '@react-native-otel/wal/metrics'
264
+ );
265
+ this.replayWal();
266
+ }
267
+
268
+ private replayWal(): void {
269
+ if (!this.wal_) return;
270
+ for (const batch of this.wal_.readAll()) {
271
+ this.deliverBatch(batch.data as MetricRecord[], batch.id);
272
+ }
273
+ }
274
+
203
275
  export(metrics: MetricRecord[]): void {
204
276
  if (metrics.length === 0) return;
205
- this.send(metrics);
277
+ this.buffer_.push(...metrics);
278
+ }
279
+
280
+ // Drain buffered metrics and deliver them immediately.
281
+ // Called by the internal timer and also useful for testing / explicit flush.
282
+ flush(): void {
283
+ if (this.buffer_.length === 0) return;
284
+ const batch = this.buffer_.splice(0);
285
+ if (this.wal_) {
286
+ const id = this.wal_.write(batch);
287
+ this.deliverBatch(batch, id);
288
+ } else {
289
+ this.deliverBatch(batch, undefined);
290
+ }
291
+ }
292
+
293
+ // Clear the flush timer and send any remaining buffered metrics.
294
+ destroy(): void {
295
+ if (this.timer_ !== undefined) {
296
+ clearInterval(this.timer_);
297
+ this.timer_ = undefined;
298
+ }
299
+ this.flush();
300
+ }
301
+
302
+ private deliverBatch(
303
+ metrics: MetricRecord[],
304
+ walId: string | undefined
305
+ ): void {
306
+ const body = this.buildBody(metrics);
307
+ fetchWithRetry(this.metricsEndpoint, {
308
+ method: 'POST',
309
+ headers: this.headers,
310
+ body,
311
+ })
312
+ .then((success) => {
313
+ if (success && walId !== undefined) {
314
+ this.wal_?.delete(walId);
315
+ }
316
+ })
317
+ .catch(() => {
318
+ // Leave in WAL for next session
319
+ });
206
320
  }
207
321
 
208
- private send(metrics: MetricRecord[]): void {
322
+ private buildBody(metrics: MetricRecord[]): string {
209
323
  const resourceAttrs = this.resource_
210
324
  ? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
211
325
  : [];
@@ -224,12 +338,11 @@ export class OtlpHttpMetricExporter implements MetricExporter {
224
338
  const otlpMetrics = Array.from(byName.entries()).map(([name, records]) => {
225
339
  const type = records[0]?.type;
226
340
 
227
- // Counters → sum; histograms + gauges → gauge (no bucket data available).
228
341
  if (type === 'counter') {
229
342
  return {
230
343
  name,
231
344
  sum: {
232
- dataPoints: records.map((r) => ({
345
+ dataPoints: (records as CounterRecord[]).map((r) => ({
233
346
  asDouble: r.value,
234
347
  startTimeUnixNano: msToNano(r.timestampMs),
235
348
  timeUnixNano: msToNano(r.timestampMs),
@@ -241,10 +354,32 @@ export class OtlpHttpMetricExporter implements MetricExporter {
241
354
  };
242
355
  }
243
356
 
357
+ if (type === 'histogram') {
358
+ return {
359
+ name,
360
+ histogram: {
361
+ dataPoints: (records as HistogramRecord[]).map((r) => ({
362
+ count: String(r.count),
363
+ sum: r.sum,
364
+ // bucketCounts in OTLP are string-encoded uint64
365
+ bucketCounts: r.bucketCounts.map(String),
366
+ // explicitBounds does not include the implicit +Inf upper bound
367
+ explicitBounds: r.bucketBoundaries,
368
+ startTimeUnixNano: msToNano(r.timestampMs),
369
+ timeUnixNano: msToNano(r.timestampMs),
370
+ attributes: toOtlpAttributes(r.attributes),
371
+ })),
372
+ // Each flush window is independent — use DELTA semantics.
373
+ aggregationTemporality: AGGREGATION_TEMPORALITY_DELTA,
374
+ },
375
+ };
376
+ }
377
+
378
+ // gauge
244
379
  return {
245
380
  name,
246
381
  gauge: {
247
- dataPoints: records.map((r) => ({
382
+ dataPoints: (records as GaugeRecord[]).map((r) => ({
248
383
  asDouble: r.value,
249
384
  timeUnixNano: msToNano(r.timestampMs),
250
385
  attributes: toOtlpAttributes(r.attributes),
@@ -253,25 +388,19 @@ export class OtlpHttpMetricExporter implements MetricExporter {
253
388
  };
254
389
  });
255
390
 
256
- const body = JSON.stringify({
391
+ return JSON.stringify({
257
392
  resourceMetrics: [
258
393
  {
259
394
  resource: { attributes: resourceAttrs },
260
395
  scopeMetrics: [
261
396
  {
262
- scope: { name: 'react-native-otel', version: '0.1.0' },
397
+ scope: { name: 'react-native-otel', version: SDK_VERSION },
263
398
  metrics: otlpMetrics,
264
399
  },
265
400
  ],
266
401
  },
267
402
  ],
268
403
  });
269
-
270
- fetch(this.metricsEndpoint, {
271
- method: 'POST',
272
- headers: this.headers,
273
- body,
274
- }).catch(() => {});
275
404
  }
276
405
  }
277
406
 
@@ -301,6 +430,7 @@ export class OtlpHttpLogExporter implements LogExporter {
301
430
  private buffer: LogRecord[] = [];
302
431
  private resource_: Readonly<Resource> | undefined;
303
432
  private timer_: ReturnType<typeof setInterval> | undefined;
433
+ private wal_: Wal<LogRecord> | undefined;
304
434
 
305
435
  constructor(options: OtlpHttpLogExporterOptions) {
306
436
  this.logsEndpoint = options.endpoint.replace(/\/$/, '') + '/v1/logs';
@@ -314,12 +444,27 @@ export class OtlpHttpLogExporter implements LogExporter {
314
444
  this.timer_ = setInterval(() => {
315
445
  this.flush();
316
446
  }, interval);
447
+ // Allow Node.js to exit cleanly in test environments without calling destroy().
448
+ if (typeof this.timer_.unref === 'function') this.timer_.unref();
317
449
  }
318
450
 
319
451
  setResource(resource: Readonly<Resource>): void {
320
452
  this.resource_ = resource;
321
453
  }
322
454
 
455
+ // Called by OtelSDK.init() when a StorageAdapter is configured.
456
+ setStorage(storage: StorageAdapter): void {
457
+ this.wal_ = new Wal<LogRecord>(storage, '@react-native-otel/wal/logs');
458
+ this.replayWal();
459
+ }
460
+
461
+ private replayWal(): void {
462
+ if (!this.wal_) return;
463
+ for (const batch of this.wal_.readAll()) {
464
+ this.deliverBatch(batch.data as LogRecord[], batch.id);
465
+ }
466
+ }
467
+
323
468
  export(logs: LogRecord[]): void {
324
469
  this.buffer.push(...logs);
325
470
  if (this.buffer.length >= this.batchSize) {
@@ -330,7 +475,12 @@ export class OtlpHttpLogExporter implements LogExporter {
330
475
  flush(): void {
331
476
  if (this.buffer.length === 0) return;
332
477
  const batch = this.buffer.splice(0);
333
- this.send(batch);
478
+ if (this.wal_) {
479
+ const id = this.wal_.write(batch);
480
+ this.deliverBatch(batch, id);
481
+ } else {
482
+ this.deliverBatch(batch, undefined);
483
+ }
334
484
  }
335
485
 
336
486
  destroy(): void {
@@ -341,7 +491,19 @@ export class OtlpHttpLogExporter implements LogExporter {
341
491
  this.flush();
342
492
  }
343
493
 
344
- private send(logs: LogRecord[]): void {
494
+ private deliverBatch(logs: LogRecord[], walId: string | undefined): void {
495
+ this.send(logs)
496
+ .then((success) => {
497
+ if (success && walId !== undefined) {
498
+ this.wal_?.delete(walId);
499
+ }
500
+ })
501
+ .catch(() => {
502
+ // Leave in WAL for next session
503
+ });
504
+ }
505
+
506
+ private send(logs: LogRecord[]): Promise<boolean> {
345
507
  const resourceAttrs = this.resource_
346
508
  ? toOtlpAttributes(this.resource_ as unknown as Record<string, unknown>)
347
509
  : [];
@@ -352,7 +514,7 @@ export class OtlpHttpLogExporter implements LogExporter {
352
514
  resource: { attributes: resourceAttrs },
353
515
  scopeLogs: [
354
516
  {
355
- scope: { name: 'react-native-otel', version: '0.1.0' },
517
+ scope: { name: 'react-native-otel', version: SDK_VERSION },
356
518
  logRecords: logs.map((log) => ({
357
519
  timeUnixNano: msToNano(log.timestampMs),
358
520
  severityNumber: LOG_SEVERITY_NUMBER[log.severity] ?? 9,
@@ -368,10 +530,10 @@ export class OtlpHttpLogExporter implements LogExporter {
368
530
  ],
369
531
  });
370
532
 
371
- fetch(this.logsEndpoint, {
533
+ return fetchWithRetry(this.logsEndpoint, {
372
534
  method: 'POST',
373
535
  headers: this.headers,
374
536
  body,
375
- }).catch(() => {});
537
+ });
376
538
  }
377
539
  }
@@ -3,14 +3,35 @@ import type { ReadonlySpan, SpanExporter } from '../core/span';
3
3
 
4
4
  export type { ReadonlySpan, SpanExporter };
5
5
 
6
- export interface MetricRecord {
7
- type: 'counter' | 'histogram' | 'gauge';
6
+ interface BaseMetricRecord {
8
7
  name: string;
9
- value: number;
10
8
  timestampMs: number;
11
9
  attributes: Attributes;
12
10
  }
13
11
 
12
+ export interface CounterRecord extends BaseMetricRecord {
13
+ type: 'counter';
14
+ value: number;
15
+ }
16
+
17
+ export interface GaugeRecord extends BaseMetricRecord {
18
+ type: 'gauge';
19
+ value: number;
20
+ }
21
+
22
+ export interface HistogramRecord extends BaseMetricRecord {
23
+ type: 'histogram';
24
+ // Aggregated data for the flush window
25
+ count: number;
26
+ sum: number;
27
+ // Explicit bucket upper bounds (last bucket is +Inf, implicit)
28
+ bucketBoundaries: number[];
29
+ // Length is bucketBoundaries.length + 1 (last entry = +Inf bucket)
30
+ bucketCounts: number[];
31
+ }
32
+
33
+ export type MetricRecord = CounterRecord | GaugeRecord | HistogramRecord;
34
+
14
35
  export interface LogRecord {
15
36
  timestampMs: number;
16
37
  severity: string;
@@ -0,0 +1,145 @@
1
+ import type { StorageAdapter } from '../instrumentation/errors';
2
+
3
+ const MAX_BATCHES = 3;
4
+ const MAX_ITEMS_PER_BATCH = 500;
5
+
6
+ // Circuit breaker: after this many consecutive delivery failures, pause for
7
+ // CIRCUIT_OPEN_MS before attempting again.
8
+ const CIRCUIT_BREAKER_THRESHOLD = 5;
9
+ const CIRCUIT_OPEN_MS = 60_000;
10
+
11
+ interface WalBatch<T> {
12
+ id: string;
13
+ timestamp: number;
14
+ data: T[];
15
+ }
16
+
17
+ // Write-ahead log backed by StorageAdapter.
18
+ // Persists undelivered export batches so they survive force-kills.
19
+ // Stores at most maxBatches batches; oldest are dropped when the limit is exceeded.
20
+ export class Wal<T> {
21
+ constructor(
22
+ private readonly storage: StorageAdapter,
23
+ private readonly storageKey: string,
24
+ private readonly maxBatches = MAX_BATCHES,
25
+ private readonly maxItems = MAX_ITEMS_PER_BATCH
26
+ ) {}
27
+
28
+ // Persist a batch and return the id needed to delete it after delivery.
29
+ write(items: T[]): string {
30
+ const batches = this.readAll();
31
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
32
+ batches.push({
33
+ id,
34
+ timestamp: Date.now(),
35
+ data: items.slice(0, this.maxItems),
36
+ });
37
+ // Keep only the most recent batches to cap storage growth.
38
+ const trimmed = batches.slice(-this.maxBatches);
39
+ this.storage.setSync(this.storageKey, JSON.stringify(trimmed));
40
+ return id;
41
+ }
42
+
43
+ // Remove a successfully delivered batch from the WAL.
44
+ delete(id: string): void {
45
+ const remaining = this.readAll().filter((b) => b.id !== id);
46
+ if (remaining.length === 0) {
47
+ this.storage.deleteSync(this.storageKey);
48
+ } else {
49
+ this.storage.setSync(this.storageKey, JSON.stringify(remaining));
50
+ }
51
+ }
52
+
53
+ // Return all pending batches (for session-start replay).
54
+ readAll(): WalBatch<T>[] {
55
+ const raw = this.storage.getSync(this.storageKey);
56
+ if (!raw) return [];
57
+ try {
58
+ return JSON.parse(raw) as WalBatch<T>[];
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+ }
64
+
65
+ // Per-endpoint circuit-breaker state.
66
+ // Key: URL string. Stores consecutive failure count and open-until timestamp.
67
+ interface CircuitState {
68
+ failures: number;
69
+ openUntil: number;
70
+ }
71
+ const circuitMap = new Map<string, CircuitState>();
72
+
73
+ function getCircuit(url: string): CircuitState {
74
+ let state = circuitMap.get(url);
75
+ if (!state) {
76
+ state = { failures: 0, openUntil: 0 };
77
+ circuitMap.set(url, state);
78
+ }
79
+ return state;
80
+ }
81
+
82
+ // The fetch implementation used for all OTLP delivery.
83
+ // Overridden by the SDK before installing fetch instrumentation so that
84
+ // exporter calls always use the original fetch — preventing infinite recursion.
85
+ let fetchImpl: typeof fetch | undefined;
86
+
87
+ export function setFetchImpl(impl: typeof fetch): void {
88
+ fetchImpl = impl;
89
+ }
90
+
91
+ // Retry a fetch up to maxRetries times with exponential backoff + jitter.
92
+ // Returns true on success, false if all retries are exhausted.
93
+ // 4xx responses are not retried (they indicate a client-side problem).
94
+ // After CIRCUIT_BREAKER_THRESHOLD consecutive failures the circuit opens for
95
+ // CIRCUIT_OPEN_MS and all attempts are skipped until it closes.
96
+ export async function fetchWithRetry(
97
+ url: string,
98
+ options: RequestInit,
99
+ maxRetries = 3,
100
+ baseDelayMs = 500
101
+ ): Promise<boolean> {
102
+ const circuit = getCircuit(url);
103
+
104
+ // Circuit open — bail out immediately without burning retries.
105
+ if (circuit.openUntil > Date.now()) {
106
+ return false;
107
+ }
108
+
109
+ // Use the override if set (pre-patch fetch), otherwise fall back to global.
110
+ const doFetch = fetchImpl ?? globalThis.fetch;
111
+
112
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
113
+ try {
114
+ const response = await doFetch(url, options);
115
+ if (response.ok || (response.status >= 400 && response.status < 500)) {
116
+ // Reset circuit on success.
117
+ circuit.failures = 0;
118
+ circuit.openUntil = 0;
119
+ return true;
120
+ }
121
+ // 5xx — fall through to retry
122
+ } catch {
123
+ // Network error — fall through to retry
124
+ }
125
+ if (attempt < maxRetries - 1) {
126
+ // Jitter: scale by 0.5–1.0 to spread out retries under load.
127
+ const jitter = 0.5 + Math.random() * 0.5;
128
+ await new Promise<void>((r) =>
129
+ setTimeout(r, baseDelayMs * Math.pow(2, attempt) * jitter)
130
+ );
131
+ }
132
+ }
133
+
134
+ // All retries exhausted — update circuit breaker.
135
+ circuit.failures += 1;
136
+ if (circuit.failures >= CIRCUIT_BREAKER_THRESHOLD) {
137
+ circuit.openUntil = Date.now() + CIRCUIT_OPEN_MS;
138
+ }
139
+ return false;
140
+ }
141
+
142
+ // Reset circuit breaker state for a URL (useful in tests).
143
+ export function resetCircuit(url: string): void {
144
+ circuitMap.delete(url);
145
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // SDK
2
2
  export { otel } from './sdk';
3
- export type { OtelConfig } from './sdk';
3
+ export type { OtelConfig, NetworkAdapter } from './sdk';
4
4
 
5
5
  // Core
6
6
  export { Span, NoopSpan } from './core/span';
@@ -8,17 +8,36 @@ export type {
8
8
  SpanKind,
9
9
  SpanStatus,
10
10
  SpanEvent,
11
+ SpanLink,
11
12
  ReadonlySpan,
13
+ SpanProcessor,
12
14
  } from './core/span';
13
15
  export { Tracer } from './core/tracer';
16
+ export type { SpanOptions } from './core/tracer';
14
17
  export { Meter, Counter, Histogram, Gauge } from './core/meter';
18
+ export type { HistogramOptions } from './core/meter';
15
19
  export { OtelLogger } from './core/log-record';
16
20
  export type { LogSeverity } from './core/log-record';
17
21
  export type { Attributes, AttributeValue } from './core/attributes';
18
22
  export type { Resource } from './core/resource';
19
23
 
24
+ // Samplers
25
+ export type { Sampler } from './core/sampler';
26
+ export {
27
+ AlwaysOnSampler,
28
+ AlwaysOffSampler,
29
+ TraceIdRatioSampler,
30
+ } from './core/sampler';
31
+
32
+ // Processors
33
+ export { SimpleSpanProcessor, NoopSpanProcessor } from './core/processor';
34
+
35
+ // Version
36
+ export { SDK_VERSION } from './version';
37
+
20
38
  // Context
21
39
  export { spanContext } from './context/span-context';
40
+ export type { SpanContextManagerPublic } from './context/span-context';
22
41
 
23
42
  // Exporters
24
43
  export type {
@@ -43,6 +62,11 @@ export type {
43
62
  OtlpHttpMetricExporterOptions,
44
63
  OtlpHttpLogExporterOptions,
45
64
  } from './exporters/otlp-http-exporter';
65
+ export {
66
+ MultiSpanExporter,
67
+ MultiMetricExporter,
68
+ MultiLogExporter,
69
+ } from './exporters/multi-exporter';
46
70
 
47
71
  // Instrumentation
48
72
  export { createNavigationInstrumentation } from './instrumentation/navigation';
@@ -56,6 +80,17 @@ export type {
56
80
  } from './instrumentation/network';
57
81
  export { installErrorInstrumentation } from './instrumentation/errors';
58
82
  export type { StorageAdapter } from './instrumentation/errors';
83
+ export {
84
+ createFetchInstrumentation,
85
+ uninstallFetchInstrumentation,
86
+ } from './instrumentation/fetch';
87
+ export type { FetchInstrumentationOptions } from './instrumentation/fetch';
88
+ export { installStartupInstrumentation } from './instrumentation/startup';
89
+ export {
90
+ createLinkingInstrumentation,
91
+ recordPushNotification,
92
+ } from './instrumentation/linking';
93
+ export type { LinkingInstrumentation } from './instrumentation/linking';
59
94
 
60
95
  // React
61
96
  export { OtelProvider, OtelContext } from './react/OtelProvider';
@@ -92,4 +92,31 @@ export function installErrorInstrumentation(params: {
92
92
 
93
93
  originalHandler?.(error, isFatal);
94
94
  });
95
+
96
+ // Wire up unhandled Promise rejection tracking.
97
+ // globalThis.onunhandledrejection is available in Hermes (default RN engine since 0.70).
98
+ // Without this, async errors that are never .catch()-ed are silently swallowed.
99
+ const prevRejectionHandler = (globalThis as Record<string, unknown>)
100
+ .onunhandledrejection as ((event: { reason: unknown }) => void) | undefined;
101
+
102
+ (globalThis as Record<string, unknown>).onunhandledrejection = (event: {
103
+ reason: unknown;
104
+ }) => {
105
+ const reason = event.reason;
106
+ const error = reason instanceof Error ? reason : new Error(String(reason));
107
+
108
+ const span = tracer.startSpan(`unhandled_rejection.${error.name}`, {
109
+ kind: 'INTERNAL',
110
+ attributes: {
111
+ [ATTR_EXCEPTION_TYPE]: error.name,
112
+ [ATTR_EXCEPTION_MESSAGE]: error.message,
113
+ [ATTR_EXCEPTION_STACKTRACE]: error.stack ?? '',
114
+ 'exception.unhandled_rejection': true,
115
+ },
116
+ });
117
+ span.setStatus('ERROR', error.message);
118
+ span.end();
119
+
120
+ prevRejectionHandler?.call(globalThis, event);
121
+ };
95
122
  }