@reproapp/react-sdk 0.0.1 → 0.0.3

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,222 @@
1
+ import { gzip } from 'pako';
2
+ const jsonBytes = (obj) => {
3
+ try {
4
+ return new TextEncoder().encode(JSON.stringify(obj)).length;
5
+ }
6
+ catch {
7
+ return Infinity;
8
+ }
9
+ };
10
+ const splitEventsBySize = (events, maxBytes, mkEnvelope) => {
11
+ const out = [];
12
+ const stack = [events.slice(0)];
13
+ while (stack.length) {
14
+ const cur = stack.pop();
15
+ const env = mkEnvelope(cur);
16
+ if (jsonBytes(env) <= maxBytes || cur.length <= 1) {
17
+ out.push(cur);
18
+ continue;
19
+ }
20
+ const mid = Math.floor(cur.length / 2);
21
+ stack.push(cur.slice(0, mid));
22
+ stack.push(cur.slice(mid));
23
+ }
24
+ return out;
25
+ };
26
+ const addTenantHeader = (headers, req) => ({
27
+ ...headers,
28
+ [req.headers.ngrokSkipHeader]: req.headers.ngrokSkipValue,
29
+ });
30
+ const postCanonicalEvents = async (req, events) => {
31
+ try {
32
+ const headers = {
33
+ 'Content-Type': 'application/json',
34
+ 'X-App-Id': req.appId,
35
+ [req.headers.internalHeader]: '1',
36
+ };
37
+ if (req.appSecret)
38
+ headers['X-App-Secret'] = req.appSecret;
39
+ if (req.appName)
40
+ headers['X-App-Name'] = req.appName;
41
+ const resp = await fetch(req.ingestUrl, {
42
+ method: 'POST',
43
+ headers: addTenantHeader(headers, req),
44
+ body: JSON.stringify({ events }),
45
+ });
46
+ if (resp.status === 413)
47
+ return 'too_large';
48
+ if (!resp.ok)
49
+ return 'fail';
50
+ return 'ok';
51
+ }
52
+ catch {
53
+ return 'fail';
54
+ }
55
+ };
56
+ const sendChunkToLogCollector = async (req, envelope, seq) => {
57
+ const tFirst = Number(envelope?.tFirst);
58
+ const tLast = Number(envelope?.tLast);
59
+ const safeTFirst = Number.isFinite(tFirst) ? tFirst : Date.now();
60
+ const safeTLast = Number.isFinite(tLast) ? tLast : safeTFirst;
61
+ const canonical = {
62
+ schema_version: req.schemaVersion,
63
+ tenant_id: req.tenantId,
64
+ app_id: req.appId,
65
+ session_id: req.sid,
66
+ event_type: 'rrweb_chunk',
67
+ event_ts: new Date(safeTLast).toISOString(),
68
+ actor_id: req.actorId,
69
+ actor_type: req.actorType,
70
+ payload: {
71
+ action_id: req.actionId,
72
+ seq,
73
+ tFirst: safeTFirst,
74
+ tLast: safeTLast,
75
+ events: Array.isArray(envelope?.events) ? envelope.events : [],
76
+ },
77
+ };
78
+ if (req.actorLabels) {
79
+ canonical.actor_labels = req.actorLabels;
80
+ }
81
+ return postCanonicalEvents(req, [canonical]);
82
+ };
83
+ const sendChunk = async (req, envelope, seq) => {
84
+ try {
85
+ const resp = await fetch(`${req.baseUrl}/v1/sessions/${req.sid}/events`, {
86
+ method: 'POST',
87
+ headers: addTenantHeader({
88
+ 'Content-Type': 'application/json',
89
+ 'x-sdk-token': req.token,
90
+ ...(req.userToken ? { Authorization: `Bearer ${req.userToken}` } : {}),
91
+ [req.headers.internalHeader]: '1',
92
+ }, req),
93
+ body: JSON.stringify({ ...envelope, seq }),
94
+ });
95
+ if (resp.status === 401)
96
+ return { status: 'fail', unauthorized: true };
97
+ if (resp.status === 413)
98
+ return { status: 'too_large', unauthorized: false };
99
+ if (!resp.ok)
100
+ return { status: 'fail', unauthorized: false };
101
+ return { status: 'ok', unauthorized: false };
102
+ }
103
+ catch {
104
+ return { status: 'fail', unauthorized: false };
105
+ }
106
+ };
107
+ const sendChunkGzip = async (req, envelope, seq) => {
108
+ try {
109
+ const gz = gzip(JSON.stringify({ ...envelope, seq }));
110
+ const resp = await fetch(`${req.baseUrl}/v1/sessions/${req.sid}/events`, {
111
+ method: 'POST',
112
+ headers: addTenantHeader({
113
+ 'Content-Type': 'application/json',
114
+ 'Content-Encoding': 'gzip',
115
+ 'x-sdk-token': req.token,
116
+ ...(req.userToken ? { Authorization: `Bearer ${req.userToken}` } : {}),
117
+ [req.headers.internalHeader]: '1',
118
+ }, req),
119
+ body: gz,
120
+ });
121
+ if (resp.status === 401)
122
+ return { status: 'fail', unauthorized: true };
123
+ if (resp.status === 413)
124
+ return { status: 'too_large', unauthorized: false };
125
+ if (!resp.ok)
126
+ return { status: 'fail', unauthorized: false };
127
+ return { status: 'ok', unauthorized: false };
128
+ }
129
+ catch {
130
+ return { status: 'fail', unauthorized: false };
131
+ }
132
+ };
133
+ const flushRrweb = async (req) => {
134
+ const mkEnvelope = (slice) => {
135
+ const tFirst = slice[0]?.timestamp ?? Date.now();
136
+ const tLast = slice[slice.length - 1]?.timestamp ?? tFirst;
137
+ return { type: 'rrweb', seq: req.seqStart, tFirst, tLast, events: slice };
138
+ };
139
+ const pieces = splitEventsBySize(req.events, req.maxBytes, mkEnvelope);
140
+ let sentCount = 0;
141
+ let nextSeq = req.seqStart;
142
+ let status = 'ok';
143
+ let unauthorized = false;
144
+ for (let i = 0; i < pieces.length; i += 1) {
145
+ const piece = pieces[i];
146
+ const pieceSeq = req.seqStart + i;
147
+ const env = mkEnvelope(piece);
148
+ let result;
149
+ if (req.useIngestPipeline) {
150
+ result = {
151
+ status: await sendChunkToLogCollector(req, env, pieceSeq),
152
+ unauthorized: false,
153
+ };
154
+ }
155
+ else if (jsonBytes(env) > 64 * 1024) {
156
+ result = await sendChunkGzip(req, env, pieceSeq);
157
+ }
158
+ else {
159
+ result = await sendChunk(req, env, pieceSeq);
160
+ }
161
+ if (result.status === 'ok') {
162
+ sentCount += piece.length;
163
+ nextSeq = pieceSeq + 1;
164
+ continue;
165
+ }
166
+ if (result.status === 'too_large' && piece.length > 1) {
167
+ pieces.splice(i, 1, ...splitEventsBySize(piece, req.maxBytes, mkEnvelope));
168
+ i -= 1;
169
+ continue;
170
+ }
171
+ status = result.status;
172
+ unauthorized = result.unauthorized;
173
+ break;
174
+ }
175
+ return {
176
+ id: req.id,
177
+ type: 'flush_rrweb_result',
178
+ sentCount,
179
+ nextSeq,
180
+ status,
181
+ unauthorized,
182
+ };
183
+ };
184
+ self.addEventListener('message', (event) => {
185
+ const message = event.data;
186
+ if (!message)
187
+ return;
188
+ if (message.type === 'post_canonical') {
189
+ void postCanonicalEvents(message, message.events)
190
+ .then((status) => {
191
+ self.postMessage({
192
+ id: message.id,
193
+ type: 'post_canonical_result',
194
+ status,
195
+ });
196
+ })
197
+ .catch(() => {
198
+ self.postMessage({
199
+ id: message.id,
200
+ type: 'post_canonical_result',
201
+ status: 'fail',
202
+ });
203
+ });
204
+ return;
205
+ }
206
+ if (message.type !== 'flush_rrweb')
207
+ return;
208
+ void flushRrweb(message)
209
+ .then((result) => {
210
+ self.postMessage(result);
211
+ })
212
+ .catch(() => {
213
+ self.postMessage({
214
+ id: message.id,
215
+ type: 'flush_rrweb_result',
216
+ sentCount: 0,
217
+ nextSeq: message.seqStart,
218
+ status: 'fail',
219
+ unauthorized: false,
220
+ });
221
+ });
222
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/react-sdk",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Repro React SDK (MVP)",
5
5
  "license": "MIT",
6
6
 
@@ -40,6 +40,7 @@
40
40
 
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",
43
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
43
44
  "prepublishOnly": "npm run build"
44
45
  },
45
46