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,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();
|