@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/lib/transport.js CHANGED
@@ -213,4 +213,66 @@ export const sendSessionPing = async (ingestUrl, token, ping) => {
213
213
  // best-effort
214
214
  }
215
215
  };
216
+ // ──────────────────────────────────────────────────────────────────
217
+ // Phase 42 sub-D.05 — attachment upload pipeline
218
+ // ──────────────────────────────────────────────────────────────────
219
+ /**
220
+ * Upload a base64-encoded binary blob as an attachment for a known
221
+ * event. The event must NOT have been POSTed yet — the server-side
222
+ * ingest validation in events.rs only honours `event.attachments[].ref`
223
+ * when the matching `event_attachments` row already exists for the
224
+ * same (event_id, project_id). Caller's contract:
225
+ *
226
+ * 1. Generate `event.id` (uuidV7).
227
+ * 2. Build the blob (e.g. via `captureScreenshot`).
228
+ * 3. `await uploadAttachment(...)` → get `{ ref, sizeBytes, mediaType }`.
229
+ * 4. Push `{ ref, kind, ... }` into `event.attachments` then enqueue.
230
+ *
231
+ * Returns `null` on any non-fatal failure (network down, store
232
+ * disabled, 4xx, timeout). The error event still ships without the
233
+ * attachment so we never lose the actual crash.
234
+ */
235
+ export const uploadAttachment = async (eventId, kind, blob, opts = {}) => {
236
+ const config = getConfig();
237
+ if (!config)
238
+ return null;
239
+ const url = `${config.ingestUrl}/v1/events/${encodeURIComponent(eventId)}/attachments/${encodeURIComponent(kind)}`;
240
+ // RN-style multipart: `{ uri, type, name }` is what the native
241
+ // FormData implementation expects for a file part — the bridge
242
+ // serializes a data: URI without us having to allocate a Blob.
243
+ const form = new FormData();
244
+ form.append('file', {
245
+ name: filenameFor(kind, blob.mediaType),
246
+ type: blob.mediaType,
247
+ uri: `data:${blob.mediaType};base64,${blob.base64}`,
248
+ });
249
+ form.append('source', opts.source ?? 'js');
250
+ try {
251
+ const resp = await fetch(url, {
252
+ body: form,
253
+ headers: {
254
+ Authorization: `Bearer ${config.token}`,
255
+ 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
256
+ },
257
+ method: 'POST',
258
+ });
259
+ if (resp.status !== 201)
260
+ return null;
261
+ const j = (await resp.json());
262
+ return {
263
+ kind,
264
+ mediaType: j.mediaType,
265
+ ref: j.refId,
266
+ sizeBytes: j.sizeBytes,
267
+ source: opts.source ?? 'js',
268
+ };
269
+ }
270
+ catch {
271
+ return null;
272
+ }
273
+ };
274
+ function filenameFor(kind, mediaType) {
275
+ const ext = mediaType.split('/')[1] ?? 'bin';
276
+ return `${kind}.${ext}`;
277
+ }
216
278
  //# sourceMappingURL=transport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
package/lib/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { App, Breadcrumb, BreadcrumbType, CaptureExtras, Device, DeviceOS, Event, EventKind, Frame, Platform, SentoriError, Tags, User, } from '@goliapkg/sentori-core';
1
+ export type { App, AttachmentKind, AttachmentMeta, AttachmentSource, Breadcrumb, BreadcrumbType, CaptureExtras, Device, DeviceOS, Event, EventKind, Frame, Platform, SentoriError, Tags, User, } from '@goliapkg/sentori-core';
2
2
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,GAAG,EACH,UAAU,EACV,cAAc,EACd,aAAa,EACb,MAAM,EACN,QAAQ,EACR,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,IAAI,EACJ,IAAI,GACL,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,GAAG,EACH,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,aAAa,EACb,MAAM,EACN,QAAQ,EACR,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,IAAI,EACJ,IAAI,GACL,MAAM,wBAAwB,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "0.5.6",
4
- "description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
3
+ "version": "0.6.0",
4
+ "description": "Sentori SDK for React Native JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
7
7
  "repository": {
@@ -41,7 +41,8 @@
41
41
  "peerDependencies": {
42
42
  "expo-modules-core": ">=2.0",
43
43
  "react": ">=18",
44
- "react-native": ">=0.74"
44
+ "react-native": ">=0.74",
45
+ "react-native-view-shot": ">=3.8"
45
46
  },
46
47
  "peerDependenciesMeta": {
47
48
  "@react-native-async-storage/async-storage": {
@@ -49,11 +50,15 @@
49
50
  },
50
51
  "expo-modules-core": {
51
52
  "optional": true
53
+ },
54
+ "react-native-view-shot": {
55
+ "optional": true
52
56
  }
53
57
  },
54
58
  "optionalDependencies": {
55
59
  "@react-native-async-storage/async-storage": ">=1.23",
56
- "expo-modules-core": ">=2.0"
60
+ "expo-modules-core": ">=2.0",
61
+ "react-native-view-shot": ">=3.8"
57
62
  },
58
63
  "devDependencies": {
59
64
  "@types/bun": "latest",
@@ -64,6 +69,6 @@
64
69
  "access": "public"
65
70
  },
66
71
  "dependencies": {
67
- "@goliapkg/sentori-core": "0.4.1"
72
+ "@goliapkg/sentori-core": "0.5.0"
68
73
  }
69
74
  }
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
2
2
 
3
3
  import { symbolicateErrorViaMetro, symbolicateStackViaMetro } from '../handlers/dev-symbolicate';
4
4
  import type { Frame, SentoriError } from '../types';
@@ -107,6 +107,39 @@ describe('symbolicateStackViaMetro', () => {
107
107
  });
108
108
  });
109
109
 
110
+ describe('metroSymbolicateUrl resolution (RN 0.83 new-arch regression)', () => {
111
+ // Without `opts.url`, the function must resolve a real Metro URL.
112
+ // RN 0.83 + new architecture leaves `NativeModules.SourceCode.scriptURL`
113
+ // undefined; the fix is to prefer RN's own `getDevServer()` helper,
114
+ // which internally calls `NativeSourceCode.getConstants().scriptURL`
115
+ // and works on both old and new arch.
116
+ test('prefers getDevServer() when available (works on new arch)', async () => {
117
+ mock.module('react-native/Libraries/Core/Devtools/getDevServer', () => ({
118
+ default: () => ({ bundleLoadedFromServer: true, url: 'http://192.168.1.100:8081/' }),
119
+ }));
120
+ const calls: string[] = [];
121
+ globalThis.fetch = (async (url: Request | string | URL) => {
122
+ calls.push(String(url));
123
+ return new Response(metroReply([{ col: 1, file: '/proj/src/a.ts', fn: 'a', line: 5 }]), {
124
+ headers: { 'content-type': 'application/json' },
125
+ status: 200,
126
+ });
127
+ }) as typeof fetch;
128
+
129
+ const out = await symbolicateStackViaMetro([minified(1)]);
130
+ expect(calls[0]).toBe('http://192.168.1.100:8081/symbolicate');
131
+ expect(out![0]?.file).toBe('/proj/src/a.ts');
132
+ });
133
+
134
+ test('returns null when getDevServer says bundle was not loaded from Metro', async () => {
135
+ mock.module('react-native/Libraries/Core/Devtools/getDevServer', () => ({
136
+ default: () => ({ bundleLoadedFromServer: false, url: 'http://localhost:8081/' }),
137
+ }));
138
+ // No fallback chain hit either (NativeModules require still throws in bun env)
139
+ expect(await symbolicateStackViaMetro([minified(1)])).toBeNull();
140
+ });
141
+ });
142
+
110
143
  describe('symbolicateErrorViaMetro', () => {
111
144
  test('replaces stack in place and recurses into the cause chain', async () => {
112
145
  globalThis.fetch = (async () =>
@@ -0,0 +1,88 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
2
+
3
+ import { setConfig, __resetForTests as resetConfig } from '../config';
4
+ import { uploadAttachment } from '../transport';
5
+
6
+ // Phase 42 sub-D.13 — unit coverage for the upload pipeline.
7
+ //
8
+ // `captureScreenshot()` itself goes through react-native-view-shot
9
+ // + RN's InteractionManager, neither of which exist in the bun:test
10
+ // runtime, so we test it indirectly: `uploadAttachment` is the
11
+ // non-RN-API surface and that's what we hit hardest. The
12
+ // `requestAnimationFrame` / InteractionManager perf gating is a
13
+ // runtime concern asserted via manual smoke + the iOS / Android
14
+ // XCTest / instrumentation tests in sub-E / sub-F.
15
+
16
+ const origFetch = globalThis.fetch;
17
+ afterEach(() => {
18
+ globalThis.fetch = origFetch;
19
+ resetConfig();
20
+ });
21
+ beforeEach(() => {
22
+ globalThis.fetch = origFetch;
23
+ setConfig({
24
+ enabled: true,
25
+ environment: 'test',
26
+ ingestUrl: 'http://localhost:18080',
27
+ release: 'app@1.0.0+1',
28
+ screenshotsEnabled: true,
29
+ token: 'st_pk_test',
30
+ });
31
+ });
32
+
33
+ describe('uploadAttachment', () => {
34
+ test('hits POST /v1/events/<id>/attachments/<kind> with the bearer token', async () => {
35
+ const seen: { method?: string; url?: string; auth?: null | string } = {};
36
+ globalThis.fetch = mock(async (url: Request | string | URL, init?: RequestInit) => {
37
+ seen.url = String(url);
38
+ seen.method = init?.method;
39
+ // Reach the Bearer header off the Headers/Init shape used here.
40
+ const headers = (init?.headers ?? {}) as Record<string, string>;
41
+ seen.auth = headers.Authorization;
42
+ return new Response(
43
+ JSON.stringify({
44
+ kind: 'screenshot',
45
+ mediaType: 'image/jpeg',
46
+ refId: '019e3000-7000-7000-8000-000000000001',
47
+ sizeBytes: 4,
48
+ }),
49
+ { headers: { 'content-type': 'application/json' }, status: 201 },
50
+ );
51
+ }) as typeof fetch;
52
+
53
+ const out = await uploadAttachment('019eaa00-0000-7000-8000-000000000001', 'screenshot', {
54
+ base64: 'AAAA',
55
+ mediaType: 'image/jpeg',
56
+ });
57
+ expect(seen.method).toBe('POST');
58
+ expect(seen.url).toBe(
59
+ 'http://localhost:18080/v1/events/019eaa00-0000-7000-8000-000000000001/attachments/screenshot',
60
+ );
61
+ expect(seen.auth).toBe('Bearer st_pk_test');
62
+ expect(out).not.toBeNull();
63
+ expect(out!.ref).toBe('019e3000-7000-7000-8000-000000000001');
64
+ expect(out!.kind).toBe('screenshot');
65
+ expect(out!.source).toBe('js');
66
+ });
67
+
68
+ test('returns null on a non-201 response', async () => {
69
+ globalThis.fetch = (async () =>
70
+ new Response('{"error":"tooLarge"}', { status: 413 })) as typeof fetch;
71
+ const out = await uploadAttachment('e', 'screenshot', { base64: '', mediaType: 'image/jpeg' });
72
+ expect(out).toBeNull();
73
+ });
74
+
75
+ test('returns null when fetch throws (offline)', async () => {
76
+ globalThis.fetch = (async () => {
77
+ throw new TypeError('Network request failed');
78
+ }) as typeof fetch;
79
+ const out = await uploadAttachment('e', 'screenshot', { base64: '', mediaType: 'image/jpeg' });
80
+ expect(out).toBeNull();
81
+ });
82
+
83
+ test('returns null without an active config (init never ran)', async () => {
84
+ resetConfig();
85
+ const out = await uploadAttachment('e', 'screenshot', { base64: '', mediaType: 'image/jpeg' });
86
+ expect(out).toBeNull();
87
+ });
88
+ });
package/src/capture.ts CHANGED
@@ -1,16 +1,31 @@
1
+ import { addBreadcrumb, getBreadcrumbs } from './breadcrumbs';
1
2
  import { getConfig, isInitialized } from './config';
2
- import { getBreadcrumbs } from './breadcrumbs';
3
3
  import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
4
+ import { captureScreenshot } from './handlers/screenshot';
4
5
  import { markSessionErrored } from './session-tracker';
5
6
  import { parseStack } from './stack';
6
- import { enqueue } from './transport';
7
+ import { enqueue, uploadAttachment } from './transport';
7
8
  import { uuidV7 } from './uuid';
8
- import type { App, Device, Event, SentoriError, Tags, User } from './types';
9
+ import type { App, AttachmentMeta, Device, Event, SentoriError, Tags, User } from './types';
9
10
 
10
11
  declare const __DEV__: boolean | undefined;
11
12
 
12
13
  let _user: User | null = null;
13
14
 
15
+ // Phase 42 sub-D.08 — per-session screenshot quota. Defaults: 10 in
16
+ // prod, unlimited (-1 sentinel) in dev so test loops + react-error-
17
+ // overlay reruns don't run out partway through the session.
18
+ const SCREENSHOT_PROD_LIMIT = 10;
19
+ let _screenshotsTaken = 0;
20
+
21
+ function screenshotBudget(): number {
22
+ return typeof __DEV__ !== 'undefined' && __DEV__ ? -1 : SCREENSHOT_PROD_LIMIT;
23
+ }
24
+
25
+ export const __resetScreenshotBudgetForTests = (): void => {
26
+ _screenshotsTaken = 0;
27
+ };
28
+
14
29
  /**
15
30
  * Attach a stable user identifier to events captured after this call.
16
31
  *
@@ -33,6 +48,11 @@ export type CaptureExtras = {
33
48
  tags?: Tags;
34
49
  user?: User;
35
50
  fingerprint?: string[];
51
+ /** Phase 42 sub-D.07: per-call screenshot override. `false` skips
52
+ * screenshot capture even when `init({ capture: { screenshot:
53
+ * true } })` is on — handy for sensitive screens. Defaults to
54
+ * whatever `config.screenshotsEnabled` says. */
55
+ screenshot?: boolean;
36
56
  };
37
57
 
38
58
  export const captureError = (error: Error, extras?: CaptureExtras): void => {
@@ -60,21 +80,71 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
60
80
  // `errored` so the next AppState=background ping reports unhealthy.
61
81
  markSessionErrored();
62
82
 
83
+ // Phase 42 sub-D.07: opt-in screenshot. Default off; per-call
84
+ // `extras.screenshot: false` always wins so callers can mute it
85
+ // on a sensitive flow even when init has it on globally.
86
+ const wantScreenshot =
87
+ config.screenshotsEnabled && extras?.screenshot !== false && allowScreenshot();
88
+
63
89
  // Phase 40 sub-E: in dev there's no uploaded source map, so ask
64
90
  // Metro to symbolicate the stack before we send it (best-effort,
65
91
  // short timeout). Release builds skip straight to enqueue and let
66
92
  // the server symbolicate at ingest against the uploaded map.
67
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
68
- void symbolicateErrorViaMetro(event.error)
69
- .catch(() => {})
70
- .then(() => enqueue(event));
71
- } else {
93
+ const pipeline = async (): Promise<void> => {
94
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
95
+ await symbolicateErrorViaMetro(event.error).catch(() => {});
96
+ }
97
+ if (wantScreenshot) {
98
+ await captureAndAttachScreenshot(event);
99
+ }
72
100
  enqueue(event);
73
- }
101
+ };
102
+ void pipeline();
74
103
  };
75
104
 
76
105
  export const captureException = captureError;
77
106
 
107
+ /** Phase 42 sub-D.08: per-session screenshot quota gate. */
108
+ function allowScreenshot(): boolean {
109
+ const budget = screenshotBudget();
110
+ if (budget < 0) return true; // dev: unlimited
111
+ if (_screenshotsTaken >= budget) return false;
112
+ _screenshotsTaken += 1;
113
+ return true;
114
+ }
115
+
116
+ /**
117
+ * Phase 42 sub-D.06/07: take a screenshot, upload it, push the
118
+ * server-issued ref into `event.attachments`. Every step is
119
+ * best-effort — on any failure we leave a breadcrumb and let the
120
+ * event ship without a thumbnail.
121
+ */
122
+ async function captureAndAttachScreenshot(event: Event): Promise<void> {
123
+ let blob: Awaited<ReturnType<typeof captureScreenshot>> = null;
124
+ try {
125
+ blob = await captureScreenshot();
126
+ } catch {
127
+ // capture itself shouldn't throw — `captureScreenshot` already
128
+ // catches — but be defensive.
129
+ }
130
+ if (!blob) {
131
+ addBreadcrumb({ type: 'custom', data: { reason: 'screenshot-capture-failed' } });
132
+ return;
133
+ }
134
+ const attachment: AttachmentMeta | null = await uploadAttachment(
135
+ event.id,
136
+ 'screenshot',
137
+ blob,
138
+ { source: 'js' },
139
+ );
140
+ if (!attachment) {
141
+ addBreadcrumb({ type: 'custom', data: { reason: 'screenshot-upload-failed' } });
142
+ return;
143
+ }
144
+ if (!event.attachments) event.attachments = [];
145
+ event.attachments.push(attachment);
146
+ }
147
+
78
148
  const errorToObject = (error: Error): SentoriError => {
79
149
  const causeRaw = (error as { cause?: unknown }).cause;
80
150
  let cause: SentoriError | null = null;
package/src/config.ts CHANGED
@@ -4,6 +4,8 @@ export type Config = {
4
4
  environment: string;
5
5
  ingestUrl: string;
6
6
  enabled: boolean;
7
+ /** Phase 42 sub-D.07: opt-in screenshot capture on captureException. */
8
+ screenshotsEnabled: boolean;
7
9
  };
8
10
 
9
11
  let _config: Config | null = null;
@@ -21,16 +21,47 @@ type MetroFrame = {
21
21
  };
22
22
 
23
23
  /** Resolve `<devServer>/symbolicate`, or null if we're not running
24
- * from a Metro dev server (release build, or not in RN). */
24
+ * from a Metro dev server (release build, or not in RN).
25
+ *
26
+ * Order matters:
27
+ * 1. `react-native/Libraries/Core/Devtools/getDevServer` — the same
28
+ * helper LogBox + RN's own symbolicateStackTrace use. Works under
29
+ * both the legacy bridge and the new architecture (TurboModule),
30
+ * because internally it calls `NativeSourceCode.getConstants()`
31
+ * which is the correct path on new arch.
32
+ * 2. `NativeModules.SourceCode.getConstants().scriptURL` — direct
33
+ * TurboModule fallback if (1) ever moves.
34
+ * 3. `NativeModules.SourceCode.scriptURL` — legacy bridge (pre-new-
35
+ * arch RN). On new arch this property is `undefined` because
36
+ * constants aren't hoisted onto the module object — which is
37
+ * exactly the symptom Insight hit on RN 0.83 + new arch.
38
+ */
25
39
  function metroSymbolicateUrl(): null | string {
26
40
  try {
27
- // NativeModules.SourceCode.scriptURL is the bundle URL — an
28
- // http(s) URL in dev, a `file://` path in a release build.
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const mod = require('react-native/Libraries/Core/Devtools/getDevServer') as {
43
+ default?: () => { bundleLoadedFromServer: boolean; url: string };
44
+ };
45
+ const getDevServer = mod.default ?? (mod as unknown as () => { bundleLoadedFromServer: boolean; url: string });
46
+ const ds = getDevServer();
47
+ if (ds.bundleLoadedFromServer && typeof ds.url === 'string') {
48
+ return ds.url.replace(/\/$/, '') + '/symbolicate';
49
+ }
50
+ } catch {
51
+ // Older RN / non-RN runtime / path moved → fall through to NativeModules.
52
+ }
53
+ try {
29
54
  // eslint-disable-next-line @typescript-eslint/no-require-imports
30
55
  const rn = require('react-native') as {
31
- NativeModules?: { SourceCode?: { scriptURL?: string } };
56
+ NativeModules?: {
57
+ SourceCode?: {
58
+ getConstants?: () => { scriptURL?: string };
59
+ scriptURL?: string;
60
+ };
61
+ };
32
62
  };
33
- const scriptURL = rn.NativeModules?.SourceCode?.scriptURL;
63
+ const sc = rn.NativeModules?.SourceCode;
64
+ const scriptURL = sc?.scriptURL ?? sc?.getConstants?.()?.scriptURL;
34
65
  if (!scriptURL || !/^https?:\/\//.test(scriptURL)) return null;
35
66
  const u = new URL(scriptURL);
36
67
  return `${u.protocol}//${u.host}/symbolicate`;
@@ -0,0 +1,115 @@
1
+ // Phase 42 sub-D.03/04 — capture a screenshot of the current view tree
2
+ // on `captureException`. Off-main-thread, best-effort, opt-in.
3
+ //
4
+ // Performance contract (sub-D.04):
5
+ // - Wait for the in-flight RN interaction batch to drain before
6
+ // touching the view shot (`InteractionManager.runAfterInteractions`)
7
+ // so we never extend the active gesture / animation by a frame.
8
+ // - Yield one paint by chaining a `requestAnimationFrame` so the
9
+ // screenshot reflects post-error UI state, not the frame that
10
+ // was already half-laid-out.
11
+ // - Capped output: 480 px on the longest edge, WebP q=70. Typical
12
+ // payload 30-80 KB; multipart hard cap is 500 KB.
13
+ // - On any failure we silently return null. The error event still
14
+ // goes to the server; the user just doesn't see a thumbnail.
15
+ //
16
+ // `react-native-view-shot` is an OPTIONAL peer. We `require()` it
17
+ // lazily so apps that don't install it never pay the bundle cost
18
+ // or fail at import time. Without it, `captureScreenshot()` returns
19
+ // `null` immediately.
20
+
21
+ import { InteractionManager } from 'react-native';
22
+
23
+ type CaptureRef = (
24
+ // Phase 42: the lib accepts a React ref or — when we pass `undefined` —
25
+ // shoots the root window. We always go for the root (no per-component
26
+ // ref) so the screenshot lines up with what the user just saw.
27
+ refOrUndefined: undefined,
28
+ opts: {
29
+ format?: 'jpg' | 'png' | 'webm';
30
+ quality?: number;
31
+ result?: 'base64' | 'data-uri' | 'tmpfile';
32
+ width?: number;
33
+ height?: number;
34
+ },
35
+ ) => Promise<string>;
36
+
37
+ type ViewShotModule = { captureRef?: CaptureRef; default?: { captureRef?: CaptureRef } };
38
+
39
+ function loadCaptureRef(): CaptureRef | null {
40
+ try {
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const mod = require('react-native-view-shot') as ViewShotModule;
43
+ return mod.captureRef ?? mod.default?.captureRef ?? null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ const MAX_LONG_EDGE_PX = 480;
50
+ const WEBP_QUALITY = 0.7;
51
+ const CAPTURE_TIMEOUT_MS = 1500;
52
+
53
+ /** What `captureScreenshot()` hands back when it succeeds. */
54
+ export type ScreenshotBlob = {
55
+ base64: string;
56
+ mediaType: string;
57
+ };
58
+
59
+ /**
60
+ * Take one screenshot, yielding the JS thread first. Returns null on
61
+ * any error (missing peer dep, native side refused, timeout, etc.).
62
+ * Caller is responsible for opt-in checks (`config.screenshotsEnabled`).
63
+ */
64
+ export async function captureScreenshot(): Promise<ScreenshotBlob | null> {
65
+ const captureRef = loadCaptureRef();
66
+ if (!captureRef) return null;
67
+
68
+ // Wait for the in-flight RN interaction batch to drain. This is
69
+ // why screenshot capture doesn't visibly stall the user's last
70
+ // action — we let React commit before we ask the OS to render.
71
+ await new Promise<void>((resolve) => {
72
+ InteractionManager.runAfterInteractions(() => resolve());
73
+ });
74
+ await new Promise<void>((resolve) => {
75
+ requestAnimationFrame(() => resolve());
76
+ });
77
+
78
+ try {
79
+ const base64 = await withTimeout(
80
+ captureRef(undefined, {
81
+ format: 'jpg',
82
+ quality: WEBP_QUALITY,
83
+ result: 'base64',
84
+ // Long-edge cap. RN view-shot scales preserving aspect ratio
85
+ // when only one dimension is set.
86
+ width: MAX_LONG_EDGE_PX,
87
+ }),
88
+ CAPTURE_TIMEOUT_MS,
89
+ );
90
+ if (!base64) return null;
91
+ // view-shot doesn't ship a WebP encoder on every RN version.
92
+ // JPEG q=70 fits the budget too (typical 40-100 KB) and every
93
+ // version handles it identically. We can swap to WebP once the
94
+ // RN minimum we support has it everywhere.
95
+ return { base64, mediaType: 'image/jpeg' };
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ function withTimeout<T>(p: Promise<T>, ms: number): Promise<T | null> {
102
+ return new Promise((resolve) => {
103
+ const t = setTimeout(() => resolve(null as unknown as T), ms);
104
+ p.then(
105
+ (v) => {
106
+ clearTimeout(t);
107
+ resolve(v);
108
+ },
109
+ () => {
110
+ clearTimeout(t);
111
+ resolve(null as unknown as T);
112
+ },
113
+ );
114
+ });
115
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import { init } from './init';
2
2
  import { addBreadcrumb } from './breadcrumbs';
3
3
  import { setUser, getUser, captureError, captureException } from './capture';
4
4
  import { ErrorBoundary } from './error-boundary';
5
+ import { MaskRegion, setMaskedNode, unsetMaskedNode } from './mask';
5
6
  import {
6
7
  endSession,
7
8
  markSessionCrashed,
@@ -16,6 +17,9 @@ export const sentori = {
16
17
  captureError,
17
18
  captureException,
18
19
  ErrorBoundary,
20
+ MaskRegion,
21
+ setMaskedNode,
22
+ unsetMaskedNode,
19
23
  startSession,
20
24
  endSession,
21
25
  markSessionCrashed,
@@ -27,6 +31,7 @@ export { init, init as initSentori } from './init';
27
31
  export { addBreadcrumb } from './breadcrumbs';
28
32
  export { setUser, getUser, captureError, captureException } from './capture';
29
33
  export { ErrorBoundary } from './error-boundary';
34
+ export { MaskRegion, setMaskedNode, unsetMaskedNode } from './mask';
30
35
  export {
31
36
  startAnrWatchdog,
32
37
  stopAnrWatchdog,
package/src/init.ts CHANGED
@@ -28,6 +28,11 @@ export type InitOptions = {
28
28
  * foreground (`AppState` → `active`), ends it on background.
29
29
  * Drives crash-free rate. Set `false` to opt out. */
30
30
  sessions?: boolean;
31
+ /** Phase 42 sub-D.07: capture a screenshot of the current screen
32
+ * on `captureException`. Opt-in — requires `react-native-view-shot`
33
+ * installed and `<MaskRegion>` placed over any sensitive UI. The
34
+ * image is webp q=70 480 px max, < 100 KB typical. */
35
+ screenshot?: boolean;
31
36
  };
32
37
  };
33
38
 
@@ -51,6 +56,7 @@ export const init = (options: InitOptions): void => {
51
56
  environment: env,
52
57
  ingestUrl: options.ingestUrl ?? DEFAULT_INGEST_URL,
53
58
  enabled: true,
59
+ screenshotsEnabled: options.capture?.screenshot === true,
54
60
  });
55
61
 
56
62
  // Tell the native crash handler about the config so the JSON it writes