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.
- package/LICENSE +20 -0
- package/README.md +37 -0
- package/lib/module/context/span-context.js +14 -0
- package/lib/module/context/span-context.js.map +1 -0
- package/lib/module/core/attributes.js +43 -0
- package/lib/module/core/attributes.js.map +1 -0
- package/lib/module/core/clock.js +8 -0
- package/lib/module/core/clock.js.map +1 -0
- package/lib/module/core/ids.js +16 -0
- package/lib/module/core/ids.js.map +1 -0
- package/lib/module/core/log-record.js +41 -0
- package/lib/module/core/log-record.js.map +1 -0
- package/lib/module/core/meter.js +70 -0
- package/lib/module/core/meter.js.map +1 -0
- package/lib/module/core/resource.js +20 -0
- package/lib/module/core/resource.js.map +1 -0
- package/lib/module/core/span.js +96 -0
- package/lib/module/core/span.js.map +1 -0
- package/lib/module/core/tracer.js +48 -0
- package/lib/module/core/tracer.js.map +1 -0
- package/lib/module/exporters/console-exporter.js +53 -0
- package/lib/module/exporters/console-exporter.js.map +1 -0
- package/lib/module/exporters/otlp-http-exporter.js +317 -0
- package/lib/module/exporters/otlp-http-exporter.js.map +1 -0
- package/lib/module/exporters/types.js +4 -0
- package/lib/module/exporters/types.js.map +1 -0
- package/lib/module/index.js +24 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/instrumentation/errors.js +63 -0
- package/lib/module/instrumentation/errors.js.map +1 -0
- package/lib/module/instrumentation/lifecycle.js +15 -0
- package/lib/module/instrumentation/lifecycle.js.map +1 -0
- package/lib/module/instrumentation/navigation.js +51 -0
- package/lib/module/instrumentation/navigation.js.map +1 -0
- package/lib/module/instrumentation/network.js +183 -0
- package/lib/module/instrumentation/network.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react/OtelProvider.js +57 -0
- package/lib/module/react/OtelProvider.js.map +1 -0
- package/lib/module/react/useOtel.js +12 -0
- package/lib/module/react/useOtel.js.map +1 -0
- package/lib/module/sdk.js +127 -0
- package/lib/module/sdk.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/context/span-context.d.ts +9 -0
- package/lib/typescript/src/context/span-context.d.ts.map +1 -0
- package/lib/typescript/src/core/attributes.d.ts +6 -0
- package/lib/typescript/src/core/attributes.d.ts.map +1 -0
- package/lib/typescript/src/core/clock.d.ts +2 -0
- package/lib/typescript/src/core/clock.d.ts.map +1 -0
- package/lib/typescript/src/core/ids.d.ts +3 -0
- package/lib/typescript/src/core/ids.d.ts.map +1 -0
- package/lib/typescript/src/core/log-record.d.ts +15 -0
- package/lib/typescript/src/core/log-record.d.ts.map +1 -0
- package/lib/typescript/src/core/meter.d.ts +30 -0
- package/lib/typescript/src/core/meter.d.ts.map +1 -0
- package/lib/typescript/src/core/resource.d.ts +25 -0
- package/lib/typescript/src/core/resource.d.ts.map +1 -0
- package/lib/typescript/src/core/span.d.ts +77 -0
- package/lib/typescript/src/core/span.d.ts.map +1 -0
- package/lib/typescript/src/core/tracer.d.ts +21 -0
- package/lib/typescript/src/core/tracer.d.ts.map +1 -0
- package/lib/typescript/src/exporters/console-exporter.d.ts +17 -0
- package/lib/typescript/src/exporters/console-exporter.d.ts.map +1 -0
- package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +58 -0
- package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -0
- package/lib/typescript/src/exporters/types.d.ts +25 -0
- package/lib/typescript/src/exporters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +25 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/errors.d.ts +15 -0
- package/lib/typescript/src/instrumentation/errors.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/lifecycle.d.ts +3 -0
- package/lib/typescript/src/instrumentation/lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/navigation.d.ts +7 -0
- package/lib/typescript/src/instrumentation/navigation.d.ts.map +1 -0
- package/lib/typescript/src/instrumentation/network.d.ts +33 -0
- package/lib/typescript/src/instrumentation/network.d.ts.map +1 -0
- package/lib/typescript/src/react/OtelProvider.d.ts +21 -0
- package/lib/typescript/src/react/OtelProvider.d.ts.map +1 -0
- package/lib/typescript/src/react/useOtel.d.ts +3 -0
- package/lib/typescript/src/react/useOtel.d.ts.map +1 -0
- package/lib/typescript/src/sdk.d.ts +50 -0
- package/lib/typescript/src/sdk.d.ts.map +1 -0
- package/package.json +125 -0
- package/src/context/span-context.ts +17 -0
- package/src/core/attributes.ts +61 -0
- package/src/core/clock.ts +5 -0
- package/src/core/ids.ts +15 -0
- package/src/core/log-record.ts +58 -0
- package/src/core/meter.ts +82 -0
- package/src/core/resource.ts +50 -0
- package/src/core/span.ts +161 -0
- package/src/core/tracer.ts +75 -0
- package/src/exporters/console-exporter.ts +89 -0
- package/src/exporters/otlp-http-exporter.ts +377 -0
- package/src/exporters/types.ts +29 -0
- package/src/index.ts +63 -0
- package/src/instrumentation/errors.ts +95 -0
- package/src/instrumentation/lifecycle.ts +17 -0
- package/src/instrumentation/navigation.ts +61 -0
- package/src/instrumentation/network.ts +253 -0
- package/src/react/OtelProvider.tsx +98 -0
- package/src/react/useOtel.ts +14 -0
- 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
|
+
>;
|