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,253 @@
1
+ import {
2
+ ATTR_HTTP_REQUEST_METHOD,
3
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
4
+ } from '@opentelemetry/semantic-conventions';
5
+
6
+ import { spanContext } from '../context/span-context';
7
+ import { generateSpanId } from '../core/ids';
8
+ import { Span, NoopSpan } from '../core/span';
9
+ import { Tracer } from '../core/tracer';
10
+
11
+ // Concurrent-safe: parent context is captured by value at request start
12
+ const activeNetworkSpans = new Map<string, Span | NoopSpan>();
13
+
14
+ // ─── Axios shape (subset we care about) ─────────────────────────────────────
15
+
16
+ export interface AxiosRequestConfig {
17
+ method?: string;
18
+ url?: string;
19
+ headers?: Record<string, unknown>;
20
+ params?: Record<string, unknown>;
21
+ data?: unknown;
22
+ __otelId?: string;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ export interface AxiosResponse {
27
+ status: number;
28
+ headers?: Record<string, unknown>;
29
+ data?: unknown;
30
+ config: AxiosRequestConfig;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ export interface AxiosError {
35
+ message: string;
36
+ response?: AxiosResponse;
37
+ config?: AxiosRequestConfig;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ // ─── Redaction helpers ───────────────────────────────────────────────────────
42
+
43
+ // Builds the set of leaf keys to redact for a given section prefix.
44
+ // e.g. section='header', paths=['header.authorization','body.password']
45
+ // → Set { 'authorization' }
46
+ function leafKeysForSection(section: string, paths: string[]): Set<string> {
47
+ const prefix = `${section}.`;
48
+ const result = new Set<string>();
49
+ for (const path of paths) {
50
+ if (path.toLowerCase().startsWith(prefix)) {
51
+ result.add(path.slice(prefix.length).toLowerCase());
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+
57
+ // Shallow-redacts an object, replacing blacklisted keys with '[REDACTED]'.
58
+ // Case-insensitive on keys. Returns the original object reference when
59
+ // there are no sensitive keys to save an allocation.
60
+ function redactObject(
61
+ obj: Record<string, unknown>,
62
+ sensitive: Set<string>
63
+ ): Record<string, unknown> {
64
+ if (sensitive.size === 0) return obj;
65
+ const result: Record<string, unknown> = {};
66
+ for (const key of Object.keys(obj)) {
67
+ result[key] = sensitive.has(key.toLowerCase()) ? '[REDACTED]' : obj[key];
68
+ }
69
+ return result;
70
+ }
71
+
72
+ // Tries to produce a plain object from a request/response body.
73
+ // Handles object and JSON-string forms; skips Blobs, FormData, etc.
74
+ function normalizeBody(data: unknown): Record<string, unknown> | undefined {
75
+ if (data === null || data === undefined) return undefined;
76
+ if (typeof data === 'object' && !Array.isArray(data)) {
77
+ return data as Record<string, unknown>;
78
+ }
79
+ if (typeof data === 'string') {
80
+ try {
81
+ const parsed: unknown = JSON.parse(data);
82
+ if (
83
+ parsed !== null &&
84
+ typeof parsed === 'object' &&
85
+ !Array.isArray(parsed)
86
+ ) {
87
+ return parsed as Record<string, unknown>;
88
+ }
89
+ } catch {
90
+ // not JSON — skip
91
+ }
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ // Serializes an object to a JSON string for use as a span attribute.
97
+ // Returns undefined when serialization fails (circular refs, etc.).
98
+ function toJsonAttr(obj: Record<string, unknown>): string | undefined {
99
+ try {
100
+ return JSON.stringify(obj);
101
+ } catch {
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ // ─── Factory ─────────────────────────────────────────────────────────────────
107
+
108
+ export interface AxiosInstrumentationOptions {
109
+ // Dot-notation paths to redact. Section prefixes:
110
+ // header.{key} — request and response headers
111
+ // body.{key} — request body
112
+ // param.{key} — URL query params
113
+ // response.{key} — response body
114
+ sensitiveKeys?: string[];
115
+ }
116
+
117
+ export function createAxiosInstrumentation(
118
+ tracer: Tracer,
119
+ options?: AxiosInstrumentationOptions
120
+ ) {
121
+ const paths = (options?.sensitiveKeys ?? []).map((k) => k.toLowerCase());
122
+
123
+ // Pre-compute sensitive leaf-key sets once at setup time, not per-request.
124
+ const sensitiveHeaders = leafKeysForSection('header', paths);
125
+ const sensitiveBody = leafKeysForSection('body', paths);
126
+ const sensitiveParams = leafKeysForSection('param', paths);
127
+ const sensitiveResponse = leafKeysForSection('response', paths);
128
+
129
+ return {
130
+ onRequest(config: AxiosRequestConfig): AxiosRequestConfig {
131
+ const otelId = generateSpanId();
132
+ const method = (config.method ?? 'GET').toUpperCase();
133
+ const url = config.url ?? '';
134
+
135
+ // Capture parent context NOW by value — concurrent-safe.
136
+ const currentSpan = spanContext.current();
137
+ const parent =
138
+ currentSpan?.traceId && currentSpan?.spanId
139
+ ? { traceId: currentSpan.traceId, spanId: currentSpan.spanId }
140
+ : null;
141
+
142
+ const span = tracer.startSpan(`http.${method} ${url}`, {
143
+ kind: 'CLIENT',
144
+ parent,
145
+ attributes: {
146
+ [ATTR_HTTP_REQUEST_METHOD]: method, // 'http.request.method'
147
+ 'http.url': url, // stable experimental, no change
148
+ },
149
+ });
150
+
151
+ // Request headers
152
+ if (config.headers) {
153
+ const redacted = redactObject(
154
+ config.headers as Record<string, unknown>,
155
+ sensitiveHeaders
156
+ );
157
+ const serialized = toJsonAttr(redacted);
158
+ if (serialized) span.setAttribute('http.request.headers', serialized);
159
+ }
160
+
161
+ // Query params
162
+ if (config.params) {
163
+ const redacted = redactObject(config.params, sensitiveParams);
164
+ const serialized = toJsonAttr(redacted);
165
+ if (serialized) span.setAttribute('http.request.params', serialized);
166
+ }
167
+
168
+ // Request body
169
+ const reqBody = normalizeBody(config.data);
170
+ if (reqBody) {
171
+ const redacted = redactObject(reqBody, sensitiveBody);
172
+ const serialized = toJsonAttr(redacted);
173
+ if (serialized) span.setAttribute('http.request.body', serialized);
174
+ }
175
+
176
+ activeNetworkSpans.set(otelId, span);
177
+ config.__otelId = otelId;
178
+ return config;
179
+ },
180
+
181
+ onResponse(response: AxiosResponse): AxiosResponse {
182
+ const otelId = response.config.__otelId;
183
+ if (otelId) {
184
+ const span = activeNetworkSpans.get(otelId);
185
+ if (span) {
186
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
187
+
188
+ // Response headers
189
+ if (response.headers) {
190
+ const redacted = redactObject(response.headers, sensitiveHeaders);
191
+ const serialized = toJsonAttr(redacted);
192
+ if (serialized)
193
+ span.setAttribute('http.response.headers', serialized);
194
+ }
195
+
196
+ // Response body
197
+ const resBody = normalizeBody(response.data);
198
+ if (resBody) {
199
+ const redacted = redactObject(resBody, sensitiveResponse);
200
+ const serialized = toJsonAttr(redacted);
201
+ if (serialized) span.setAttribute('http.response.body', serialized);
202
+ }
203
+
204
+ span.setStatus('OK');
205
+ span.end();
206
+ activeNetworkSpans.delete(otelId);
207
+ }
208
+ }
209
+ return response;
210
+ },
211
+
212
+ onError(error: AxiosError): Promise<never> {
213
+ const otelId = error.config?.__otelId;
214
+ if (otelId) {
215
+ const span = activeNetworkSpans.get(otelId);
216
+ if (span) {
217
+ span.setAttribute(
218
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
219
+ error.response?.status ?? 0
220
+ );
221
+
222
+ // Error response headers + body when available
223
+ if (error.response?.headers) {
224
+ const redacted = redactObject(
225
+ error.response.headers,
226
+ sensitiveHeaders
227
+ );
228
+ const serialized = toJsonAttr(redacted);
229
+ if (serialized)
230
+ span.setAttribute('http.response.headers', serialized);
231
+ }
232
+
233
+ const errBody = normalizeBody(error.response?.data);
234
+ if (errBody) {
235
+ const redacted = redactObject(errBody, sensitiveResponse);
236
+ const serialized = toJsonAttr(redacted);
237
+ if (serialized) span.setAttribute('http.response.body', serialized);
238
+ }
239
+
240
+ span.recordException(error as unknown as Error);
241
+ span.setStatus('ERROR', error.message);
242
+ span.end();
243
+ activeNetworkSpans.delete(otelId);
244
+ }
245
+ }
246
+ return Promise.reject(error);
247
+ },
248
+ };
249
+ }
250
+
251
+ export type AxiosInstrumentation = ReturnType<
252
+ typeof createAxiosInstrumentation
253
+ >;
@@ -0,0 +1,98 @@
1
+ import React, {
2
+ createContext,
3
+ Component,
4
+ type ErrorInfo,
5
+ type ReactNode,
6
+ } from 'react';
7
+
8
+ import { OtelLogger } from '../core/log-record';
9
+ import { Meter } from '../core/meter';
10
+ import { Tracer } from '../core/tracer';
11
+ import { otel } from '../sdk';
12
+
13
+ export interface OtelContextValue {
14
+ tracer: Tracer;
15
+ meter: Meter;
16
+ logger: OtelLogger;
17
+ recordEvent: (name: string, attributes?: Record<string, unknown>) => void;
18
+ setUser: (user: { id?: string; email?: string }) => void;
19
+ }
20
+
21
+ export const OtelContext = createContext<OtelContextValue | undefined>(
22
+ undefined
23
+ );
24
+
25
+ interface ErrorBoundaryProps {
26
+ children: ReactNode;
27
+ tracer: Tracer;
28
+ }
29
+
30
+ interface ErrorBoundaryState {
31
+ hasError: boolean;
32
+ }
33
+
34
+ class OtelErrorBoundary extends Component<
35
+ ErrorBoundaryProps,
36
+ ErrorBoundaryState
37
+ > {
38
+ constructor(props: ErrorBoundaryProps) {
39
+ super(props);
40
+ this.state = { hasError: false };
41
+ }
42
+
43
+ static getDerivedStateFromError(): ErrorBoundaryState {
44
+ return { hasError: true };
45
+ }
46
+
47
+ componentDidCatch(error: Error, _info: ErrorInfo): void {
48
+ this.props.tracer.recordException(error, {
49
+ 'error.source': 'react_error_boundary',
50
+ });
51
+ }
52
+
53
+ render(): ReactNode {
54
+ if (this.state.hasError) {
55
+ return null;
56
+ }
57
+ return this.props.children;
58
+ }
59
+ }
60
+
61
+ export interface OtelProviderProps {
62
+ children: ReactNode;
63
+ withErrorBoundary?: boolean;
64
+ }
65
+
66
+ export function OtelProvider({
67
+ children,
68
+ withErrorBoundary = false,
69
+ }: OtelProviderProps): React.ReactElement {
70
+ const tracer = otel.getTracer();
71
+ const meter = otel.getMeter();
72
+ const logger = otel.getLogger();
73
+
74
+ const value: OtelContextValue = {
75
+ tracer,
76
+ meter,
77
+ logger,
78
+ recordEvent: (name, attributes) =>
79
+ tracer.recordEvent(
80
+ name,
81
+ attributes as Record<
82
+ string,
83
+ string | number | boolean | string[] | number[] | boolean[]
84
+ >
85
+ ),
86
+ setUser: (user) => otel.setUser(user),
87
+ };
88
+
89
+ const content = (
90
+ <OtelContext.Provider value={value}>{children}</OtelContext.Provider>
91
+ );
92
+
93
+ if (withErrorBoundary) {
94
+ return <OtelErrorBoundary tracer={tracer}>{content}</OtelErrorBoundary>;
95
+ }
96
+
97
+ return content;
98
+ }
@@ -0,0 +1,14 @@
1
+ import { useContext } from 'react';
2
+
3
+ import type { OtelContextValue } from './OtelProvider';
4
+ import { OtelContext } from './OtelProvider';
5
+
6
+ export function useOtel(): OtelContextValue {
7
+ const ctx = useContext(OtelContext);
8
+ if (!ctx) {
9
+ throw new Error(
10
+ '[react-native-otel] useOtel must be used inside OtelProvider.'
11
+ );
12
+ }
13
+ return ctx;
14
+ }
package/src/sdk.ts ADDED
@@ -0,0 +1,179 @@
1
+ import type { Attributes } from './core/attributes';
2
+ import type { Resource } from './core/resource';
3
+ import type {
4
+ SpanExporter,
5
+ MetricExporter,
6
+ LogExporter,
7
+ } from './exporters/types';
8
+ import type { StorageAdapter } from './instrumentation/errors';
9
+ import { spanContext } from './context/span-context';
10
+ import { setMaxStringLength } from './core/attributes';
11
+ import { OtelLogger } from './core/log-record';
12
+ import { Meter } from './core/meter';
13
+ import { buildResource } from './core/resource';
14
+ import { Tracer } from './core/tracer';
15
+ import { installErrorInstrumentation } from './instrumentation/errors';
16
+ import { installLifecycleInstrumentation } from './instrumentation/lifecycle';
17
+
18
+ export interface OtelConfig {
19
+ serviceName: string;
20
+ serviceVersion?: string;
21
+ osName?: string;
22
+ osVersion?: string;
23
+ deviceBrand?: string;
24
+ deviceModel?: string;
25
+ deviceType?: string | number;
26
+ appBuild?: string;
27
+ environment?: string;
28
+ exporter?: SpanExporter;
29
+ metricExporter?: MetricExporter;
30
+ logExporter?: LogExporter;
31
+ sampleRate?: number;
32
+ debug?: boolean;
33
+ storage?: StorageAdapter;
34
+ // Truncate string attribute values longer than this. Default: 1024.
35
+ maxAttributeStringLength?: number;
36
+ // Dot-notation paths to redact from network captures.
37
+ // Sections: header, body, param, response
38
+ // Examples: ['header.authorization', 'body.password', 'response.token']
39
+ sensitiveKeys?: string[];
40
+ }
41
+
42
+ class OtelSDK {
43
+ private userAttributes_: Attributes = {};
44
+ private tracer_: Tracer | undefined;
45
+ private meter_: Meter | undefined;
46
+ private logger_: OtelLogger | undefined;
47
+ private resource_: Readonly<Resource> | undefined;
48
+ private exporter_: SpanExporter | undefined;
49
+ private metricExporter_: MetricExporter | undefined;
50
+ private logExporter_: LogExporter | undefined;
51
+ private sensitiveKeys_: string[] = [];
52
+ private initialized = false;
53
+
54
+ init(config: OtelConfig): void {
55
+ if (this.initialized) return;
56
+ this.initialized = true;
57
+
58
+ if (config.maxAttributeStringLength !== undefined) {
59
+ setMaxStringLength(config.maxAttributeStringLength);
60
+ }
61
+
62
+ this.sensitiveKeys_ = config.sensitiveKeys ?? [];
63
+ this.exporter_ = config.exporter;
64
+ this.metricExporter_ = config.metricExporter;
65
+ this.logExporter_ = config.logExporter;
66
+
67
+ this.resource_ = buildResource({
68
+ serviceName: config.serviceName,
69
+ serviceVersion: config.serviceVersion ?? '0.0.0',
70
+ osName: config.osName ?? '',
71
+ osVersion: config.osVersion ?? '',
72
+ deviceBrand: config.deviceBrand ?? '',
73
+ deviceModel: config.deviceModel ?? '',
74
+ deviceType: config.deviceType ?? '',
75
+ appBuild: config.appBuild ?? '',
76
+ environment: config.environment ?? 'production',
77
+ });
78
+
79
+ // If any exporter supports resource injection (e.g. OtlpHttp*Exporter),
80
+ // hand it the resource so it can include it in OTLP payloads.
81
+ const injectResource = (exp: unknown) => {
82
+ if (exp && typeof exp === 'object' && 'setResource' in exp) {
83
+ (exp as { setResource(r: Readonly<Resource>): void }).setResource(
84
+ this.resource_ as Readonly<Resource>
85
+ );
86
+ }
87
+ };
88
+ injectResource(config.exporter);
89
+ injectResource(config.metricExporter);
90
+ injectResource(config.logExporter);
91
+
92
+ this.tracer_ = new Tracer({
93
+ exporter: config.exporter,
94
+ sampleRate: config.sampleRate,
95
+ getUserAttributes: () => ({ ...this.userAttributes_ }),
96
+ });
97
+
98
+ this.meter_ = new Meter(config.metricExporter);
99
+ this.logger_ = new OtelLogger(config.logExporter);
100
+
101
+ // Check for pending crash span and install global error handler
102
+ installErrorInstrumentation({
103
+ tracer: this.tracer_,
104
+ storage: config.storage,
105
+ exporter: config.exporter,
106
+ });
107
+
108
+ // Install lifecycle instrumentation
109
+ installLifecycleInstrumentation(this.meter_);
110
+ }
111
+
112
+ // Records a named event on the current screen span.
113
+ // Safe to call before init() — silently no-ops if SDK is not yet initialized.
114
+ recordEvent(name: string, properties?: Record<string, unknown>): void {
115
+ this.tracer_?.recordEvent(name, properties as unknown as Attributes);
116
+ }
117
+
118
+ setUser(user: { id?: string; email?: string }): void {
119
+ this.userAttributes_ = {
120
+ ...(user.id ? { 'user.id': user.id } : {}),
121
+ ...(user.email ? { 'user.email': user.email } : {}),
122
+ };
123
+ }
124
+
125
+ getTracer(): Tracer {
126
+ if (!this.tracer_) {
127
+ throw new Error(
128
+ '[react-native-otel] SDK not initialized. Call otel.init() first.'
129
+ );
130
+ }
131
+ return this.tracer_;
132
+ }
133
+
134
+ getMeter(): Meter {
135
+ if (!this.meter_) {
136
+ throw new Error(
137
+ '[react-native-otel] SDK not initialized. Call otel.init() first.'
138
+ );
139
+ }
140
+ return this.meter_;
141
+ }
142
+
143
+ getSensitiveKeys(): string[] {
144
+ return this.sensitiveKeys_;
145
+ }
146
+
147
+ getLogger(): OtelLogger {
148
+ if (!this.logger_) {
149
+ throw new Error(
150
+ '[react-native-otel] SDK not initialized. Call otel.init() first.'
151
+ );
152
+ }
153
+ return this.logger_;
154
+ }
155
+
156
+ async shutdown(): Promise<void> {
157
+ // End active screen span
158
+ const current = spanContext.current();
159
+ if (current) {
160
+ current.end();
161
+ spanContext.setCurrent(undefined);
162
+ }
163
+
164
+ // Flush buffered metrics
165
+ this.meter_?.flush();
166
+
167
+ // Flush + tear down exporter timers (e.g. OtlpHttp*Exporter)
168
+ const destroyExporter = (exp: unknown) => {
169
+ if (exp && typeof exp === 'object' && 'destroy' in exp) {
170
+ (exp as { destroy(): void }).destroy();
171
+ }
172
+ };
173
+ destroyExporter(this.exporter_);
174
+ destroyExporter(this.metricExporter_);
175
+ destroyExporter(this.logExporter_);
176
+ }
177
+ }
178
+
179
+ export const otel = new OtelSDK();