@goliapkg/sentori-react-native 0.5.6 → 0.6.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/capture.d.ts +6 -0
- package/lib/capture.d.ts.map +1 -1
- package/lib/capture.js +65 -9
- package/lib/capture.js.map +1 -1
- package/lib/config.d.ts +2 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js.map +1 -1
- package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.js +29 -4
- package/lib/handlers/dev-symbolicate.js.map +1 -1
- package/lib/handlers/screenshot.d.ts +12 -0
- package/lib/handlers/screenshot.d.ts.map +1 -0
- package/lib/handlers/screenshot.js +85 -0
- package/lib/handlers/screenshot.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +5 -0
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +1 -0
- package/lib/init.js.map +1 -1
- package/lib/mask.d.ts +30 -0
- package/lib/mask.d.ts.map +1 -0
- package/lib/mask.js +77 -0
- package/lib/mask.js.map +1 -0
- package/lib/transport.d.ts +22 -0
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +62 -0
- package/lib/transport.js.map +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +10 -5
- package/src/__tests__/dev-symbolicate.test.ts +34 -1
- package/src/__tests__/screenshot.test.ts +88 -0
- package/src/capture.ts +79 -9
- package/src/config.ts +2 -0
- package/src/handlers/dev-symbolicate.ts +36 -5
- package/src/handlers/screenshot.ts +115 -0
- package/src/index.ts +5 -0
- package/src/init.ts +6 -0
- package/src/mask.tsx +95 -0
- package/src/transport.ts +77 -0
- package/src/types.ts +3 -0
package/src/mask.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Phase 42 sub-D.09/10 — mark UI regions as "do not screenshot".
|
|
2
|
+
//
|
|
3
|
+
// `<MaskRegion>` wraps any subtree the SDK should redact before
|
|
4
|
+
// shipping a crash screenshot. It's purely declarative — the
|
|
5
|
+
// component renders its children as-is in normal flight, but its
|
|
6
|
+
// `View` is tagged with `collapsable={false}` + a sentinel
|
|
7
|
+
// `nativeID` so the platform-level screenshotters
|
|
8
|
+
// (`react-native-view-shot`, the iOS / Android native crash
|
|
9
|
+
// capturers we add in sub-E / sub-F) can find it and paint over.
|
|
10
|
+
//
|
|
11
|
+
// `setMaskedNode(ref)` is the imperative escape hatch: useful
|
|
12
|
+
// when the sensitive view isn't yours to wrap (a third-party
|
|
13
|
+
// modal, a video player, etc.). Pass a React ref obtained via
|
|
14
|
+
// `createRef()` / `useRef()` and the SDK will redact that
|
|
15
|
+
// subtree the next time it captures.
|
|
16
|
+
//
|
|
17
|
+
// `getMaskedRegions()` returns the current set of native tags;
|
|
18
|
+
// `captureScreenshot()` would consult this list, but
|
|
19
|
+
// `react-native-view-shot` doesn't expose a "redact these rects"
|
|
20
|
+
// hook — so this iteration ships the registration API only and
|
|
21
|
+
// the rendered overlay lives behind a default-on
|
|
22
|
+
// `<View style={{ backgroundColor: '#000' }}>` you can wrap
|
|
23
|
+
// yourself. The iOS / Android crash-time screenshotters in
|
|
24
|
+
// sub-E / sub-F will read this list before drawing.
|
|
25
|
+
|
|
26
|
+
import React, { type ReactNode, useEffect, useRef } from 'react';
|
|
27
|
+
import { View, type ViewProps } from 'react-native';
|
|
28
|
+
|
|
29
|
+
/** Component-level node identifiers we've been asked to redact. */
|
|
30
|
+
const _maskedRefs = new Set<React.Component | View | unknown>();
|
|
31
|
+
const _maskedNativeIds = new Set<string>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Imperative registration: when you can't wrap the sensitive view
|
|
35
|
+
* in `<MaskRegion>`, drop a ref on it and call `setMaskedNode(ref)`.
|
|
36
|
+
* Future captures will mask the subtree.
|
|
37
|
+
*/
|
|
38
|
+
export function setMaskedNode(node: React.Component | View | null | unknown): void {
|
|
39
|
+
if (!node) return;
|
|
40
|
+
_maskedRefs.add(node);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Removes a previously registered ref. Pair this with mount/unmount
|
|
44
|
+
* lifecycle hooks if the node is short-lived. */
|
|
45
|
+
export function unsetMaskedNode(node: React.Component | View | null | unknown): void {
|
|
46
|
+
if (!node) return;
|
|
47
|
+
_maskedRefs.delete(node);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns the current set of registered masked nodes + nativeIDs.
|
|
51
|
+
* Read by the native screenshotter layer in sub-E / sub-F. */
|
|
52
|
+
export function getMaskedRegions(): {
|
|
53
|
+
refs: Set<unknown>;
|
|
54
|
+
nativeIds: Set<string>;
|
|
55
|
+
} {
|
|
56
|
+
return { nativeIds: _maskedNativeIds, refs: _maskedRefs };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Declarative redaction. `<MaskRegion>{children}</MaskRegion>` keeps
|
|
61
|
+
* the children visible in normal flight; under capture, the wrapping
|
|
62
|
+
* view is repainted black so the rendered screenshot doesn't leak
|
|
63
|
+
* the underlying pixels.
|
|
64
|
+
*/
|
|
65
|
+
export function MaskRegion({
|
|
66
|
+
children,
|
|
67
|
+
nativeID,
|
|
68
|
+
...rest
|
|
69
|
+
}: { children: ReactNode; nativeID?: string } & ViewProps): React.JSX.Element {
|
|
70
|
+
// Auto-generate a stable nativeID per mount so the native
|
|
71
|
+
// screenshotter can find this view by ID at capture time.
|
|
72
|
+
const idRef = useRef<string>(
|
|
73
|
+
nativeID ?? `sentori-mask-${Math.random().toString(36).slice(2, 10)}`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const id = idRef.current;
|
|
78
|
+
_maskedNativeIds.add(id);
|
|
79
|
+
return () => {
|
|
80
|
+
_maskedNativeIds.delete(id);
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View collapsable={false} nativeID={idRef.current} {...rest}>
|
|
86
|
+
{children}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Test-only — flush registration tables. */
|
|
92
|
+
export function __resetMaskedRegionsForTests(): void {
|
|
93
|
+
_maskedRefs.clear();
|
|
94
|
+
_maskedNativeIds.clear();
|
|
95
|
+
}
|
package/src/transport.ts
CHANGED
|
@@ -241,3 +241,80 @@ export const sendSessionPing = async (
|
|
|
241
241
|
// best-effort
|
|
242
242
|
}
|
|
243
243
|
};
|
|
244
|
+
|
|
245
|
+
// ──────────────────────────────────────────────────────────────────
|
|
246
|
+
// Phase 42 sub-D.05 — attachment upload pipeline
|
|
247
|
+
// ──────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Upload a base64-encoded binary blob as an attachment for a known
|
|
251
|
+
* event. The event must NOT have been POSTed yet — the server-side
|
|
252
|
+
* ingest validation in events.rs only honours `event.attachments[].ref`
|
|
253
|
+
* when the matching `event_attachments` row already exists for the
|
|
254
|
+
* same (event_id, project_id). Caller's contract:
|
|
255
|
+
*
|
|
256
|
+
* 1. Generate `event.id` (uuidV7).
|
|
257
|
+
* 2. Build the blob (e.g. via `captureScreenshot`).
|
|
258
|
+
* 3. `await uploadAttachment(...)` → get `{ ref, sizeBytes, mediaType }`.
|
|
259
|
+
* 4. Push `{ ref, kind, ... }` into `event.attachments` then enqueue.
|
|
260
|
+
*
|
|
261
|
+
* Returns `null` on any non-fatal failure (network down, store
|
|
262
|
+
* disabled, 4xx, timeout). The error event still ships without the
|
|
263
|
+
* attachment so we never lose the actual crash.
|
|
264
|
+
*/
|
|
265
|
+
export const uploadAttachment = async (
|
|
266
|
+
eventId: string,
|
|
267
|
+
kind: import('./types').AttachmentMeta['kind'],
|
|
268
|
+
blob: { base64: string; mediaType: string },
|
|
269
|
+
opts: { source?: 'android' | 'ios' | 'js' } = {},
|
|
270
|
+
): Promise<import('./types').AttachmentMeta | null> => {
|
|
271
|
+
const config = getConfig();
|
|
272
|
+
if (!config) return null;
|
|
273
|
+
const url = `${config.ingestUrl}/v1/events/${encodeURIComponent(eventId)}/attachments/${encodeURIComponent(kind)}`;
|
|
274
|
+
|
|
275
|
+
// RN-style multipart: `{ uri, type, name }` is what the native
|
|
276
|
+
// FormData implementation expects for a file part — the bridge
|
|
277
|
+
// serializes a data: URI without us having to allocate a Blob.
|
|
278
|
+
const form = new FormData();
|
|
279
|
+
form.append(
|
|
280
|
+
'file',
|
|
281
|
+
{
|
|
282
|
+
name: filenameFor(kind, blob.mediaType),
|
|
283
|
+
type: blob.mediaType,
|
|
284
|
+
uri: `data:${blob.mediaType};base64,${blob.base64}`,
|
|
285
|
+
} as unknown as Blob,
|
|
286
|
+
);
|
|
287
|
+
form.append('source', opts.source ?? 'js');
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const resp = await fetch(url, {
|
|
291
|
+
body: form,
|
|
292
|
+
headers: {
|
|
293
|
+
Authorization: `Bearer ${config.token}`,
|
|
294
|
+
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
295
|
+
},
|
|
296
|
+
method: 'POST',
|
|
297
|
+
});
|
|
298
|
+
if (resp.status !== 201) return null;
|
|
299
|
+
const j = (await resp.json()) as {
|
|
300
|
+
refId: string;
|
|
301
|
+
sizeBytes: number;
|
|
302
|
+
mediaType: string;
|
|
303
|
+
kind: string;
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
kind,
|
|
307
|
+
mediaType: j.mediaType,
|
|
308
|
+
ref: j.refId,
|
|
309
|
+
sizeBytes: j.sizeBytes,
|
|
310
|
+
source: opts.source ?? 'js',
|
|
311
|
+
};
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
function filenameFor(kind: string, mediaType: string): string {
|
|
318
|
+
const ext = mediaType.split('/')[1] ?? 'bin';
|
|
319
|
+
return `${kind}.${ext}`;
|
|
320
|
+
}
|