@goliapkg/sentori-react-native 3.1.0 → 5.0.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/android/build.gradle +21 -0
- package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
- package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
- package/android/src/test/resources/robolectric.properties +10 -0
- package/lib/config.d.ts +16 -47
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +3 -0
- package/lib/config.js.map +1 -1
- package/lib/device.d.ts +5 -0
- package/lib/device.d.ts.map +1 -0
- package/lib/device.js +29 -0
- package/lib/device.js.map +1 -0
- package/lib/emit-hooks.d.ts +7 -0
- package/lib/emit-hooks.d.ts.map +1 -0
- package/lib/emit-hooks.js +24 -0
- package/lib/emit-hooks.js.map +1 -0
- package/lib/error-boundary.d.ts.map +1 -1
- package/lib/error-boundary.js +1 -1
- package/lib/error-boundary.js.map +1 -1
- package/lib/feature-flags.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.d.ts +1 -1
- package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.js +2 -0
- package/lib/handlers/dev-symbolicate.js.map +1 -1
- package/lib/handlers/global.js +1 -1
- package/lib/handlers/global.js.map +1 -1
- package/lib/handlers/lifecycle.js +3 -3
- package/lib/handlers/lifecycle.js.map +1 -1
- package/lib/handlers/network.d.ts +2 -2
- package/lib/handlers/network.d.ts.map +1 -1
- package/lib/handlers/network.js +69 -187
- package/lib/handlers/network.js.map +1 -1
- package/lib/handlers/promise.js +1 -1
- package/lib/handlers/promise.js.map +1 -1
- package/lib/handlers/screenshot.d.ts.map +1 -1
- package/lib/handlers/screenshot.js +3 -12
- package/lib/handlers/screenshot.js.map +1 -1
- package/lib/index.d.ts +31 -109
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +51 -103
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +3 -150
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +88 -261
- package/lib/init.js.map +1 -1
- package/lib/long-task-monitor.d.ts +1 -7
- package/lib/long-task-monitor.d.ts.map +1 -1
- package/lib/long-task-monitor.js +37 -58
- package/lib/long-task-monitor.js.map +1 -1
- package/lib/mobile-vitals.d.ts +3 -31
- package/lib/mobile-vitals.d.ts.map +1 -1
- package/lib/mobile-vitals.js +22 -61
- package/lib/mobile-vitals.js.map +1 -1
- package/lib/native-pending.d.ts +3 -0
- package/lib/native-pending.d.ts.map +1 -0
- package/lib/native-pending.js +64 -0
- package/lib/native-pending.js.map +1 -0
- package/lib/navigation.d.ts +3 -17
- package/lib/navigation.d.ts.map +1 -1
- package/lib/navigation.js +32 -133
- package/lib/navigation.js.map +1 -1
- package/lib/netinfo.d.ts +1 -1
- package/lib/netinfo.d.ts.map +1 -1
- package/lib/netinfo.js +2 -2
- package/lib/netinfo.js.map +1 -1
- package/lib/push.d.ts.map +1 -1
- package/lib/push.js +2 -4
- package/lib/push.js.map +1 -1
- package/lib/rage-tap.d.ts.map +1 -1
- package/lib/rage-tap.js +28 -23
- package/lib/rage-tap.js.map +1 -1
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +3 -24
- package/lib/replay.js.map +1 -1
- package/lib/scope.d.ts +7 -0
- package/lib/scope.d.ts.map +1 -0
- package/lib/scope.js +41 -0
- package/lib/scope.js.map +1 -0
- package/lib/transport.d.ts +19 -92
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +75 -288
- package/lib/transport.js.map +1 -1
- package/lib/verbs.d.ts +16 -0
- package/lib/verbs.d.ts.map +1 -0
- package/lib/verbs.js +153 -0
- package/lib/verbs.js.map +1 -0
- package/package.json +5 -5
- package/src/__tests__/iron-rule.test.ts +128 -0
- package/src/__tests__/screenshot.test.ts +0 -2
- package/src/__tests__/transport.test.ts +66 -213
- package/src/__tests__/verbs.test.ts +91 -0
- package/src/config.ts +19 -47
- package/src/device.ts +36 -0
- package/src/emit-hooks.ts +30 -0
- package/src/error-boundary.tsx +1 -1
- package/src/handlers/dev-symbolicate.ts +2 -1
- package/src/handlers/global.ts +1 -1
- package/src/handlers/lifecycle.ts +3 -3
- package/src/handlers/network.ts +95 -211
- package/src/handlers/promise.ts +1 -1
- package/src/handlers/screenshot.ts +3 -11
- package/src/index.ts +81 -215
- package/src/init.ts +94 -446
- package/src/long-task-monitor.ts +42 -64
- package/src/mobile-vitals.ts +22 -82
- package/src/native-pending.ts +92 -0
- package/src/navigation.ts +34 -142
- package/src/netinfo.ts +3 -3
- package/src/push.ts +2 -5
- package/src/rage-tap.tsx +29 -26
- package/src/replay.ts +3 -20
- package/src/scope.ts +50 -0
- package/src/transport.ts +84 -371
- package/src/verbs.ts +198 -0
- package/lib/breadcrumbs.d.ts +0 -18
- package/lib/breadcrumbs.d.ts.map +0 -1
- package/lib/breadcrumbs.js +0 -42
- package/lib/breadcrumbs.js.map +0 -1
- package/lib/bundle-info.d.ts +0 -12
- package/lib/bundle-info.d.ts.map +0 -1
- package/lib/bundle-info.js +0 -73
- package/lib/bundle-info.js.map +0 -1
- package/lib/capture.d.ts +0 -140
- package/lib/capture.d.ts.map +0 -1
- package/lib/capture.js +0 -511
- package/lib/capture.js.map +0 -1
- package/lib/compat/sentry.d.ts +0 -126
- package/lib/compat/sentry.d.ts.map +0 -1
- package/lib/compat/sentry.js +0 -332
- package/lib/compat/sentry.js.map +0 -1
- package/lib/control-channel.d.ts +0 -6
- package/lib/control-channel.d.ts.map +0 -1
- package/lib/control-channel.js +0 -96
- package/lib/control-channel.js.map +0 -1
- package/lib/expo-compat.d.ts +0 -270
- package/lib/expo-compat.d.ts.map +0 -1
- package/lib/expo-compat.js +0 -500
- package/lib/expo-compat.js.map +0 -1
- package/lib/feedback-widget.d.ts +0 -35
- package/lib/feedback-widget.d.ts.map +0 -1
- package/lib/feedback-widget.js +0 -193
- package/lib/feedback-widget.js.map +0 -1
- package/lib/feedback.d.ts +0 -2
- package/lib/feedback.d.ts.map +0 -1
- package/lib/feedback.js +0 -17
- package/lib/feedback.js.map +0 -1
- package/lib/heartbeat.d.ts +0 -9
- package/lib/heartbeat.d.ts.map +0 -1
- package/lib/heartbeat.js +0 -138
- package/lib/heartbeat.js.map +0 -1
- package/lib/index.js.bak +0 -64
- package/lib/install-id.d.ts +0 -17
- package/lib/install-id.d.ts.map +0 -1
- package/lib/install-id.js +0 -125
- package/lib/install-id.js.map +0 -1
- package/lib/lifecycle.d.ts +0 -28
- package/lib/lifecycle.d.ts.map +0 -1
- package/lib/lifecycle.js +0 -73
- package/lib/lifecycle.js.map +0 -1
- package/lib/mask.d.ts +0 -16
- package/lib/mask.d.ts.map +0 -1
- package/lib/mask.js +0 -36
- package/lib/mask.js.map +0 -1
- package/lib/measure.d.ts +0 -4
- package/lib/measure.d.ts.map +0 -1
- package/lib/measure.js +0 -25
- package/lib/measure.js.map +0 -1
- package/lib/metrics.d.ts +0 -24
- package/lib/metrics.d.ts.map +0 -1
- package/lib/metrics.js +0 -81
- package/lib/metrics.js.map +0 -1
- package/lib/pre-crash-sentinel.d.ts +0 -12
- package/lib/pre-crash-sentinel.d.ts.map +0 -1
- package/lib/pre-crash-sentinel.js +0 -116
- package/lib/pre-crash-sentinel.js.map +0 -1
- package/lib/report-security.d.ts +0 -40
- package/lib/report-security.d.ts.map +0 -1
- package/lib/report-security.js +0 -159
- package/lib/report-security.js.map +0 -1
- package/lib/runtime-metrics-fps.d.ts +0 -19
- package/lib/runtime-metrics-fps.d.ts.map +0 -1
- package/lib/runtime-metrics-fps.js +0 -128
- package/lib/runtime-metrics-fps.js.map +0 -1
- package/lib/runtime-metrics-heap.d.ts +0 -13
- package/lib/runtime-metrics-heap.d.ts.map +0 -1
- package/lib/runtime-metrics-heap.js +0 -67
- package/lib/runtime-metrics-heap.js.map +0 -1
- package/lib/runtime-metrics-network.d.ts +0 -23
- package/lib/runtime-metrics-network.d.ts.map +0 -1
- package/lib/runtime-metrics-network.js +0 -99
- package/lib/runtime-metrics-network.js.map +0 -1
- package/lib/runtime-metrics.d.ts +0 -43
- package/lib/runtime-metrics.d.ts.map +0 -1
- package/lib/runtime-metrics.js +0 -115
- package/lib/runtime-metrics.js.map +0 -1
- package/lib/sample-profiler.d.ts +0 -21
- package/lib/sample-profiler.d.ts.map +0 -1
- package/lib/sample-profiler.js +0 -152
- package/lib/sample-profiler.js.map +0 -1
- package/lib/sentinel-context.d.ts +0 -4
- package/lib/sentinel-context.d.ts.map +0 -1
- package/lib/sentinel-context.js +0 -52
- package/lib/sentinel-context.js.map +0 -1
- package/lib/session-tracker.d.ts +0 -6
- package/lib/session-tracker.d.ts.map +0 -1
- package/lib/session-tracker.js +0 -50
- package/lib/session-tracker.js.map +0 -1
- package/lib/stack.d.ts +0 -3
- package/lib/stack.d.ts.map +0 -1
- package/lib/stack.js +0 -7
- package/lib/stack.js.map +0 -1
- package/lib/state-snapshots.d.ts +0 -33
- package/lib/state-snapshots.d.ts.map +0 -1
- package/lib/state-snapshots.js +0 -109
- package/lib/state-snapshots.js.map +0 -1
- package/lib/track.d.ts +0 -34
- package/lib/track.d.ts.map +0 -1
- package/lib/track.js +0 -107
- package/lib/track.js.map +0 -1
- package/lib/trail.d.ts +0 -12
- package/lib/trail.d.ts.map +0 -1
- package/lib/trail.js +0 -28
- package/lib/trail.js.map +0 -1
- package/lib/trust-score.d.ts +0 -20
- package/lib/trust-score.d.ts.map +0 -1
- package/lib/trust-score.js +0 -151
- package/lib/trust-score.js.map +0 -1
- package/lib/types.d.ts +0 -2
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -4
- package/lib/types.js.map +0 -1
- package/lib/uuid.d.ts +0 -2
- package/lib/uuid.d.ts.map +0 -1
- package/lib/uuid.js +0 -3
- package/lib/uuid.js.map +0 -1
- package/src/__tests__/before-send.test.ts +0 -72
- package/src/__tests__/breadcrumbs.test.ts +0 -44
- package/src/__tests__/capture-replay.test.ts +0 -150
- package/src/__tests__/compat-sentry.test.ts +0 -121
- package/src/__tests__/control-channel.test.ts +0 -111
- package/src/__tests__/install-id.test.ts +0 -60
- package/src/__tests__/long-task-monitor.test.ts +0 -59
- package/src/__tests__/measure.test.ts +0 -45
- package/src/__tests__/navigation.test.ts +0 -175
- package/src/__tests__/push.test.ts +0 -238
- package/src/__tests__/report-security.test.ts +0 -106
- package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
- package/src/__tests__/runtime-metrics-network.test.ts +0 -78
- package/src/__tests__/sample-profiler.test.ts +0 -59
- package/src/__tests__/stack.test.ts +0 -45
- package/src/__tests__/state-snapshots.test.ts +0 -119
- package/src/__tests__/tracing.test.ts +0 -201
- package/src/__tests__/track.test.ts +0 -91
- package/src/__tests__/uuid.test.ts +0 -41
- package/src/breadcrumbs.ts +0 -57
- package/src/bundle-info.ts +0 -95
- package/src/capture.ts +0 -660
- package/src/compat/sentry.ts +0 -490
- package/src/control-channel.ts +0 -98
- package/src/expo-compat.ts +0 -698
- package/src/feedback-widget.tsx +0 -316
- package/src/feedback.ts +0 -21
- package/src/heartbeat.ts +0 -157
- package/src/install-id.ts +0 -146
- package/src/lifecycle.ts +0 -76
- package/src/mask.ts +0 -41
- package/src/measure.ts +0 -28
- package/src/metrics.ts +0 -94
- package/src/pre-crash-sentinel.ts +0 -140
- package/src/report-security.ts +0 -165
- package/src/runtime-metrics-fps.ts +0 -137
- package/src/runtime-metrics-heap.ts +0 -78
- package/src/runtime-metrics-network.ts +0 -103
- package/src/runtime-metrics.ts +0 -122
- package/src/sample-profiler.ts +0 -169
- package/src/sentinel-context.ts +0 -60
- package/src/session-tracker.ts +0 -53
- package/src/stack.ts +0 -10
- package/src/state-snapshots.ts +0 -133
- package/src/track.ts +0 -123
- package/src/trail.ts +0 -32
- package/src/trust-score.ts +0 -176
- package/src/types.ts +0 -21
- package/src/uuid.ts +0 -2
package/src/transport.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// Event transport — the only place the SDK talks to the network.
|
|
2
|
+
//
|
|
3
|
+
// Quiet by default, complete when it matters (design.md §4): events
|
|
4
|
+
// batch on a 5 s timer or a 10-deep queue, whichever first; assert
|
|
5
|
+
// pass-counts piggyback on whatever batch goes out next (never their
|
|
6
|
+
// own request); failures back off and finally persist to an offline
|
|
7
|
+
// queue drained on next launch. Nothing here ever throws into the
|
|
8
|
+
// host app.
|
|
9
|
+
|
|
10
|
+
import type { AssertStat, BatchEnvelope, WireEvent } from '@goliapkg/sentori-core';
|
|
11
|
+
import { logger } from '@goliapkg/sentori-core';
|
|
2
12
|
|
|
3
13
|
import { getConfig } from './config';
|
|
4
|
-
import { isLiveMode } from './control-channel';
|
|
5
14
|
import { isAnyNativeModuleLinked } from './native-loader';
|
|
6
|
-
import type { Event } from './types';
|
|
7
15
|
|
|
8
16
|
const FLUSH_INTERVAL_MS = 5_000;
|
|
9
17
|
const BATCH_SIZE = 10;
|
|
@@ -11,28 +19,15 @@ const MAX_RETRY = 3;
|
|
|
11
19
|
const STORAGE_KEY = '@sentori/pending';
|
|
12
20
|
const MAX_PERSISTED = 1000;
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
// them on their own timer, cap each request at the server's per-batch
|
|
16
|
-
// limit, and drop on failure rather than persisting offline.
|
|
17
|
-
const SPAN_FLUSH_INTERVAL_MS = 10_000;
|
|
18
|
-
const SPAN_BATCH_MAX = 200;
|
|
22
|
+
const SDK_VERSION = '5.0.0';
|
|
19
23
|
|
|
20
|
-
let _queue:
|
|
24
|
+
let _queue: WireEvent[] = [];
|
|
25
|
+
let _assertStats = new Map<string, AssertStat>();
|
|
21
26
|
let _flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
-
let _spanTimer: ReturnType<typeof setInterval> | null = null;
|
|
23
27
|
let _started = false;
|
|
24
28
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
export const enqueue = (event: Event): void => {
|
|
29
|
+
export const enqueue = (event: WireEvent): void => {
|
|
28
30
|
_queue.push(event);
|
|
29
|
-
// v1.1 +S7 升级 — when the dashboard has armed live-debug for the
|
|
30
|
-
// current user, flush immediately instead of waiting for the 5 s
|
|
31
|
-
// batch interval. Dashboard sees each event with sub-second latency.
|
|
32
|
-
if (isLiveMode()) {
|
|
33
|
-
void flush();
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
31
|
if (_queue.length >= BATCH_SIZE) {
|
|
37
32
|
void flush();
|
|
38
33
|
} else if (!_flushTimer) {
|
|
@@ -43,73 +38,58 @@ export const enqueue = (event: Event): void => {
|
|
|
43
38
|
}
|
|
44
39
|
};
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await sendSpansOnce(chunk, config.ingestUrl, config.token);
|
|
65
|
-
} catch {
|
|
66
|
-
// drop the remaining chunks — span uploads aren't worth retrying
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
41
|
+
/**
|
|
42
|
+
* Count an assert outcome. Pass-counts NEVER become events — they
|
|
43
|
+
* aggregate here and ride the next batch envelope (the liveness
|
|
44
|
+
* ledger without a heartbeat flood).
|
|
45
|
+
*/
|
|
46
|
+
export const countAssert = (name: string, ok: boolean, release: string): void => {
|
|
47
|
+
const key = `${name}${release}`;
|
|
48
|
+
const cur = _assertStats.get(key) ?? { name, release, passDelta: 0, failDelta: 0 };
|
|
49
|
+
if (ok) cur.passDelta += 1;
|
|
50
|
+
else cur.failDelta = (cur.failDelta ?? 0) + 1;
|
|
51
|
+
_assertStats.set(key, cur);
|
|
52
|
+
// Stats with no event traffic still ship eventually, on a lazy
|
|
53
|
+
// timer six times the batch interval.
|
|
54
|
+
if (!_flushTimer && _queue.length === 0) {
|
|
55
|
+
_flushTimer = setTimeout(() => {
|
|
56
|
+
_flushTimer = null;
|
|
57
|
+
void flush();
|
|
58
|
+
}, FLUSH_INTERVAL_MS * 6);
|
|
69
59
|
}
|
|
70
60
|
};
|
|
71
61
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
ingestUrl: string,
|
|
75
|
-
token: string,
|
|
76
|
-
): Promise<void> => {
|
|
77
|
-
const resp = await fetch(`${ingestUrl}/v1/spans:batch`, {
|
|
78
|
-
method: 'POST',
|
|
79
|
-
headers: {
|
|
80
|
-
'Content-Type': 'application/json',
|
|
81
|
-
Authorization: `Bearer ${token}`,
|
|
82
|
-
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
83
|
-
},
|
|
84
|
-
body: JSON.stringify({ spans }),
|
|
85
|
-
});
|
|
86
|
-
// 5xx: server overloaded. 4xx (bad token / quota / oversized): also
|
|
87
|
-
// pointless to keep sending. Either way, stop the rest of the batch.
|
|
88
|
-
if (resp.status >= 400) throw new Error(`spans-${resp.status}`);
|
|
62
|
+
export const startTransport = (): void => {
|
|
63
|
+
_started = true;
|
|
89
64
|
};
|
|
90
65
|
|
|
91
66
|
export const flush = async (): Promise<void> => {
|
|
92
67
|
if (!_started) return;
|
|
93
|
-
if (_queue.length === 0) return;
|
|
94
|
-
|
|
95
68
|
const config = getConfig();
|
|
96
69
|
if (!config) return;
|
|
97
70
|
|
|
98
|
-
const
|
|
71
|
+
const events = _queue.splice(0, _queue.length);
|
|
72
|
+
const stats = [..._assertStats.values()];
|
|
73
|
+
_assertStats = new Map();
|
|
99
74
|
if (_flushTimer) {
|
|
100
75
|
clearTimeout(_flushTimer);
|
|
101
76
|
_flushTimer = null;
|
|
102
77
|
}
|
|
78
|
+
if (events.length === 0 && stats.length === 0) return;
|
|
79
|
+
|
|
80
|
+
const envelope: BatchEnvelope = { events };
|
|
81
|
+
if (stats.length > 0) envelope.assertStats = stats;
|
|
103
82
|
|
|
104
83
|
try {
|
|
105
|
-
await sendWithRetry(
|
|
84
|
+
await sendWithRetry(envelope, config.ingestUrl, config.token);
|
|
106
85
|
} catch {
|
|
107
|
-
|
|
86
|
+
// Events survive offline; assert deltas are cheap enough to lose.
|
|
87
|
+
await persist(events);
|
|
108
88
|
}
|
|
109
89
|
};
|
|
110
90
|
|
|
111
91
|
const sendWithRetry = async (
|
|
112
|
-
|
|
92
|
+
envelope: BatchEnvelope,
|
|
113
93
|
ingestUrl: string,
|
|
114
94
|
token: string,
|
|
115
95
|
): Promise<void> => {
|
|
@@ -117,7 +97,7 @@ const sendWithRetry = async (
|
|
|
117
97
|
let delayMs = 1000;
|
|
118
98
|
while (true) {
|
|
119
99
|
try {
|
|
120
|
-
await sendOnce(
|
|
100
|
+
await sendOnce(envelope, ingestUrl, token);
|
|
121
101
|
return;
|
|
122
102
|
} catch (e) {
|
|
123
103
|
attempt++;
|
|
@@ -129,22 +109,18 @@ const sendWithRetry = async (
|
|
|
129
109
|
};
|
|
130
110
|
|
|
131
111
|
const sendOnce = async (
|
|
132
|
-
|
|
112
|
+
envelope: BatchEnvelope,
|
|
133
113
|
ingestUrl: string,
|
|
134
114
|
token: string,
|
|
135
115
|
): Promise<void> => {
|
|
136
|
-
const
|
|
137
|
-
events.length === 1 ? `${ingestUrl}/v1/events` : `${ingestUrl}/v1/events:batch`;
|
|
138
|
-
const body = events.length === 1 ? events[0] : { events };
|
|
139
|
-
|
|
140
|
-
const resp = await fetch(url, {
|
|
116
|
+
const resp = await fetch(`${ingestUrl}/v1/events:batch`, {
|
|
141
117
|
method: 'POST',
|
|
142
118
|
headers: {
|
|
143
119
|
'Content-Type': 'application/json',
|
|
144
120
|
Authorization: `Bearer ${token}`,
|
|
145
121
|
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
146
122
|
},
|
|
147
|
-
body: JSON.stringify(
|
|
123
|
+
body: JSON.stringify(envelope),
|
|
148
124
|
});
|
|
149
125
|
|
|
150
126
|
if (resp.status === 429) {
|
|
@@ -162,11 +138,11 @@ const sendOnce = async (
|
|
|
162
138
|
if (resp.status >= 500) {
|
|
163
139
|
throw new Error(`server-${resp.status}`);
|
|
164
140
|
}
|
|
165
|
-
// 4xx other than 429 = client error
|
|
141
|
+
// 4xx other than 429 = client error; per-item outcomes are the
|
|
142
|
+
// server's business — drop silently rather than crashloop.
|
|
166
143
|
};
|
|
167
144
|
|
|
168
|
-
const sleep = (ms: number): Promise<void> =>
|
|
169
|
-
new Promise((r) => setTimeout(r, ms));
|
|
145
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
170
146
|
|
|
171
147
|
type AsyncStorageLike = {
|
|
172
148
|
getItem(key: string): Promise<string | null>;
|
|
@@ -175,18 +151,14 @@ type AsyncStorageLike = {
|
|
|
175
151
|
};
|
|
176
152
|
|
|
177
153
|
const getAsyncStorage = async (): Promise<AsyncStorageLike | null> => {
|
|
178
|
-
//
|
|
179
|
-
//
|
|
154
|
+
// Host may have the JS package without pod install / prebuild →
|
|
155
|
+
// getItem would crash from a microtask outside our reach.
|
|
180
156
|
if (!isAnyNativeModuleLinked(['RNCAsyncStorage', 'AsyncStorageModule'])) {
|
|
181
157
|
return null;
|
|
182
158
|
}
|
|
183
159
|
try {
|
|
184
|
-
// Resolve via the host's runtime `require` rather than `import()
|
|
185
|
-
//
|
|
186
|
-
// peer dep isn't installed in monorepo-root CI, which made `bun
|
|
187
|
-
// run build:sdks` fail to find the type declarations. The peer is
|
|
188
|
-
// optional at runtime anyway — the isAnyNativeModuleLinked guard
|
|
189
|
-
// above already returned `null` if the package isn't installed.
|
|
160
|
+
// Resolve via the host's runtime `require` rather than `import()`
|
|
161
|
+
// — the peer dep is optional and absent in monorepo CI.
|
|
190
162
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
191
163
|
const mod = require('@react-native-async-storage/async-storage') as {
|
|
192
164
|
default?: AsyncStorageLike;
|
|
@@ -197,12 +169,13 @@ const getAsyncStorage = async (): Promise<AsyncStorageLike | null> => {
|
|
|
197
169
|
}
|
|
198
170
|
};
|
|
199
171
|
|
|
200
|
-
const persist = async (events:
|
|
172
|
+
const persist = async (events: WireEvent[]): Promise<void> => {
|
|
173
|
+
if (events.length === 0) return;
|
|
201
174
|
const AsyncStorage = await getAsyncStorage();
|
|
202
175
|
if (!AsyncStorage) return;
|
|
203
176
|
try {
|
|
204
177
|
const existing = await AsyncStorage.getItem(STORAGE_KEY);
|
|
205
|
-
const prev:
|
|
178
|
+
const prev: WireEvent[] = existing ? JSON.parse(existing) : [];
|
|
206
179
|
const merged = [...prev, ...events].slice(-MAX_PERSISTED);
|
|
207
180
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
|
208
181
|
} catch {
|
|
@@ -217,7 +190,7 @@ export const drainOfflineQueue = async (): Promise<void> => {
|
|
|
217
190
|
const raw = await AsyncStorage.getItem(STORAGE_KEY);
|
|
218
191
|
if (!raw) return;
|
|
219
192
|
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
220
|
-
const events:
|
|
193
|
+
const events: WireEvent[] = JSON.parse(raw);
|
|
221
194
|
for (const e of events) _queue.push(e);
|
|
222
195
|
await flush();
|
|
223
196
|
} catch {
|
|
@@ -225,228 +198,27 @@ export const drainOfflineQueue = async (): Promise<void> => {
|
|
|
225
198
|
}
|
|
226
199
|
};
|
|
227
200
|
|
|
228
|
-
export const __resetForTests = (): void => {
|
|
229
|
-
_queue = [];
|
|
230
|
-
if (_flushTimer) clearTimeout(_flushTimer);
|
|
231
|
-
_flushTimer = null;
|
|
232
|
-
if (_spanTimer) clearInterval(_spanTimer);
|
|
233
|
-
_spanTimer = null;
|
|
234
|
-
_started = false;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
export const __peekQueue = (): readonly Event[] => _queue;
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Phase 26 sub-B: session ping transport. Best-effort; we don't queue
|
|
241
|
-
* pings the way we queue events because they fire on background and
|
|
242
|
-
* AsyncStorage writes during background can be killed by the OS. If
|
|
243
|
-
* the network's down, the ping is lost — the session counters tolerate
|
|
244
|
-
* this.
|
|
245
|
-
*/
|
|
246
|
-
export const sendSessionPing = async (
|
|
247
|
-
ingestUrl: string,
|
|
248
|
-
token: string,
|
|
249
|
-
ping: unknown
|
|
250
|
-
): Promise<void> => {
|
|
251
|
-
try {
|
|
252
|
-
await fetch(`${ingestUrl}/v1/sessions`, {
|
|
253
|
-
body: JSON.stringify(ping),
|
|
254
|
-
headers: {
|
|
255
|
-
Authorization: `Bearer ${token}`,
|
|
256
|
-
'Content-Type': 'application/json',
|
|
257
|
-
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
258
|
-
},
|
|
259
|
-
method: 'POST',
|
|
260
|
-
});
|
|
261
|
-
} catch {
|
|
262
|
-
// best-effort
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* v1.1 chunk B — flush a batched set of `sentori.track()` analytics
|
|
268
|
-
* events. The host SDK batches calls into a fixed-size ring drained
|
|
269
|
-
* every 30 s. Best-effort; a failure drops the batch.
|
|
270
|
-
*/
|
|
271
|
-
export const sendTrackBatch = async (
|
|
272
|
-
ingestUrl: string,
|
|
273
|
-
token: string,
|
|
274
|
-
events: Array<{
|
|
275
|
-
environment?: string;
|
|
276
|
-
name: string;
|
|
277
|
-
props?: Record<string, unknown>;
|
|
278
|
-
release?: string;
|
|
279
|
-
route?: string;
|
|
280
|
-
sessionId?: string;
|
|
281
|
-
ts: string;
|
|
282
|
-
userId?: string;
|
|
283
|
-
}>,
|
|
284
|
-
): Promise<void> => {
|
|
285
|
-
if (events.length === 0) return;
|
|
286
|
-
try {
|
|
287
|
-
await fetch(`${ingestUrl}/v1/track:batch`, {
|
|
288
|
-
body: JSON.stringify({ events }),
|
|
289
|
-
headers: {
|
|
290
|
-
Authorization: `Bearer ${token}`,
|
|
291
|
-
'Content-Type': 'application/json',
|
|
292
|
-
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
293
|
-
},
|
|
294
|
-
method: 'POST',
|
|
295
|
-
});
|
|
296
|
-
} catch {
|
|
297
|
-
// best-effort
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* v0.8.3 — flush a batched set of custom metrics. The host SDK
|
|
303
|
-
* batches recordMetric() calls into a fixed-size ring (drained on
|
|
304
|
-
* a timer + at next captureException) so a busy loop doesn't spin
|
|
305
|
-
* up one fetch per point. Best-effort, no retry.
|
|
306
|
-
*/
|
|
307
|
-
export const sendMetricsBatch = async (
|
|
308
|
-
ingestUrl: string,
|
|
309
|
-
token: string,
|
|
310
|
-
metrics: Array<{
|
|
311
|
-
name: string;
|
|
312
|
-
tags?: Record<string, string>;
|
|
313
|
-
ts?: string;
|
|
314
|
-
value: number;
|
|
315
|
-
}>,
|
|
316
|
-
): Promise<void> => {
|
|
317
|
-
if (metrics.length === 0) return;
|
|
318
|
-
try {
|
|
319
|
-
await fetch(`${ingestUrl}/v1/metrics:batch`, {
|
|
320
|
-
body: JSON.stringify({ metrics }),
|
|
321
|
-
headers: {
|
|
322
|
-
Authorization: `Bearer ${token}`,
|
|
323
|
-
'Content-Type': 'application/json',
|
|
324
|
-
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
325
|
-
},
|
|
326
|
-
method: 'POST',
|
|
327
|
-
});
|
|
328
|
-
} catch {
|
|
329
|
-
// best-effort
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
|
|
333
201
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
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
|
-
|
|
372
|
-
/**
|
|
373
|
-
* v0.8.2 — submit a user-supplied bug report. Fire-and-forget; resolves
|
|
374
|
-
* with the server-assigned id on success or `null` on any failure.
|
|
375
|
-
* The host app typically calls this from a "Report a problem" form;
|
|
376
|
-
* pass `eventId` if you're reporting a specific crash the user just
|
|
377
|
-
* saw so the report links to that event's issue automatically.
|
|
378
|
-
*/
|
|
379
|
-
export const sendUserReport = async (
|
|
380
|
-
ingestUrl: string,
|
|
381
|
-
token: string,
|
|
382
|
-
report: {
|
|
383
|
-
body: string;
|
|
384
|
-
email?: string;
|
|
385
|
-
eventId?: string;
|
|
386
|
-
name?: string;
|
|
387
|
-
title: string;
|
|
388
|
-
},
|
|
389
|
-
): Promise<null | { id: string; issueId: null | string }> => {
|
|
390
|
-
try {
|
|
391
|
-
const resp = await fetch(`${ingestUrl}/v1/user-reports`, {
|
|
392
|
-
body: JSON.stringify(report),
|
|
393
|
-
headers: {
|
|
394
|
-
Authorization: `Bearer ${token}`,
|
|
395
|
-
'Content-Type': 'application/json',
|
|
396
|
-
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
397
|
-
},
|
|
398
|
-
method: 'POST',
|
|
399
|
-
});
|
|
400
|
-
if (!resp.ok) return null;
|
|
401
|
-
const j = (await resp.json()) as { id: string; issueId: null | string };
|
|
402
|
-
return j;
|
|
403
|
-
} catch {
|
|
404
|
-
return null;
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
// ──────────────────────────────────────────────────────────────────
|
|
409
|
-
// Phase 42 sub-D.05 — attachment upload pipeline
|
|
410
|
-
// ──────────────────────────────────────────────────────────────────
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Upload a base64-encoded binary blob as an attachment for a known
|
|
414
|
-
* event. The event must NOT have been POSTed yet — the server-side
|
|
415
|
-
* ingest validation in events.rs only honours `event.attachments[].ref`
|
|
416
|
-
* when the matching `event_attachments` row already exists for the
|
|
417
|
-
* same (event_id, project_id). Caller's contract:
|
|
418
|
-
*
|
|
419
|
-
* 1. Generate `event.id` (uuidV7).
|
|
420
|
-
* 2. Build the blob (e.g. via `captureScreenshot`).
|
|
421
|
-
* 3. `await uploadAttachment(...)` → get `{ ref, sizeBytes, mediaType }`.
|
|
422
|
-
* 4. Push `{ ref, kind, ... }` into `event.attachments` then enqueue.
|
|
423
|
-
*
|
|
424
|
-
* Returns `null` on any non-fatal failure (network down, store
|
|
425
|
-
* disabled, 4xx, timeout). The error event still ships without the
|
|
426
|
-
* attachment so we never lose the actual crash.
|
|
202
|
+
* Upload one attachment for an already-enqueued event. Returns null
|
|
203
|
+
* on any non-fatal failure — the event still ships without the
|
|
204
|
+
* attachment so the crash itself is never lost.
|
|
427
205
|
*/
|
|
428
206
|
export const uploadAttachment = async (
|
|
429
207
|
eventId: string,
|
|
430
|
-
kind: import('
|
|
208
|
+
kind: import('@goliapkg/sentori-core').AttachmentKind,
|
|
431
209
|
blob: { base64: string; mediaType: string },
|
|
432
210
|
opts: { source?: 'android' | 'ios' | 'js' } = {},
|
|
433
|
-
): Promise<
|
|
211
|
+
): Promise<{ ref: string } | null> => {
|
|
434
212
|
const config = getConfig();
|
|
435
213
|
if (!config) return null;
|
|
436
214
|
const url = `${config.ingestUrl}/v1/events/${encodeURIComponent(eventId)}/attachments/${encodeURIComponent(kind)}`;
|
|
437
215
|
|
|
438
|
-
// RN-style multipart: `{ uri, type, name }` is what the native
|
|
439
|
-
// FormData implementation expects for a file part — the bridge
|
|
440
|
-
// serializes a data: URI without us having to allocate a Blob.
|
|
441
216
|
const form = new FormData();
|
|
442
|
-
form.append(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
uri: `data:${blob.mediaType};base64,${blob.base64}`,
|
|
448
|
-
} as unknown as Blob,
|
|
449
|
-
);
|
|
217
|
+
form.append('file', {
|
|
218
|
+
name: `${kind}.bin`,
|
|
219
|
+
type: blob.mediaType,
|
|
220
|
+
uri: `data:${blob.mediaType};base64,${blob.base64}`,
|
|
221
|
+
} as unknown as Blob);
|
|
450
222
|
form.append('source', opts.source ?? 'js');
|
|
451
223
|
|
|
452
224
|
try {
|
|
@@ -458,84 +230,25 @@ export const uploadAttachment = async (
|
|
|
458
230
|
},
|
|
459
231
|
method: 'POST',
|
|
460
232
|
});
|
|
461
|
-
// Phase 48 sub-A: accept any 2xx instead of strict 201. Reverse
|
|
462
|
-
// proxies in front of ingest occasionally rewrite 201 → 202 (the
|
|
463
|
-
// exact symptom Insight observed), and a 200 is also a valid
|
|
464
|
-
// "stored" response. We still require a JSON body shaped like
|
|
465
|
-
// UploadResponse; non-JSON bodies fall through to null.
|
|
466
233
|
if (resp.status < 200 || resp.status >= 300) {
|
|
467
|
-
|
|
468
|
-
// rc.6 — surface the status in dev so Insight-style triage
|
|
469
|
-
// doesn't have to guess between 413/422/500. Pre-rc.6 only
|
|
470
|
-
// the breadcrumb carried the reason; logcat only saw the
|
|
471
|
-
// generic `upload returned null` line.
|
|
472
|
-
logger.debug('transport', 'attachment upload non-2xx',
|
|
473
|
-
'eventId=', eventId,
|
|
474
|
-
'kind=', kind,
|
|
475
|
-
'status=', resp.status,
|
|
476
|
-
);
|
|
234
|
+
logger.warn(`attachment ${kind} upload http_${resp.status}`);
|
|
477
235
|
return null;
|
|
478
236
|
}
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
sizeBytes: number;
|
|
482
|
-
mediaType: string;
|
|
483
|
-
kind: string;
|
|
484
|
-
};
|
|
485
|
-
if (!j || !j.refId) {
|
|
486
|
-
noteAttachmentFailure(eventId, kind, 'bad_response_body');
|
|
487
|
-
logger.debug('transport', 'attachment upload bad-response-body',
|
|
488
|
-
'eventId=', eventId,
|
|
489
|
-
'kind=', kind,
|
|
490
|
-
'status=', resp.status,
|
|
491
|
-
);
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
kind,
|
|
496
|
-
mediaType: j.mediaType,
|
|
497
|
-
ref: j.refId,
|
|
498
|
-
sizeBytes: j.sizeBytes,
|
|
499
|
-
source: opts.source ?? 'js',
|
|
500
|
-
};
|
|
237
|
+
const body = (await resp.json()) as { refId?: string };
|
|
238
|
+
return body.refId ? { ref: body.refId } : null;
|
|
501
239
|
} catch (e) {
|
|
502
|
-
|
|
503
|
-
noteAttachmentFailure(eventId, kind, reason);
|
|
504
|
-
logger.debug('transport', 'attachment upload fetch threw',
|
|
505
|
-
'eventId=', eventId,
|
|
506
|
-
'kind=', kind,
|
|
507
|
-
'reason=', reason,
|
|
508
|
-
);
|
|
240
|
+
logger.warn(`attachment ${kind} upload failed: ${String(e)}`);
|
|
509
241
|
return null;
|
|
510
242
|
}
|
|
511
243
|
};
|
|
512
244
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
* Drop a breadcrumb so the next event on the same SDK instance
|
|
521
|
-
* carries a tagged trail entry. Dashboard's breadcrumb panel will
|
|
522
|
-
* show `sentori.attach.failed { eventId, kind, reason }` and we can
|
|
523
|
-
* tell upload-broke from never-tried at a glance.
|
|
524
|
-
*/
|
|
525
|
-
function noteAttachmentFailure(
|
|
526
|
-
eventId: string,
|
|
527
|
-
kind: string,
|
|
528
|
-
reason: string,
|
|
529
|
-
): void {
|
|
530
|
-
addBreadcrumb('custom', {
|
|
531
|
-
category: 'sentori.attach.failed',
|
|
532
|
-
eventId,
|
|
533
|
-
kind,
|
|
534
|
-
reason,
|
|
535
|
-
});
|
|
536
|
-
}
|
|
245
|
+
export const __resetForTests = (): void => {
|
|
246
|
+
_queue = [];
|
|
247
|
+
_assertStats = new Map();
|
|
248
|
+
if (_flushTimer) clearTimeout(_flushTimer);
|
|
249
|
+
_flushTimer = null;
|
|
250
|
+
_started = false;
|
|
251
|
+
};
|
|
537
252
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return `${kind}.${ext}`;
|
|
541
|
-
}
|
|
253
|
+
export const __peekQueue = (): readonly WireEvent[] => _queue;
|
|
254
|
+
export const __peekAssertStats = (): readonly AssertStat[] => [..._assertStats.values()];
|