@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.
Files changed (44) hide show
  1. package/lib/capture.d.ts +6 -0
  2. package/lib/capture.d.ts.map +1 -1
  3. package/lib/capture.js +65 -9
  4. package/lib/capture.js.map +1 -1
  5. package/lib/config.d.ts +2 -0
  6. package/lib/config.d.ts.map +1 -1
  7. package/lib/config.js.map +1 -1
  8. package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
  9. package/lib/handlers/dev-symbolicate.js +29 -4
  10. package/lib/handlers/dev-symbolicate.js.map +1 -1
  11. package/lib/handlers/screenshot.d.ts +12 -0
  12. package/lib/handlers/screenshot.d.ts.map +1 -0
  13. package/lib/handlers/screenshot.js +85 -0
  14. package/lib/handlers/screenshot.js.map +1 -0
  15. package/lib/index.d.ts +5 -0
  16. package/lib/index.d.ts.map +1 -1
  17. package/lib/index.js +5 -0
  18. package/lib/index.js.map +1 -1
  19. package/lib/init.d.ts +5 -0
  20. package/lib/init.d.ts.map +1 -1
  21. package/lib/init.js +1 -0
  22. package/lib/init.js.map +1 -1
  23. package/lib/mask.d.ts +30 -0
  24. package/lib/mask.d.ts.map +1 -0
  25. package/lib/mask.js +77 -0
  26. package/lib/mask.js.map +1 -0
  27. package/lib/transport.d.ts +22 -0
  28. package/lib/transport.d.ts.map +1 -1
  29. package/lib/transport.js +62 -0
  30. package/lib/transport.js.map +1 -1
  31. package/lib/types.d.ts +1 -1
  32. package/lib/types.d.ts.map +1 -1
  33. package/package.json +10 -5
  34. package/src/__tests__/dev-symbolicate.test.ts +34 -1
  35. package/src/__tests__/screenshot.test.ts +88 -0
  36. package/src/capture.ts +79 -9
  37. package/src/config.ts +2 -0
  38. package/src/handlers/dev-symbolicate.ts +36 -5
  39. package/src/handlers/screenshot.ts +115 -0
  40. package/src/index.ts +5 -0
  41. package/src/init.ts +6 -0
  42. package/src/mask.tsx +95 -0
  43. package/src/transport.ts +77 -0
  44. 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
+ }
package/src/types.ts CHANGED
@@ -3,6 +3,9 @@
3
3
 
4
4
  export type {
5
5
  App,
6
+ AttachmentKind,
7
+ AttachmentMeta,
8
+ AttachmentSource,
6
9
  Breadcrumb,
7
10
  BreadcrumbType,
8
11
  CaptureExtras,