appdoctor-rn 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/dist/index.js ADDED
@@ -0,0 +1,595 @@
1
+ import { createContext, useRef, useEffect, useContext } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/core/types.ts
5
+ var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
6
+ var DEFAULT_MAX_QUEUE_SIZE = 200;
7
+
8
+ // src/core/sampling.ts
9
+ function shouldSample(rate) {
10
+ if (rate >= 1) return true;
11
+ if (rate <= 0) return false;
12
+ return Math.random() < rate;
13
+ }
14
+
15
+ // src/core/client.ts
16
+ function now() {
17
+ return Date.now();
18
+ }
19
+ function newSessionId() {
20
+ return `${now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
21
+ }
22
+ var AppDoctorClient = class {
23
+ constructor(config = {}) {
24
+ this.queue = [];
25
+ this.fetchPatched = false;
26
+ this.sessionId = newSessionId();
27
+ this.config = {
28
+ enabled: config.enabled ?? true,
29
+ noop: config.noop ?? false,
30
+ sampleRate: config.sampleRate ?? 1,
31
+ flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
32
+ maxQueueSize: config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
33
+ instrumentFetch: config.instrumentFetch ?? true,
34
+ tags: config.tags,
35
+ context: config.context,
36
+ redactNetworkEvent: config.redactNetworkEvent
37
+ };
38
+ this.transports = [...config.transports ?? []];
39
+ if (this.config.enabled) {
40
+ this.startFlushTimer();
41
+ if (this.config.instrumentFetch && typeof globalThis.fetch === "function") {
42
+ this.patchFetch();
43
+ }
44
+ }
45
+ }
46
+ updateConfig(partial) {
47
+ if (partial.enabled !== void 0) {
48
+ this.config.enabled = partial.enabled;
49
+ }
50
+ if (partial.noop !== void 0) {
51
+ this.config.noop = partial.noop;
52
+ }
53
+ if (partial.sampleRate !== void 0) {
54
+ this.config.sampleRate = partial.sampleRate;
55
+ }
56
+ if (partial.flushIntervalMs !== void 0) {
57
+ this.config.flushIntervalMs = partial.flushIntervalMs;
58
+ this.restartFlushTimer();
59
+ }
60
+ if (partial.maxQueueSize !== void 0) {
61
+ this.config.maxQueueSize = partial.maxQueueSize;
62
+ }
63
+ if (partial.tags !== void 0) {
64
+ this.config.tags = partial.tags;
65
+ }
66
+ if (partial.context !== void 0) {
67
+ this.config.context = partial.context;
68
+ }
69
+ if (partial.instrumentFetch !== void 0) {
70
+ const next = partial.instrumentFetch;
71
+ this.config.instrumentFetch = next;
72
+ if (next && !this.fetchPatched) this.patchFetch();
73
+ if (!next && this.fetchPatched) this.restoreFetch();
74
+ }
75
+ if (partial.transports !== void 0) {
76
+ this.transports.length = 0;
77
+ this.transports.push(...partial.transports);
78
+ }
79
+ if (partial.redactNetworkEvent !== void 0) {
80
+ this.config.redactNetworkEvent = partial.redactNetworkEvent;
81
+ }
82
+ }
83
+ addTransport(transport) {
84
+ this.transports.push(transport);
85
+ }
86
+ emit(event) {
87
+ if (!this.config.enabled) return;
88
+ if (this.config.noop) return;
89
+ if (!shouldSample(this.config.sampleRate)) return;
90
+ const { tags: eventTags, ...payload } = event;
91
+ const tags = this.config.tags || eventTags ? { ...this.config.tags, ...eventTags } : void 0;
92
+ const full = {
93
+ ...payload,
94
+ timestamp: now(),
95
+ sessionId: this.sessionId,
96
+ tags,
97
+ context: this.config.context
98
+ };
99
+ this.enqueue(full);
100
+ }
101
+ enqueue(event) {
102
+ if (this.queue.length >= this.config.maxQueueSize) {
103
+ this.queue.shift();
104
+ }
105
+ this.queue.push(event);
106
+ }
107
+ captureError(message, context, err) {
108
+ const stack = err instanceof Error && typeof err.stack === "string" ? err.stack : void 0;
109
+ const sdkErr = {
110
+ name: "sdk_error",
111
+ message,
112
+ stack,
113
+ errorContext: context
114
+ };
115
+ this.emit(sdkErr);
116
+ }
117
+ async flush() {
118
+ if (this.queue.length === 0) return;
119
+ const batch = this.queue.splice(0, this.queue.length);
120
+ await Promise.all(
121
+ this.transports.map(async (t) => {
122
+ try {
123
+ await t.send(batch);
124
+ } catch (e) {
125
+ this.safeInternalError("transport.send failed", e);
126
+ }
127
+ })
128
+ );
129
+ }
130
+ async shutdown() {
131
+ if (this.flushTimer) {
132
+ clearInterval(this.flushTimer);
133
+ this.flushTimer = void 0;
134
+ }
135
+ this.restoreFetch();
136
+ await this.flush();
137
+ await Promise.all(
138
+ this.transports.map(async (t) => {
139
+ if (!t.shutdown) return;
140
+ try {
141
+ await t.shutdown();
142
+ } catch (e) {
143
+ this.safeInternalError("transport.shutdown failed", e);
144
+ }
145
+ })
146
+ );
147
+ }
148
+ startFlushTimer() {
149
+ this.flushTimer = setInterval(() => {
150
+ void this.flush();
151
+ }, this.config.flushIntervalMs);
152
+ if (typeof this.flushTimer === "object" && this.flushTimer && "unref" in this.flushTimer && typeof this.flushTimer.unref === "function") {
153
+ this.flushTimer.unref();
154
+ }
155
+ }
156
+ restartFlushTimer() {
157
+ if (this.flushTimer) clearInterval(this.flushTimer);
158
+ this.startFlushTimer();
159
+ }
160
+ safeInternalError(msg, err) {
161
+ const dev = typeof __DEV__ !== "undefined" ? __DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
162
+ if (dev) console.warn(`[AppDoctor] ${msg}`, err);
163
+ }
164
+ patchFetch() {
165
+ if (this.fetchPatched) return;
166
+ const g = globalThis;
167
+ this.originalFetch = g.fetch.bind(globalThis);
168
+ const originalFetch = this.originalFetch;
169
+ g.fetch = async (input, init) => {
170
+ const start = now();
171
+ let method = "GET";
172
+ let url = "";
173
+ try {
174
+ if (typeof input === "string") {
175
+ url = input;
176
+ } else if (input instanceof URL) {
177
+ url = input.href;
178
+ } else if (typeof Request !== "undefined" && input instanceof Request) {
179
+ url = input.url;
180
+ method = input.method;
181
+ }
182
+ if (init?.method) method = init.method;
183
+ } catch {
184
+ }
185
+ const urlForEvent = url || (typeof input === "string" ? input : input instanceof URL ? input.href : typeof Request !== "undefined" && input instanceof Request ? input.url : "[request]");
186
+ try {
187
+ const res = await originalFetch(input, init);
188
+ const durationMs = now() - start;
189
+ const event = this.applyNetworkRedaction({
190
+ method,
191
+ url: urlForEvent,
192
+ status: res.status,
193
+ durationMs,
194
+ success: true
195
+ });
196
+ this.emit({
197
+ name: "api_request",
198
+ ...event
199
+ });
200
+ return res;
201
+ } catch (e) {
202
+ const durationMs = now() - start;
203
+ const event = this.applyNetworkRedaction({
204
+ method,
205
+ url: urlForEvent,
206
+ durationMs,
207
+ success: false,
208
+ errorMessage: e instanceof Error ? e.message : String(e)
209
+ });
210
+ this.emit({
211
+ name: "api_request",
212
+ ...event
213
+ });
214
+ throw e;
215
+ }
216
+ };
217
+ this.fetchPatched = true;
218
+ }
219
+ restoreFetch() {
220
+ if (!this.fetchPatched || !this.originalFetch) return;
221
+ globalThis.fetch = this.originalFetch;
222
+ this.fetchPatched = false;
223
+ this.originalFetch = void 0;
224
+ }
225
+ applyNetworkRedaction(event) {
226
+ if (!this.config.redactNetworkEvent) return event;
227
+ const redacted = this.config.redactNetworkEvent(event);
228
+ return { ...event, ...redacted };
229
+ }
230
+ };
231
+
232
+ // src/transports/console-transport.ts
233
+ function createConsoleTransport(options = {}) {
234
+ const label = options.label ?? "AppDoctor";
235
+ const slowScreenThresholdMs = options.slowScreenThresholdMs ?? 1e3;
236
+ const slowApiThresholdMs = options.slowApiThresholdMs ?? 800;
237
+ function toHint(event) {
238
+ if (event.name === "screen_load" && event.phase === "ready" && event.durationMs >= slowScreenThresholdMs) {
239
+ return `Slow screen "${event.screen}" (${event.durationMs}ms). Check expensive effects and repeated renders.`;
240
+ }
241
+ if (event.name === "api_request" && event.durationMs >= slowApiThresholdMs) {
242
+ return `Slow API "${event.method} ${event.url}" (${event.durationMs}ms). Check server latency, payload size, and retry loops.`;
243
+ }
244
+ return void 0;
245
+ }
246
+ return {
247
+ send(events) {
248
+ if (events.length === 0) return;
249
+ for (const event of events) {
250
+ const hint = toHint(event);
251
+ if (hint) {
252
+ console.warn(`[${label}] ${hint}`);
253
+ }
254
+ }
255
+ console.log(`[${label}]`, events);
256
+ }
257
+ };
258
+ }
259
+
260
+ // src/transports/http-transport.ts
261
+ async function sleep(ms) {
262
+ await new Promise((r) => setTimeout(r, ms));
263
+ }
264
+ function createHttpTransport(options) {
265
+ const maxRetries = options.maxRetries ?? 3;
266
+ const initialBackoffMs = options.initialBackoffMs ?? 500;
267
+ return {
268
+ async send(events) {
269
+ if (events.length === 0) return;
270
+ const body = {
271
+ events: [...events],
272
+ ...options.getExtraBody?.() ?? {}
273
+ };
274
+ let attempt = 0;
275
+ let backoff = initialBackoffMs;
276
+ for (; ; ) {
277
+ try {
278
+ const res = await fetch(options.url, {
279
+ method: "POST",
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ ...options.headers
283
+ },
284
+ body: JSON.stringify(body)
285
+ });
286
+ if (res.ok) return;
287
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) {
288
+ return;
289
+ }
290
+ } catch {
291
+ }
292
+ attempt += 1;
293
+ if (attempt > maxRetries) return;
294
+ await sleep(backoff);
295
+ backoff *= 2;
296
+ }
297
+ }
298
+ };
299
+ }
300
+
301
+ // src/navigation/route-name.ts
302
+ function getActiveRouteName(state) {
303
+ if (!state || typeof state !== "object") return void 0;
304
+ const s = state;
305
+ if (!Array.isArray(s.routes) || typeof s.index !== "number") return void 0;
306
+ const route = s.routes[s.index];
307
+ if (!route || typeof route !== "object") return void 0;
308
+ if (route.state !== void 0) return getActiveRouteName(route.state);
309
+ return typeof route.name === "string" ? route.name : void 0;
310
+ }
311
+
312
+ // src/navigation/create-navigation-listener.ts
313
+ function createNavigationStateListener(client) {
314
+ const timing = {};
315
+ return {
316
+ listener(state) {
317
+ const screen = getActiveRouteName(state);
318
+ if (!screen || screen === timing.lastScreen) return;
319
+ timing.lastScreen = screen;
320
+ const startedAt = Date.now();
321
+ client.emit({
322
+ name: "screen_load",
323
+ screen,
324
+ phase: "start",
325
+ durationMs: 0
326
+ });
327
+ queueMicrotask(() => {
328
+ if (timing.lastScreen !== screen) return;
329
+ client.emit({
330
+ name: "screen_load",
331
+ screen,
332
+ phase: "ready",
333
+ durationMs: Date.now() - startedAt
334
+ });
335
+ });
336
+ },
337
+ reset() {
338
+ timing.lastScreen = void 0;
339
+ }
340
+ };
341
+ }
342
+
343
+ // src/network/axios-instrumentation.ts
344
+ function getConfig(errorOrResponse) {
345
+ if (!errorOrResponse || typeof errorOrResponse !== "object") return void 0;
346
+ if ("config" in errorOrResponse && errorOrResponse.config && typeof errorOrResponse.config === "object") {
347
+ return errorOrResponse.config;
348
+ }
349
+ return void 0;
350
+ }
351
+ function getResponseMeta(response) {
352
+ if (!response || typeof response !== "object") return {};
353
+ const r = response;
354
+ const status = "status" in r && typeof r.status === "number" ? r.status : void 0;
355
+ const rawConfig = "config" in r ? r.config : void 0;
356
+ let method;
357
+ let url;
358
+ if (rawConfig !== null && typeof rawConfig === "object") {
359
+ if ("method" in rawConfig && typeof rawConfig.method === "string") {
360
+ method = rawConfig.method;
361
+ }
362
+ if ("url" in rawConfig && typeof rawConfig.url === "string") {
363
+ url = rawConfig.url;
364
+ }
365
+ }
366
+ return {
367
+ status,
368
+ method,
369
+ url
370
+ };
371
+ }
372
+ function instrumentAxios(axios, client, options = {}) {
373
+ function applyRedaction(event) {
374
+ const redacted = options.redactNetworkEvent?.(event);
375
+ return redacted ? { ...event, ...redacted } : event;
376
+ }
377
+ const starts = /* @__PURE__ */ new WeakMap();
378
+ const reqId = axios.interceptors.request.use((config) => {
379
+ if (config !== null && typeof config === "object") {
380
+ starts.set(config, Date.now());
381
+ }
382
+ return config;
383
+ });
384
+ const resId = axios.interceptors.response.use(
385
+ (response) => {
386
+ const cfg = getConfig(response);
387
+ if (cfg !== null && typeof cfg === "object") {
388
+ const start = starts.get(cfg);
389
+ if (start !== void 0) {
390
+ const { status, method, url } = getResponseMeta(response);
391
+ const event = applyRedaction({
392
+ method: (method ?? "GET").toUpperCase(),
393
+ url: url ?? "",
394
+ status,
395
+ durationMs: Date.now() - start,
396
+ success: true
397
+ });
398
+ client.emit({
399
+ name: "api_request",
400
+ ...event
401
+ });
402
+ starts.delete(cfg);
403
+ }
404
+ }
405
+ return response;
406
+ },
407
+ (error) => {
408
+ const cfg = getConfig(error);
409
+ if (cfg !== null && typeof cfg === "object") {
410
+ const start = starts.get(cfg);
411
+ if (start !== void 0) {
412
+ let method = "GET";
413
+ let url = "";
414
+ let status;
415
+ let errorMessage = String(error);
416
+ if (error !== null && typeof error === "object") {
417
+ if ("message" in error && typeof error.message === "string") {
418
+ errorMessage = error.message;
419
+ }
420
+ const resp = "response" in error && error.response !== null && typeof error.response === "object" ? error.response : void 0;
421
+ if (resp && "status" in resp && typeof resp.status === "number") {
422
+ status = resp.status;
423
+ }
424
+ const errCfg = "config" in error && error.config !== null && typeof error.config === "object" ? error.config : void 0;
425
+ if (errCfg) {
426
+ if ("method" in errCfg && typeof errCfg.method === "string") {
427
+ method = errCfg.method.toUpperCase();
428
+ }
429
+ if ("url" in errCfg && typeof errCfg.url === "string") {
430
+ url = errCfg.url;
431
+ }
432
+ }
433
+ }
434
+ const event = applyRedaction({
435
+ method,
436
+ url,
437
+ status,
438
+ durationMs: Date.now() - start,
439
+ success: false,
440
+ errorMessage
441
+ });
442
+ client.emit({
443
+ name: "api_request",
444
+ ...event
445
+ });
446
+ starts.delete(cfg);
447
+ }
448
+ }
449
+ const rejectReason = error instanceof Error ? error : new Error(String(error));
450
+ return Promise.reject(rejectReason);
451
+ }
452
+ );
453
+ return () => {
454
+ axios.interceptors.request.eject(reqId);
455
+ axios.interceptors.response.eject(resId);
456
+ };
457
+ }
458
+
459
+ // src/utils/track-api.ts
460
+ async function trackApi(client, label, fn) {
461
+ const start = Date.now();
462
+ try {
463
+ const result = await fn();
464
+ client.emit({
465
+ name: "api_request",
466
+ method: "TRACK",
467
+ url: label,
468
+ status: 200,
469
+ durationMs: Date.now() - start,
470
+ success: true
471
+ });
472
+ return result;
473
+ } catch (e) {
474
+ client.emit({
475
+ name: "api_request",
476
+ method: "TRACK",
477
+ url: label,
478
+ durationMs: Date.now() - start,
479
+ success: false,
480
+ errorMessage: e instanceof Error ? e.message : String(e)
481
+ });
482
+ throw e;
483
+ }
484
+ }
485
+ var AppDoctorContext = createContext(null);
486
+ function AppDoctorProvider({
487
+ children,
488
+ ...config
489
+ }) {
490
+ const clientRef = useRef(null);
491
+ if (!clientRef.current) {
492
+ clientRef.current = new AppDoctorClient(config);
493
+ }
494
+ const {
495
+ enabled,
496
+ noop,
497
+ sampleRate,
498
+ flushIntervalMs,
499
+ maxQueueSize,
500
+ instrumentFetch,
501
+ tags,
502
+ context,
503
+ transports,
504
+ redactNetworkEvent
505
+ } = config;
506
+ useEffect(() => {
507
+ clientRef.current?.updateConfig({
508
+ enabled,
509
+ noop,
510
+ sampleRate,
511
+ flushIntervalMs,
512
+ maxQueueSize,
513
+ instrumentFetch,
514
+ tags,
515
+ context,
516
+ transports,
517
+ redactNetworkEvent
518
+ });
519
+ }, [
520
+ enabled,
521
+ noop,
522
+ sampleRate,
523
+ flushIntervalMs,
524
+ maxQueueSize,
525
+ instrumentFetch,
526
+ tags,
527
+ context,
528
+ transports,
529
+ redactNetworkEvent
530
+ ]);
531
+ useEffect(() => {
532
+ return () => {
533
+ void clientRef.current?.shutdown();
534
+ clientRef.current = null;
535
+ };
536
+ }, []);
537
+ return /* @__PURE__ */ jsx(AppDoctorContext.Provider, { value: clientRef.current, children });
538
+ }
539
+ function useAppDoctor() {
540
+ const client = useContext(AppDoctorContext);
541
+ if (!client) {
542
+ throw new Error("useAppDoctor must be used within AppDoctorProvider");
543
+ }
544
+ return client;
545
+ }
546
+ function useTrackScreen(screen, options = {}) {
547
+ const fromContext = useContext(AppDoctorContext);
548
+ const client = options.client ?? fromContext;
549
+ if (!client) {
550
+ throw new Error(
551
+ "useTrackScreen requires AppDoctorProvider or options.client"
552
+ );
553
+ }
554
+ useEffect(() => {
555
+ const startedAt = Date.now();
556
+ client.emit({
557
+ name: "screen_load",
558
+ screen,
559
+ phase: "start",
560
+ durationMs: 0
561
+ });
562
+ queueMicrotask(() => {
563
+ client.emit({
564
+ name: "screen_load",
565
+ screen,
566
+ phase: "ready",
567
+ durationMs: Date.now() - startedAt
568
+ });
569
+ });
570
+ }, [client, screen]);
571
+ }
572
+ function useTrackRender(componentName, options = {}) {
573
+ const fromContext = useContext(AppDoctorContext);
574
+ const client = options.client ?? fromContext;
575
+ if (!client) {
576
+ throw new Error(
577
+ "useTrackRender requires AppDoctorProvider or options.client"
578
+ );
579
+ }
580
+ const every = options.every ?? 10;
581
+ const count = useRef(0);
582
+ count.current += 1;
583
+ useEffect(() => {
584
+ if (count.current % every !== 0) return;
585
+ client.emit({
586
+ name: "render_event",
587
+ component: componentName,
588
+ renderCount: count.current
589
+ });
590
+ });
591
+ }
592
+
593
+ export { AppDoctorClient, AppDoctorContext, AppDoctorProvider, createConsoleTransport, createHttpTransport, createNavigationStateListener, getActiveRouteName, instrumentAxios, shouldSample, trackApi, useAppDoctor, useTrackRender, useTrackScreen };
594
+ //# sourceMappingURL=index.js.map
595
+ //# sourceMappingURL=index.js.map