@goliapkg/sentori-react-native 0.7.5 → 0.8.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/lib/bundle-info.d.ts +12 -0
- package/lib/bundle-info.d.ts.map +1 -0
- package/lib/bundle-info.js +73 -0
- package/lib/bundle-info.js.map +1 -0
- package/lib/capture.d.ts.map +1 -1
- package/lib/capture.js +6 -0
- package/lib/capture.js.map +1 -1
- package/lib/feature-flags.d.ts +9 -0
- package/lib/feature-flags.d.ts.map +1 -0
- package/lib/feature-flags.js +44 -0
- package/lib/feature-flags.js.map +1 -0
- package/lib/handlers/network.d.ts +9 -1
- package/lib/handlers/network.d.ts.map +1 -1
- package/lib/handlers/network.js +189 -18
- package/lib/handlers/network.js.map +1 -1
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +16 -1
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +26 -2
- package/lib/init.js.map +1 -1
- package/lib/launch-crash-guard.d.ts +37 -0
- package/lib/launch-crash-guard.d.ts.map +1 -0
- package/lib/launch-crash-guard.js +163 -0
- package/lib/launch-crash-guard.js.map +1 -0
- package/lib/measure.d.ts +4 -0
- package/lib/measure.d.ts.map +1 -0
- package/lib/measure.js +25 -0
- package/lib/measure.js.map +1 -0
- package/lib/metrics.d.ts +9 -0
- package/lib/metrics.d.ts.map +1 -0
- package/lib/metrics.js +64 -0
- package/lib/metrics.js.map +1 -0
- package/lib/rage-tap-detector.d.ts +8 -0
- package/lib/rage-tap-detector.d.ts.map +1 -0
- package/lib/rage-tap-detector.js +21 -0
- package/lib/rage-tap-detector.js.map +1 -0
- package/lib/rage-tap.d.ts +6 -0
- package/lib/rage-tap.d.ts.map +1 -0
- package/lib/rage-tap.js +35 -0
- package/lib/rage-tap.js.map +1 -0
- package/lib/transport.d.ts +12 -0
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +24 -0
- package/lib/transport.js.map +1 -1
- package/package.json +11 -3
- package/src/__tests__/feature-flags.test.ts +55 -0
- package/src/__tests__/measure.test.ts +45 -0
- package/src/__tests__/network-graphql.test.ts +75 -0
- package/src/__tests__/rage-tap.test.ts +38 -0
- package/src/bundle-info.ts +95 -0
- package/src/capture.ts +6 -0
- package/src/feature-flags.ts +47 -0
- package/src/handlers/network.ts +198 -18
- package/src/index.ts +29 -0
- package/src/init.ts +57 -2
- package/src/launch-crash-guard.ts +221 -0
- package/src/measure.ts +28 -0
- package/src/metrics.ts +74 -0
- package/src/rage-tap-detector.ts +26 -0
- package/src/rage-tap.tsx +48 -0
- package/src/transport.ts +32 -0
package/src/measure.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// v0.9.0 #14 — `sentori.measureFn(name, fn)`. Profile-lite. Wrap an
|
|
2
|
+
// async (or sync) function call in a span so it shows on the issue
|
|
3
|
+
// detail trace waterfall without writing the boilerplate every time.
|
|
4
|
+
// The full Hermes-sampler profiler (#4) is the deep version of this
|
|
5
|
+
// idea; `measureFn` is the cheap version that doesn't need a native
|
|
6
|
+
// module.
|
|
7
|
+
|
|
8
|
+
import { startSpan } from '@goliapkg/sentori-core';
|
|
9
|
+
|
|
10
|
+
export async function measureFn<T>(
|
|
11
|
+
name: string,
|
|
12
|
+
fn: () => Promise<T> | T,
|
|
13
|
+
opts?: { tags?: Record<string, string> },
|
|
14
|
+
): Promise<T> {
|
|
15
|
+
const span = startSpan('sentori.measureFn', {
|
|
16
|
+
name,
|
|
17
|
+
tags: opts?.tags ?? {},
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const result = await fn();
|
|
21
|
+
span.finish({ status: 'ok' });
|
|
22
|
+
return result;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (e instanceof Error) span.setTag('error.message', e.message);
|
|
25
|
+
span.finish({ status: 'error' });
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// v0.8.3 — custom metrics buffer.
|
|
2
|
+
//
|
|
3
|
+
// `recordMetric(name, value, tags?)` pushes a point into a fixed-size
|
|
4
|
+
// ring. A timer flushes the ring every 30 s (or when the buffer is
|
|
5
|
+
// full); captureException also forces a flush so the metrics line up
|
|
6
|
+
// with the error event in the dashboard. Best-effort: a flush failure
|
|
7
|
+
// drops the batch on the floor — metrics aren't critical telemetry.
|
|
8
|
+
//
|
|
9
|
+
// Why not one fetch per point: noisy loops (`recordMetric('frame', 1)`
|
|
10
|
+
// in a render hook) would burn the JS thread + saturate the device's
|
|
11
|
+
// outgoing connection pool. Batching makes the SDK safe to use as a
|
|
12
|
+
// cheap counter primitive.
|
|
13
|
+
|
|
14
|
+
import { getConfig, isInitialized } from './config';
|
|
15
|
+
import { sendMetricsBatch } from './transport';
|
|
16
|
+
|
|
17
|
+
type Point = {
|
|
18
|
+
name: string;
|
|
19
|
+
tags?: Record<string, string>;
|
|
20
|
+
ts: string;
|
|
21
|
+
value: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const MAX_BUFFER = 500;
|
|
25
|
+
const FLUSH_INTERVAL_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
let _buf: Point[] = [];
|
|
28
|
+
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
29
|
+
|
|
30
|
+
export function recordMetric(
|
|
31
|
+
name: string,
|
|
32
|
+
value: number,
|
|
33
|
+
tags?: Record<string, string>,
|
|
34
|
+
): void {
|
|
35
|
+
if (!isInitialized()) return;
|
|
36
|
+
if (typeof name !== 'string' || name.length === 0 || name.length > 200) return;
|
|
37
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return;
|
|
38
|
+
if (tags && Object.keys(tags).length > 20) return;
|
|
39
|
+
_buf.push({ name, tags, ts: new Date().toISOString(), value });
|
|
40
|
+
if (_buf.length >= MAX_BUFFER) {
|
|
41
|
+
void flushMetrics();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function flushMetrics(): Promise<void> {
|
|
46
|
+
if (_buf.length === 0) return;
|
|
47
|
+
const config = getConfig();
|
|
48
|
+
if (!config) return;
|
|
49
|
+
const batch = _buf;
|
|
50
|
+
_buf = [];
|
|
51
|
+
await sendMetricsBatch(config.ingestUrl, config.token, batch);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start the 30 s flush timer. Called once from `init()`. Idempotent.
|
|
56
|
+
* `clearMetricsTimer` is exposed for tests / teardown.
|
|
57
|
+
*/
|
|
58
|
+
export function startMetricsTimer(): void {
|
|
59
|
+
if (_timer !== null) return;
|
|
60
|
+
_timer = setInterval(() => {
|
|
61
|
+
void flushMetrics();
|
|
62
|
+
}, FLUSH_INTERVAL_MS);
|
|
63
|
+
// Don't keep the process alive solely for this timer (Node would).
|
|
64
|
+
// In RN setInterval is a NoopRef so this is harmless there.
|
|
65
|
+
(_timer as unknown as { unref?: () => void }).unref?.();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function __resetMetricsForTests(): void {
|
|
69
|
+
if (_timer !== null) {
|
|
70
|
+
clearInterval(_timer);
|
|
71
|
+
_timer = null;
|
|
72
|
+
}
|
|
73
|
+
_buf = [];
|
|
74
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// v0.9.0 #12 — pure rage-tap detection logic. Lives outside the .tsx
|
|
2
|
+
// component so unit tests can import it without dragging in
|
|
3
|
+
// `react-native` (whose flow syntax breaks bun:test parser).
|
|
4
|
+
|
|
5
|
+
export const RAGE_WINDOW_MS = 800;
|
|
6
|
+
export const RAGE_THRESHOLD = 3;
|
|
7
|
+
|
|
8
|
+
/** Given the per-target recent-tap buckets, a target id, and `now`,
|
|
9
|
+
* return `true` iff this tap crosses the rage threshold. Side
|
|
10
|
+
* effect: writes/clears the bucket inside `map` so successive
|
|
11
|
+
* taps after a triggered rage event don't immediately re-trigger. */
|
|
12
|
+
export function recordTap(
|
|
13
|
+
map: Map<number, number[]>,
|
|
14
|
+
target: number,
|
|
15
|
+
now: number,
|
|
16
|
+
): boolean {
|
|
17
|
+
const previous = map.get(target) ?? [];
|
|
18
|
+
const fresh = previous.filter((t) => now - t <= RAGE_WINDOW_MS);
|
|
19
|
+
fresh.push(now);
|
|
20
|
+
if (fresh.length >= RAGE_THRESHOLD) {
|
|
21
|
+
map.delete(target);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
map.set(target, fresh);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
package/src/rage-tap.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// v0.9.0 #12 — rage-tap / multi-click detection.
|
|
2
|
+
//
|
|
3
|
+
// Wrap your app root (typically next to ErrorBoundary) with
|
|
4
|
+
// `<sentori.RageTapCapture>{children}</sentori.RageTapCapture>`.
|
|
5
|
+
// We listen to bubble-phase `onTouchEnd` and emit a `ui.multiClick`
|
|
6
|
+
// breadcrumb when the same native target receives ≥ 3 taps within
|
|
7
|
+
// 800 ms. Pure observation — no event capture, no gesture
|
|
8
|
+
// interference; existing Touchables / Pressables / GestureHandler
|
|
9
|
+
// continue to fire normally.
|
|
10
|
+
|
|
11
|
+
import React, { useCallback, useRef } from 'react';
|
|
12
|
+
import { View, type GestureResponderEvent, type ViewProps } from 'react-native';
|
|
13
|
+
|
|
14
|
+
import { addBreadcrumb } from './breadcrumbs';
|
|
15
|
+
import {
|
|
16
|
+
RAGE_THRESHOLD,
|
|
17
|
+
RAGE_WINDOW_MS,
|
|
18
|
+
recordTap,
|
|
19
|
+
} from './rage-tap-detector';
|
|
20
|
+
|
|
21
|
+
export function RageTapCapture({
|
|
22
|
+
children,
|
|
23
|
+
...rest
|
|
24
|
+
}: ViewProps & { children?: React.ReactNode }): React.JSX.Element {
|
|
25
|
+
const recent = useRef<Map<number, number[]>>(new Map());
|
|
26
|
+
|
|
27
|
+
const onTouchEnd = useCallback((e: GestureResponderEvent) => {
|
|
28
|
+
const target = e.nativeEvent?.target;
|
|
29
|
+
if (typeof target !== 'number') return;
|
|
30
|
+
if (recordTap(recent.current, target, Date.now())) {
|
|
31
|
+
addBreadcrumb({
|
|
32
|
+
type: 'user',
|
|
33
|
+
data: {
|
|
34
|
+
kind: 'ui.multiClick',
|
|
35
|
+
target: String(target),
|
|
36
|
+
taps: RAGE_THRESHOLD,
|
|
37
|
+
windowMs: RAGE_WINDOW_MS,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View {...rest} onTouchEnd={onTouchEnd}>
|
|
45
|
+
{children}
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/transport.ts
CHANGED
|
@@ -242,6 +242,38 @@ export const sendSessionPing = async (
|
|
|
242
242
|
}
|
|
243
243
|
};
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* v0.8.3 — flush a batched set of custom metrics. The host SDK
|
|
247
|
+
* batches recordMetric() calls into a fixed-size ring (drained on
|
|
248
|
+
* a timer + at next captureException) so a busy loop doesn't spin
|
|
249
|
+
* up one fetch per point. Best-effort, no retry.
|
|
250
|
+
*/
|
|
251
|
+
export const sendMetricsBatch = async (
|
|
252
|
+
ingestUrl: string,
|
|
253
|
+
token: string,
|
|
254
|
+
metrics: Array<{
|
|
255
|
+
name: string;
|
|
256
|
+
tags?: Record<string, string>;
|
|
257
|
+
ts?: string;
|
|
258
|
+
value: number;
|
|
259
|
+
}>,
|
|
260
|
+
): Promise<void> => {
|
|
261
|
+
if (metrics.length === 0) return;
|
|
262
|
+
try {
|
|
263
|
+
await fetch(`${ingestUrl}/v1/metrics:batch`, {
|
|
264
|
+
body: JSON.stringify({ metrics }),
|
|
265
|
+
headers: {
|
|
266
|
+
Authorization: `Bearer ${token}`,
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
269
|
+
},
|
|
270
|
+
method: 'POST',
|
|
271
|
+
});
|
|
272
|
+
} catch {
|
|
273
|
+
// best-effort
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
245
277
|
/**
|
|
246
278
|
* v0.8.2 — submit a user-supplied bug report. Fire-and-forget; resolves
|
|
247
279
|
* with the server-assigned id on success or `null` on any failure.
|