@goliapkg/sentori-react-native 1.3.0 → 2.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 (59) hide show
  1. package/README.md +63 -3
  2. package/lib/config.d.ts +5 -0
  3. package/lib/config.d.ts.map +1 -1
  4. package/lib/config.js.map +1 -1
  5. package/lib/feedback.d.ts +2 -0
  6. package/lib/feedback.d.ts.map +1 -0
  7. package/lib/feedback.js +17 -0
  8. package/lib/feedback.js.map +1 -0
  9. package/lib/handlers/network.d.ts.map +1 -1
  10. package/lib/handlers/network.js +7 -0
  11. package/lib/handlers/network.js.map +1 -1
  12. package/lib/init.d.ts +18 -0
  13. package/lib/init.d.ts.map +1 -1
  14. package/lib/init.js +39 -0
  15. package/lib/init.js.map +1 -1
  16. package/lib/metrics.d.ts +2 -4
  17. package/lib/metrics.d.ts.map +1 -1
  18. package/lib/metrics.js.map +1 -1
  19. package/lib/navigation.d.ts.map +1 -1
  20. package/lib/navigation.js +14 -1
  21. package/lib/navigation.js.map +1 -1
  22. package/lib/runtime-metrics-fps.d.ts +19 -0
  23. package/lib/runtime-metrics-fps.d.ts.map +1 -0
  24. package/lib/runtime-metrics-fps.js +128 -0
  25. package/lib/runtime-metrics-fps.js.map +1 -0
  26. package/lib/runtime-metrics-heap.d.ts +13 -0
  27. package/lib/runtime-metrics-heap.d.ts.map +1 -0
  28. package/lib/runtime-metrics-heap.js +67 -0
  29. package/lib/runtime-metrics-heap.js.map +1 -0
  30. package/lib/runtime-metrics-network.d.ts +23 -0
  31. package/lib/runtime-metrics-network.d.ts.map +1 -0
  32. package/lib/runtime-metrics-network.js +99 -0
  33. package/lib/runtime-metrics-network.js.map +1 -0
  34. package/lib/runtime-metrics.d.ts +43 -0
  35. package/lib/runtime-metrics.d.ts.map +1 -0
  36. package/lib/runtime-metrics.js +115 -0
  37. package/lib/runtime-metrics.js.map +1 -0
  38. package/lib/track.d.ts.map +1 -1
  39. package/lib/track.js +9 -0
  40. package/lib/track.js.map +1 -1
  41. package/lib/transport.d.ts +18 -0
  42. package/lib/transport.d.ts.map +1 -1
  43. package/lib/transport.js +31 -0
  44. package/lib/transport.js.map +1 -1
  45. package/package.json +8 -3
  46. package/src/__tests__/runtime-metrics-instruments.test.ts +109 -0
  47. package/src/__tests__/runtime-metrics-network.test.ts +78 -0
  48. package/src/config.ts +5 -0
  49. package/src/feedback.ts +21 -0
  50. package/src/handlers/network.ts +11 -0
  51. package/src/init.ts +61 -0
  52. package/src/metrics.ts +3 -1
  53. package/src/navigation.ts +14 -1
  54. package/src/runtime-metrics-fps.ts +137 -0
  55. package/src/runtime-metrics-heap.ts +78 -0
  56. package/src/runtime-metrics-network.ts +103 -0
  57. package/src/runtime-metrics.ts +122 -0
  58. package/src/track.ts +9 -0
  59. package/src/transport.ts +39 -0
@@ -0,0 +1,103 @@
1
+ // v2.1 W2 part 4 — network bytes auto-instrument.
2
+ //
3
+ // Two counters (sent / received bytes) incremented by the existing
4
+ // fetch patch on every request/response, drained every 30 s as
5
+ // runtime.network.bytes_sent + runtime.network.bytes_received.
6
+ //
7
+ // Best-effort: not every Response exposes `content-length`
8
+ // (chunked encoding, server stripped it, gzipped without a
9
+ // pre-decode header). For those we report 0 — undercounts vs.
10
+ // over-attributing arbitrary numbers. Request body size is
11
+ // estimated only when init.body is a string or has `.byteLength`;
12
+ // FormData / Blob / ReadableStream are not measured.
13
+ //
14
+ // XHR is NOT instrumented here — the existing patchXhr() in
15
+ // handlers/network.ts does the trace span work, and XHR usage in
16
+ // 2026-era RN apps is rare enough that the engineering cost of
17
+ // a second patch isn't justified by the missing data. If a host
18
+ // app shows up with significant XHR traffic we add it in a patch
19
+ // release.
20
+
21
+ import { emitMetric } from '@goliapkg/sentori-core';
22
+
23
+ const TICK_MS = 30_000;
24
+
25
+ let _bytesSent = 0;
26
+ let _bytesReceived = 0;
27
+ let _timer: null | ReturnType<typeof setInterval> = null;
28
+
29
+ /** Called from handlers/network.ts on every outbound request.
30
+ * Cheap — two adds, no allocation. */
31
+ export function recordNetworkBytes(sent: number, received: number): void {
32
+ if (sent > 0) _bytesSent += sent;
33
+ if (received > 0) _bytesReceived += received;
34
+ }
35
+
36
+ function estimateBodyBytes(body: BodyInit | null | undefined): number {
37
+ if (body == null) return 0;
38
+ if (typeof body === 'string') return body.length;
39
+ // ArrayBuffer / Uint8Array / ArrayBufferView all carry byteLength.
40
+ const maybe = body as { byteLength?: number };
41
+ if (typeof maybe.byteLength === 'number') return maybe.byteLength;
42
+ // FormData / Blob / ReadableStream — not measured.
43
+ return 0;
44
+ }
45
+
46
+ /** Estimate request bytes from a fetch init. Used by the fetch
47
+ * patch in handlers/network.ts. */
48
+ export function estimateRequestBytes(init?: RequestInit): number {
49
+ return estimateBodyBytes(init?.body);
50
+ }
51
+
52
+ /** Read response bytes from the Content-Length header. Returns
53
+ * 0 if missing or unparseable — undercount-safe. */
54
+ export function estimateResponseBytes(headers: Headers | undefined | null): number {
55
+ if (!headers) return 0;
56
+ const v = headers.get?.('content-length');
57
+ if (!v) return 0;
58
+ const n = parseInt(v, 10);
59
+ return Number.isFinite(n) && n > 0 ? n : 0;
60
+ }
61
+
62
+ function emit(): void {
63
+ if (_bytesSent > 0) {
64
+ emitMetric('runtime.network.bytes_sent', _bytesSent);
65
+ _bytesSent = 0;
66
+ }
67
+ if (_bytesReceived > 0) {
68
+ emitMetric('runtime.network.bytes_received', _bytesReceived);
69
+ _bytesReceived = 0;
70
+ }
71
+ }
72
+
73
+ /** Idempotent start — second call is a no-op. */
74
+ export function startNetworkBytesInstrument(): void {
75
+ if (_timer !== null) return;
76
+ _timer = setInterval(emit, TICK_MS);
77
+ (_timer as unknown as { unref?: () => void }).unref?.();
78
+ }
79
+
80
+ /** Stop the periodic emit. Idempotent. */
81
+ export function stopNetworkBytesInstrument(): void {
82
+ if (_timer !== null) {
83
+ clearInterval(_timer);
84
+ _timer = null;
85
+ }
86
+ }
87
+
88
+ /** Test-only: force one emit tick + reset counters. */
89
+ export function __forceNetworkEmitForTests(): void {
90
+ emit();
91
+ }
92
+
93
+ /** Test-only: peek raw counter state without resetting. */
94
+ export function __peekNetworkCountersForTests(): { sent: number; received: number } {
95
+ return { sent: _bytesSent, received: _bytesReceived };
96
+ }
97
+
98
+ /** Test-only: reset for clean test runs. */
99
+ export function __resetNetworkBytesForTests(): void {
100
+ stopNetworkBytesInstrument();
101
+ _bytesSent = 0;
102
+ _bytesReceived = 0;
103
+ }
@@ -0,0 +1,122 @@
1
+ // v2.1 W2 part 2 — RN runtime metrics flusher + cold-start
2
+ // instrument.
3
+ //
4
+ // The buffer + emit primitives live in @goliapkg/sentori-core
5
+ // (runtime-metrics.ts). This module owns:
6
+ // • the periodic flush timer (30 s, coalesced w/ event flush)
7
+ // • the rebuffer-on-failure recovery path
8
+ // • the cold-start auto-instrument (one-shot at init)
9
+ //
10
+ // Other auto-instrument modules (FPS / heap / route-nav / network
11
+ // bytes) ship in the W2 part 3+ chunks; each is its own file with
12
+ // its own per-tick perf budget test gated as stop-ship in CI.
13
+ //
14
+ // NEVER rule: every public surface here is wrapped in safeFn
15
+ // boundaries by the SDK init layer; the flush timer itself
16
+ // catches all rejections + reports via the circuit breaker.
17
+
18
+ import {
19
+ drainRuntimeMetricsForFlush,
20
+ emitMetric,
21
+ rebufferRuntimeMetrics,
22
+ reportInternal,
23
+ } from '@goliapkg/sentori-core';
24
+
25
+ import { getConfig, isInitialized } from './config';
26
+ import { sendRuntimeMetricsBatch } from './transport';
27
+
28
+ const FLUSH_INTERVAL_MS = 30_000;
29
+
30
+ let _timer: null | ReturnType<typeof setInterval> = null;
31
+ let _coldStartT0: null | number = null;
32
+ let _coldStartEmitted = false;
33
+
34
+ /**
35
+ * Drain core's runtime-metrics ring and POST to
36
+ * /v1/runtime-metrics:batch. Rebuffers on failure so the next
37
+ * tick retries; sustained outages spill into the ring's drop
38
+ * counter which reports to the SDK self-report channel.
39
+ *
40
+ * Returns when the round-trip settles (success or failure). Per
41
+ * the NEVER rule, never throws — failure is logged + rebuffered,
42
+ * the resolved promise's value is undefined.
43
+ */
44
+ export async function flushRuntimeMetrics(): Promise<void> {
45
+ if (!isInitialized()) return;
46
+ const config = getConfig();
47
+ if (!config) return;
48
+ const batch = drainRuntimeMetricsForFlush();
49
+ if (batch.length === 0) return;
50
+ const ok = await sendRuntimeMetricsBatch(config.ingestUrl, config.token, batch);
51
+ if (!ok) {
52
+ rebufferRuntimeMetrics(batch);
53
+ reportInternal('runtime-metrics.flush', new Error('runtime-metrics POST failed'));
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Start the 30 s flush timer. Called once from `init()`. Idempotent
59
+ * — repeated calls are a no-op so users that call
60
+ * `Sentori.init({ metrics: true })` more than once (HMR, fast
61
+ * refresh) don't get multiple timers.
62
+ */
63
+ export function startRuntimeMetricsTimer(): void {
64
+ if (_timer !== null) return;
65
+ _timer = setInterval(() => {
66
+ void flushRuntimeMetrics();
67
+ }, FLUSH_INTERVAL_MS);
68
+ // Don't keep Node alive solely for this timer. RN's setInterval
69
+ // is a NoopRef so this is harmless there; Node + CLI tests
70
+ // benefit so the process can exit cleanly.
71
+ (_timer as unknown as { unref?: () => void }).unref?.();
72
+ }
73
+
74
+ /**
75
+ * Capture the wall-clock at `init()` so cold-start instrumentation
76
+ * has a t0. Called from init() before any other auto-instrument
77
+ * fires. Returns the captured t0 in millis (callers that want to
78
+ * stash + use it later can hold the value).
79
+ */
80
+ export function markColdStartT0(): number {
81
+ if (_coldStartT0 === null) {
82
+ _coldStartT0 = Date.now();
83
+ }
84
+ return _coldStartT0;
85
+ }
86
+
87
+ /**
88
+ * Emit one `runtime.cold_start_ms` metric point. Idempotent per
89
+ * session — the second + later calls are a no-op so route-nav
90
+ * instrument (W2 part 3) can call this safely without worrying
91
+ * about double-counting.
92
+ *
93
+ * Hosts that want a more interactive-pixel-perfect cold-start
94
+ * boundary (TTI vs. raw init→render) can call
95
+ * `markTimeToFullDisplay()` and we emit that separately as
96
+ * `runtime.time_to_full_display_ms` (existing v1 instrument).
97
+ */
98
+ export function emitColdStart(): void {
99
+ if (_coldStartEmitted) return;
100
+ if (_coldStartT0 === null) return;
101
+ const ms = Date.now() - _coldStartT0;
102
+ if (ms <= 0 || ms > 60_000) {
103
+ // Implausible — likely the host called emit before init or
104
+ // we got into a freeze-then-resume situation. Drop silently.
105
+ return;
106
+ }
107
+ emitMetric('runtime.cold_start_ms', ms);
108
+ _coldStartEmitted = true;
109
+ }
110
+
111
+ /**
112
+ * Test-only escape hatch: stop the timer + reset cold-start state
113
+ * so vitest / bun:test teardown doesn't leak intervals across runs.
114
+ */
115
+ export function __resetRuntimeMetricsRnForTests(): void {
116
+ if (_timer !== null) {
117
+ clearInterval(_timer);
118
+ _timer = null;
119
+ }
120
+ _coldStartT0 = null;
121
+ _coldStartEmitted = false;
122
+ }
package/src/track.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  // Hosts that don't use react-navigation can call
18
18
  // `sentori.track('$pageview', { route: 'Cart' })` themselves.
19
19
 
20
+ import { addInternalBreadcrumb } from './breadcrumbs';
20
21
  import { getCurrentUserId } from './capture';
21
22
  import { getConfig, isInitialized } from './config';
22
23
  import { sendTrackBatch } from './transport';
@@ -74,6 +75,14 @@ export function track(name: string, props?: TrackProps, route?: string): void {
74
75
  userId: getCurrentUserId(),
75
76
  };
76
77
  _buf.push(ev);
78
+ // v2.0 W3 — auto-breadcrumb. When `init.capture.trackAutoBreadcrumb`
79
+ // is `true`, push a `{ type: 'track', data: { name, props } }`
80
+ // breadcrumb so the customer journey leading up to a later
81
+ // `captureException` / `captureMessage` is visible in the dashboard.
82
+ // Defaults off — see Config.trackAutoBreadcrumb docstring.
83
+ if (config?.trackAutoBreadcrumb === true) {
84
+ addInternalBreadcrumb('track', props ? { name, props } : { name });
85
+ }
77
86
  if (_buf.length >= MAX_BUFFER) {
78
87
  void flushTrack();
79
88
  }
package/src/transport.ts CHANGED
@@ -330,6 +330,45 @@ export const sendMetricsBatch = async (
330
330
  }
331
331
  };
332
332
 
333
+ /**
334
+ * v2.1 W2 — POST a batched set of auto-instrument runtime metric
335
+ * points. Sibling of sendMetricsBatch; different endpoint
336
+ * (`/v1/runtime-metrics:batch`) because the storage shape +
337
+ * validation rules + rate-limit budget differ — see
338
+ * docs/design/v2-metrics.md.
339
+ *
340
+ * Returns true on 2xx so the caller can leave the batch drained;
341
+ * returns false on anything else (network error / non-2xx) so the
342
+ * caller rebuffer-and-retries on the next flush via
343
+ * `rebufferRuntimeMetrics(batch)`.
344
+ */
345
+ export const sendRuntimeMetricsBatch = async (
346
+ ingestUrl: string,
347
+ token: string,
348
+ metrics: Array<{
349
+ name: string;
350
+ tags?: Record<string, string>;
351
+ ts: string;
352
+ value: number;
353
+ }>,
354
+ ): Promise<boolean> => {
355
+ if (metrics.length === 0) return true;
356
+ try {
357
+ const resp = await fetch(`${ingestUrl}/v1/runtime-metrics:batch`, {
358
+ body: JSON.stringify({ metrics }),
359
+ headers: {
360
+ Authorization: `Bearer ${token}`,
361
+ 'Content-Type': 'application/json',
362
+ 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
363
+ },
364
+ method: 'POST',
365
+ });
366
+ return resp.ok;
367
+ } catch {
368
+ return false;
369
+ }
370
+ };
371
+
333
372
  /**
334
373
  * v0.8.2 — submit a user-supplied bug report. Fire-and-forget; resolves
335
374
  * with the server-assigned id on success or `null` on any failure.