@goliapkg/sentori-react-native 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/README.md +5 -0
- package/SentoriReactNative.podspec +21 -0
- package/android/build.gradle +38 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +213 -0
- package/android/src/main/java/com/sentori/SentoriModule.kt +39 -0
- package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +60 -0
- package/expo-module.config.json +9 -0
- package/ios/SentoriCrashHandler.swift +160 -0
- package/ios/SentoriModule.swift +43 -0
- package/ios/Tests/SentoriCrashHandlerTests.swift +59 -0
- package/lib/breadcrumbs.d.ts +11 -0
- package/lib/breadcrumbs.d.ts.map +1 -0
- package/lib/breadcrumbs.js +21 -0
- package/lib/breadcrumbs.js.map +1 -0
- package/lib/capture.d.ts +23 -0
- package/lib/capture.d.ts.map +1 -0
- package/lib/capture.js +91 -0
- package/lib/capture.js.map +1 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +10 -0
- package/lib/config.js.map +1 -0
- package/lib/error-boundary.d.ts +17 -0
- package/lib/error-boundary.d.ts.map +1 -0
- package/lib/error-boundary.js +26 -0
- package/lib/error-boundary.js.map +1 -0
- package/lib/handlers/global.d.ts +2 -0
- package/lib/handlers/global.d.ts.map +1 -0
- package/lib/handlers/global.js +29 -0
- package/lib/handlers/global.js.map +1 -0
- package/lib/handlers/network.d.ts +2 -0
- package/lib/handlers/network.d.ts.map +1 -0
- package/lib/handlers/network.js +69 -0
- package/lib/handlers/network.js.map +1 -0
- package/lib/handlers/promise.d.ts +2 -0
- package/lib/handlers/promise.d.ts.map +1 -0
- package/lib/handlers/promise.js +27 -0
- package/lib/handlers/promise.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/init.d.ts +18 -0
- package/lib/init.d.ts.map +1 -0
- package/lib/init.js +56 -0
- package/lib/init.js.map +1 -0
- package/lib/native.d.ts +23 -0
- package/lib/native.d.ts.map +1 -0
- package/lib/native.js +56 -0
- package/lib/native.js.map +1 -0
- package/lib/stack.d.ts +3 -0
- package/lib/stack.d.ts.map +1 -0
- package/lib/stack.js +69 -0
- package/lib/stack.js.map +1 -0
- package/lib/transport.d.ts +8 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +143 -0
- package/lib/transport.js.map +1 -0
- package/lib/types.d.ts +62 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/uuid.d.ts +11 -0
- package/lib/uuid.d.ts.map +1 -0
- package/lib/uuid.js +46 -0
- package/lib/uuid.js.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/breadcrumbs.test.ts +44 -0
- package/src/__tests__/stack.test.ts +43 -0
- package/src/__tests__/transport.test.ts +112 -0
- package/src/__tests__/uuid.test.ts +41 -0
- package/src/breadcrumbs.ts +33 -0
- package/src/capture.ts +108 -0
- package/src/config.ts +21 -0
- package/src/error-boundary.tsx +38 -0
- package/src/handlers/global.ts +36 -0
- package/src/handlers/network.ts +70 -0
- package/src/handlers/promise.ts +38 -0
- package/src/index.ts +37 -0
- package/src/init.ts +80 -0
- package/src/native.ts +71 -0
- package/src/stack.ts +72 -0
- package/src/transport.ts +164 -0
- package/src/types.ts +63 -0
- package/src/uuid.ts +56 -0
package/src/transport.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getConfig } from './config';
|
|
2
|
+
import type { Event } from './types';
|
|
3
|
+
|
|
4
|
+
const FLUSH_INTERVAL_MS = 5_000;
|
|
5
|
+
const BATCH_SIZE = 10;
|
|
6
|
+
const MAX_RETRY = 3;
|
|
7
|
+
const STORAGE_KEY = '@sentori/pending';
|
|
8
|
+
const MAX_PERSISTED = 1000;
|
|
9
|
+
|
|
10
|
+
let _queue: Event[] = [];
|
|
11
|
+
let _flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
12
|
+
let _started = false;
|
|
13
|
+
|
|
14
|
+
const SDK_VERSION = '0.0.0';
|
|
15
|
+
|
|
16
|
+
export const enqueue = (event: Event): void => {
|
|
17
|
+
_queue.push(event);
|
|
18
|
+
if (_queue.length >= BATCH_SIZE) {
|
|
19
|
+
void flush();
|
|
20
|
+
} else if (!_flushTimer) {
|
|
21
|
+
_flushTimer = setTimeout(() => {
|
|
22
|
+
_flushTimer = null;
|
|
23
|
+
void flush();
|
|
24
|
+
}, FLUSH_INTERVAL_MS);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const startTransport = (): void => {
|
|
29
|
+
_started = true;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const flush = async (): Promise<void> => {
|
|
33
|
+
if (!_started) return;
|
|
34
|
+
if (_queue.length === 0) return;
|
|
35
|
+
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
if (!config) return;
|
|
38
|
+
|
|
39
|
+
const batch = _queue.splice(0, _queue.length);
|
|
40
|
+
if (_flushTimer) {
|
|
41
|
+
clearTimeout(_flushTimer);
|
|
42
|
+
_flushTimer = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await sendWithRetry(batch, config.ingestUrl, config.token);
|
|
47
|
+
} catch {
|
|
48
|
+
await persist(batch);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sendWithRetry = async (
|
|
53
|
+
events: Event[],
|
|
54
|
+
ingestUrl: string,
|
|
55
|
+
token: string,
|
|
56
|
+
): Promise<void> => {
|
|
57
|
+
let attempt = 0;
|
|
58
|
+
let delayMs = 1000;
|
|
59
|
+
while (true) {
|
|
60
|
+
try {
|
|
61
|
+
await sendOnce(events, ingestUrl, token);
|
|
62
|
+
return;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
attempt++;
|
|
65
|
+
if (attempt >= MAX_RETRY) throw e;
|
|
66
|
+
await sleep(delayMs);
|
|
67
|
+
delayMs *= 2;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const sendOnce = async (
|
|
73
|
+
events: Event[],
|
|
74
|
+
ingestUrl: string,
|
|
75
|
+
token: string,
|
|
76
|
+
): Promise<void> => {
|
|
77
|
+
const url =
|
|
78
|
+
events.length === 1 ? `${ingestUrl}/v1/events` : `${ingestUrl}/v1/events:batch`;
|
|
79
|
+
const body = events.length === 1 ? events[0] : { events };
|
|
80
|
+
|
|
81
|
+
const resp = await fetch(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
Authorization: `Bearer ${token}`,
|
|
86
|
+
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (resp.status === 429) {
|
|
92
|
+
let retryAfterMs = 5000;
|
|
93
|
+
try {
|
|
94
|
+
const j = (await resp.json()) as { retryAfterMs?: number };
|
|
95
|
+
if (typeof j.retryAfterMs === 'number') retryAfterMs = j.retryAfterMs;
|
|
96
|
+
} catch {
|
|
97
|
+
// ignore body parse error
|
|
98
|
+
}
|
|
99
|
+
await sleep(retryAfterMs);
|
|
100
|
+
throw new Error('rate-limited');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (resp.status >= 500) {
|
|
104
|
+
throw new Error(`server-${resp.status}`);
|
|
105
|
+
}
|
|
106
|
+
// 4xx other than 429 = client error, drop silently
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const sleep = (ms: number): Promise<void> =>
|
|
110
|
+
new Promise((r) => setTimeout(r, ms));
|
|
111
|
+
|
|
112
|
+
type AsyncStorageLike = {
|
|
113
|
+
getItem(key: string): Promise<string | null>;
|
|
114
|
+
setItem(key: string, value: string): Promise<void>;
|
|
115
|
+
removeItem(key: string): Promise<void>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getAsyncStorage = async (): Promise<AsyncStorageLike | null> => {
|
|
119
|
+
try {
|
|
120
|
+
const mod = (await import(
|
|
121
|
+
'@react-native-async-storage/async-storage'
|
|
122
|
+
)) as { default: AsyncStorageLike };
|
|
123
|
+
return mod.default;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const persist = async (events: Event[]): Promise<void> => {
|
|
130
|
+
const AsyncStorage = await getAsyncStorage();
|
|
131
|
+
if (!AsyncStorage) return;
|
|
132
|
+
try {
|
|
133
|
+
const existing = await AsyncStorage.getItem(STORAGE_KEY);
|
|
134
|
+
const prev: Event[] = existing ? JSON.parse(existing) : [];
|
|
135
|
+
const merged = [...prev, ...events].slice(-MAX_PERSISTED);
|
|
136
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
|
137
|
+
} catch {
|
|
138
|
+
// best-effort
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const drainOfflineQueue = async (): Promise<void> => {
|
|
143
|
+
const AsyncStorage = await getAsyncStorage();
|
|
144
|
+
if (!AsyncStorage) return;
|
|
145
|
+
try {
|
|
146
|
+
const raw = await AsyncStorage.getItem(STORAGE_KEY);
|
|
147
|
+
if (!raw) return;
|
|
148
|
+
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
149
|
+
const events: Event[] = JSON.parse(raw);
|
|
150
|
+
for (const e of events) _queue.push(e);
|
|
151
|
+
await flush();
|
|
152
|
+
} catch {
|
|
153
|
+
// best-effort
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const __resetForTests = (): void => {
|
|
158
|
+
_queue = [];
|
|
159
|
+
if (_flushTimer) clearTimeout(_flushTimer);
|
|
160
|
+
_flushTimer = null;
|
|
161
|
+
_started = false;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const __peekQueue = (): readonly Event[] => _queue;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type Platform = 'javascript' | 'ios' | 'android';
|
|
2
|
+
export type DeviceOS = 'ios' | 'android' | 'web' | 'other';
|
|
3
|
+
export type EventKind = 'error';
|
|
4
|
+
export type BreadcrumbType = 'nav' | 'net' | 'log' | 'user' | 'custom';
|
|
5
|
+
|
|
6
|
+
export type Event = {
|
|
7
|
+
id: string;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
kind: EventKind;
|
|
10
|
+
platform: Platform;
|
|
11
|
+
release: string;
|
|
12
|
+
environment: string;
|
|
13
|
+
device: Device;
|
|
14
|
+
app: App;
|
|
15
|
+
user?: User | null;
|
|
16
|
+
tags?: Tags;
|
|
17
|
+
breadcrumbs?: Breadcrumb[];
|
|
18
|
+
error: SentoriError;
|
|
19
|
+
fingerprint?: string[];
|
|
20
|
+
traceId?: string | null;
|
|
21
|
+
spanId?: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Device = {
|
|
25
|
+
os: DeviceOS;
|
|
26
|
+
osVersion: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
locale?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type App = {
|
|
32
|
+
version: string;
|
|
33
|
+
build?: string;
|
|
34
|
+
framework?: { name: string; version: string };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type User = { id?: string; anonymous?: boolean };
|
|
38
|
+
|
|
39
|
+
export type Tags = Record<string, string>;
|
|
40
|
+
|
|
41
|
+
export type SentoriError = {
|
|
42
|
+
type: string;
|
|
43
|
+
message: string;
|
|
44
|
+
stack: Frame[];
|
|
45
|
+
cause?: SentoriError | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type Frame = {
|
|
49
|
+
function?: string;
|
|
50
|
+
file: string;
|
|
51
|
+
line: number;
|
|
52
|
+
column?: number;
|
|
53
|
+
inApp: boolean;
|
|
54
|
+
absolutePath?: string;
|
|
55
|
+
preContext?: string[];
|
|
56
|
+
postContext?: string[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type Breadcrumb = {
|
|
60
|
+
timestamp: string;
|
|
61
|
+
type: BreadcrumbType;
|
|
62
|
+
data: Record<string, unknown>;
|
|
63
|
+
};
|
package/src/uuid.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 9562 UUID v7 generator.
|
|
3
|
+
* Layout:
|
|
4
|
+
* bytes 0-5 (48 bits) — Unix epoch milliseconds, big-endian
|
|
5
|
+
* byte 6 (high nibble) — version 7 (0x70)
|
|
6
|
+
* byte 6 (low nibble) + byte 7 — 12 random bits
|
|
7
|
+
* byte 8 (high 2 bits) — variant 10
|
|
8
|
+
* byte 8 (low 6 bits) + bytes 9-15 — 62 random bits
|
|
9
|
+
*/
|
|
10
|
+
export const uuidV7 = (): string => {
|
|
11
|
+
const ts = Date.now();
|
|
12
|
+
const buf = new Uint8Array(16);
|
|
13
|
+
|
|
14
|
+
buf[0] = Math.floor(ts / 0x10000000000) & 0xff;
|
|
15
|
+
buf[1] = Math.floor(ts / 0x100000000) & 0xff;
|
|
16
|
+
buf[2] = Math.floor(ts / 0x1000000) & 0xff;
|
|
17
|
+
buf[3] = Math.floor(ts / 0x10000) & 0xff;
|
|
18
|
+
buf[4] = Math.floor(ts / 0x100) & 0xff;
|
|
19
|
+
buf[5] = ts & 0xff;
|
|
20
|
+
|
|
21
|
+
fillRandom(buf.subarray(6));
|
|
22
|
+
|
|
23
|
+
buf[6] = (buf[6]! & 0x0f) | 0x70; // version 7
|
|
24
|
+
buf[8] = (buf[8]! & 0x3f) | 0x80; // variant 10xx
|
|
25
|
+
|
|
26
|
+
const hex = Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
27
|
+
return (
|
|
28
|
+
hex.slice(0, 8) +
|
|
29
|
+
'-' +
|
|
30
|
+
hex.slice(8, 12) +
|
|
31
|
+
'-' +
|
|
32
|
+
hex.slice(12, 16) +
|
|
33
|
+
'-' +
|
|
34
|
+
hex.slice(16, 20) +
|
|
35
|
+
'-' +
|
|
36
|
+
hex.slice(20, 32)
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const fillRandom = (buf: Uint8Array): void => {
|
|
41
|
+
// Prefer crypto.getRandomValues (Hermes 0.74+, browsers, Node, Bun).
|
|
42
|
+
// RN apps targeting older Hermes should add `react-native-get-random-values`
|
|
43
|
+
// before importing the SDK.
|
|
44
|
+
const cryptoObj = (
|
|
45
|
+
globalThis as {
|
|
46
|
+
crypto?: { getRandomValues?: (b: Uint8Array) => Uint8Array };
|
|
47
|
+
}
|
|
48
|
+
).crypto;
|
|
49
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
|
|
50
|
+
cryptoObj.getRandomValues(buf);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < buf.length; i++) {
|
|
54
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
55
|
+
}
|
|
56
|
+
};
|