yaver-feedback-react-native 0.8.13 → 0.9.2
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/app.plugin.js +126 -9
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -2
- package/dist/FeedbackModal.js +477 -66
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +94 -1
- package/dist/P2PClient.js +281 -2
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +6 -1
- package/dist/VibeChatScreen.js +204 -1
- package/dist/YaverFeedback.d.ts +98 -0
- package/dist/YaverFeedback.js +509 -46
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
- package/dist/__tests__/ReportIdentity.test.js +168 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/device.d.ts +19 -9
- package/dist/_core/device.js +20 -14
- package/dist/capture.d.ts +6 -0
- package/dist/capture.js +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +215 -3
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +14 -3
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +563 -87
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +314 -2
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +219 -0
- package/src/YaverFeedback.ts +498 -46
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/ReportIdentity.test.ts +203 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/device.ts +20 -14
- package/src/capture.ts +51 -0
- package/src/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +217 -3
- package/src/voice.ts +270 -0
package/src/YaverFeedback.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { NativeModules } from 'react-native';
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
2
|
import { FeedbackConfig, CapturedError } from './types';
|
|
3
3
|
import { YaverDiscovery } from './Discovery';
|
|
4
4
|
import { BlackBox } from './BlackBox';
|
|
5
|
-
import { P2PClient } from './P2PClient';
|
|
5
|
+
import { P2PClient, resolveReportIdentity } from './P2PClient';
|
|
6
6
|
import { ShakeDetector } from './ShakeDetector';
|
|
7
|
+
import {
|
|
8
|
+
captureStoreScreenshots,
|
|
9
|
+
CaptureStoreScreenshotsOptions,
|
|
10
|
+
CaptureStoreScreenshotsResult,
|
|
11
|
+
} from './storeShots';
|
|
7
12
|
import {
|
|
8
13
|
configureAuthEndpoints,
|
|
9
14
|
setStrictNativeAuth,
|
|
@@ -23,6 +28,13 @@ import {
|
|
|
23
28
|
QuickIconColorPreset,
|
|
24
29
|
} from './preferences';
|
|
25
30
|
|
|
31
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
32
|
+
const maybeNodeTimer = timer as unknown as { unref?: () => void };
|
|
33
|
+
if (typeof maybeNodeTimer.unref === 'function') {
|
|
34
|
+
maybeNodeTimer.unref();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
// Is this JS runtime the Yaver mobile app's super-host bridge? The
|
|
27
39
|
// YaverInfo native module is only registered inside Yaver's container
|
|
28
40
|
// (mobile/ios/Yaver/YaverInfo.{swift,m} + Android counterpart); a
|
|
@@ -147,9 +159,16 @@ let config: FeedbackConfig | null = null;
|
|
|
147
159
|
let enabled = false;
|
|
148
160
|
let p2pClient: P2PClient | null = null;
|
|
149
161
|
let shakeDetector: ShakeDetector | null = null;
|
|
162
|
+
/** Unsubscribe for the BlackBox command handler registered by init(). Held so
|
|
163
|
+
* a re-init (or destroy) can drop the old one instead of stacking. */
|
|
164
|
+
let commandUnsubscribe: (() => void) | null = null;
|
|
150
165
|
let p2pAuthToken: string | null = null;
|
|
151
166
|
let p2pRelayPassword: string = '';
|
|
167
|
+
/** Pending BlackBox auto-start retry. Held so destroy()/re-init can cancel it
|
|
168
|
+
* instead of leaving a timer chain running against a torn-down config. */
|
|
169
|
+
let autoStartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
152
170
|
let reportLaunchInFlight = false;
|
|
171
|
+
let crashReportInFlight = false;
|
|
153
172
|
|
|
154
173
|
/** Resolve the user's relay password by validating their auth token
|
|
155
174
|
* against Convex. Used whenever we (re)build the P2PClient so a
|
|
@@ -196,6 +215,7 @@ async function resolveRelayPassword(authToken: string, convexUrl?: string): Prom
|
|
|
196
215
|
/** Ring buffer of captured errors. */
|
|
197
216
|
let errorBuffer: CapturedError[] = [];
|
|
198
217
|
let maxErrors = 5;
|
|
218
|
+
let crashHandlerInstalled = false;
|
|
199
219
|
|
|
200
220
|
/** Track whether BlackBox was running before disable (to restart on enable). */
|
|
201
221
|
let blackBoxWasStreaming = false;
|
|
@@ -266,6 +286,79 @@ export class YaverFeedback {
|
|
|
266
286
|
* If no `agentUrl` is provided, the SDK will attempt auto-discovery
|
|
267
287
|
* via `YaverDiscovery` on the first `startReport()` call.
|
|
268
288
|
*/
|
|
289
|
+
/** The runtime lane, when running on web inside a Yaver preview WebView. */
|
|
290
|
+
static detectWebLane(): 'browser' | 'webrtc' | null {
|
|
291
|
+
try {
|
|
292
|
+
if (Platform.OS !== 'web' || typeof window === 'undefined') return null;
|
|
293
|
+
const l = String((window as unknown as { __yaverLane?: string }).__yaverLane || '').toLowerCase();
|
|
294
|
+
if (l === 'browser' || l === 'webrtc') return l;
|
|
295
|
+
} catch { /* noop */ }
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Mount a draggable DOM floating "Y" icon for the BROWSER lane (RN-web). It
|
|
301
|
+
* lives in the WebView's document.body (position:fixed, max z-index), so it
|
|
302
|
+
* renders ON TOP of the page inside the WebView and is never occluded by the
|
|
303
|
+
* fullScreen preview modal — the one occlusion-proof affordance on iOS.
|
|
304
|
+
* Tap opens the SDK's existing report flow (the RN FeedbackModal renders in
|
|
305
|
+
* the same WebView DOM, also un-occluded). No-op off-web or off-lane, and
|
|
306
|
+
* idempotent (a re-init reuses the existing node).
|
|
307
|
+
*/
|
|
308
|
+
static mountBrowserLaneIcon(): void {
|
|
309
|
+
if (YaverFeedback.detectWebLane() !== 'browser') return;
|
|
310
|
+
try {
|
|
311
|
+
const doc = (globalThis as unknown as { document?: Document }).document;
|
|
312
|
+
if (!doc || !doc.body) return;
|
|
313
|
+
if (doc.getElementById('yaver-feedback-btn')) return; // idempotent
|
|
314
|
+
const btn = doc.createElement('div');
|
|
315
|
+
btn.id = 'yaver-feedback-btn';
|
|
316
|
+
btn.textContent = 'Y';
|
|
317
|
+
btn.title = 'Yaver Feedback — drag to move · tap to report';
|
|
318
|
+
btn.style.cssText =
|
|
319
|
+
'position:fixed;bottom:20px;right:20px;width:44px;height:44px;border-radius:50%;' +
|
|
320
|
+
'background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;' +
|
|
321
|
+
'cursor:pointer;z-index:99999;font-size:18px;font-weight:bold;touch-action:none;' +
|
|
322
|
+
'box-shadow:0 4px 12px rgba(99,102,241,0.4);transition:transform 0.2s;';
|
|
323
|
+
const drag = { on: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
|
|
324
|
+
const w = window as unknown as { innerWidth: number; innerHeight: number };
|
|
325
|
+
const applyPos = (x: number, y: number) => {
|
|
326
|
+
const m = 8;
|
|
327
|
+
const nx = Math.max(m, Math.min(w.innerWidth - 44 - m, x));
|
|
328
|
+
const ny = Math.max(m, Math.min(w.innerHeight - 44 - m, y));
|
|
329
|
+
btn.style.left = nx + 'px'; btn.style.top = ny + 'px';
|
|
330
|
+
btn.style.right = 'auto'; btn.style.bottom = 'auto';
|
|
331
|
+
try { localStorage.setItem('yaver-feedback-btn-pos', JSON.stringify({ x: nx, y: ny })); } catch { /* noop */ }
|
|
332
|
+
};
|
|
333
|
+
try { const s = localStorage.getItem('yaver-feedback-btn-pos'); if (s) { const p = JSON.parse(s); if (typeof p?.x === 'number') applyPos(p.x, p.y); } } catch { /* noop */ }
|
|
334
|
+
btn.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
335
|
+
drag.on = true; drag.moved = false;
|
|
336
|
+
const r = btn.getBoundingClientRect();
|
|
337
|
+
drag.sx = e.clientX; drag.sy = e.clientY; drag.ox = r.left; drag.oy = r.top;
|
|
338
|
+
btn.setPointerCapture(e.pointerId); btn.style.transition = 'none';
|
|
339
|
+
});
|
|
340
|
+
btn.addEventListener('pointermove', (e: PointerEvent) => {
|
|
341
|
+
if (!drag.on) return;
|
|
342
|
+
const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
|
|
343
|
+
if (Math.abs(dx) + Math.abs(dy) > 5) drag.moved = true;
|
|
344
|
+
if (drag.moved) applyPos(drag.ox + dx, drag.oy + dy);
|
|
345
|
+
});
|
|
346
|
+
const end = (e: PointerEvent) => {
|
|
347
|
+
if (!drag.on) return;
|
|
348
|
+
drag.on = false; btn.style.transition = 'transform 0.2s';
|
|
349
|
+
try { btn.releasePointerCapture(e.pointerId); } catch { /* noop */ }
|
|
350
|
+
if (!drag.moved) { try { void YaverFeedback.startReport(); } catch { /* noop */ } }
|
|
351
|
+
};
|
|
352
|
+
btn.addEventListener('pointerup', end);
|
|
353
|
+
btn.addEventListener('pointercancel', end);
|
|
354
|
+
// Also answer the host's forwarded shake (Yaver injects yaver-feedback:launch).
|
|
355
|
+
const launch = () => { try { void YaverFeedback.startReport(); } catch { /* noop */ } };
|
|
356
|
+
window.addEventListener('yaver-feedback:launch', launch as EventListener);
|
|
357
|
+
(window as unknown as { __yaverFeedbackLaunch?: () => void }).__yaverFeedbackLaunch = launch;
|
|
358
|
+
doc.body.appendChild(btn);
|
|
359
|
+
} catch { /* noop — never break the guest app over a feedback affordance */ }
|
|
360
|
+
}
|
|
361
|
+
|
|
269
362
|
static init(cfg: FeedbackConfig): void {
|
|
270
363
|
config = {
|
|
271
364
|
trigger: 'shake',
|
|
@@ -293,6 +386,9 @@ export class YaverFeedback {
|
|
|
293
386
|
}
|
|
294
387
|
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
295
388
|
errorBuffer = [];
|
|
389
|
+
if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
|
|
390
|
+
YaverFeedback.installCrashHandler();
|
|
391
|
+
}
|
|
296
392
|
return;
|
|
297
393
|
}
|
|
298
394
|
if (config.disableShakeGesture && (!config.quickIcon || config.quickIcon === 'auto')) {
|
|
@@ -359,24 +455,25 @@ export class YaverFeedback {
|
|
|
359
455
|
// Set up error capture buffer size
|
|
360
456
|
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
361
457
|
errorBuffer = [];
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (shakeDetector) {
|
|
365
|
-
shakeDetector.stop();
|
|
366
|
-
shakeDetector = null;
|
|
367
|
-
}
|
|
368
|
-
if (enabled && config.trigger === 'shake' && !config.disableShakeGesture) {
|
|
369
|
-
shakeDetector = new ShakeDetector();
|
|
370
|
-
shakeDetector.start(() => {
|
|
371
|
-
YaverFeedback.notifyShake();
|
|
372
|
-
if (config?.reportingOnly) {
|
|
373
|
-
YaverFeedback.sendAutoReport();
|
|
374
|
-
} else {
|
|
375
|
-
YaverFeedback.startReport();
|
|
376
|
-
}
|
|
377
|
-
});
|
|
458
|
+
if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
|
|
459
|
+
YaverFeedback.installCrashHandler();
|
|
378
460
|
}
|
|
379
461
|
|
|
462
|
+
// Wire up shake detection when trigger is 'shake'. Rebuild from scratch:
|
|
463
|
+
// config was just replaced, so the old detector may be closing over stale
|
|
464
|
+
// settings.
|
|
465
|
+
YaverFeedback.stopShakeDetector();
|
|
466
|
+
YaverFeedback.syncShakeDetector();
|
|
467
|
+
|
|
468
|
+
// Browser lane (RN-web inside Yaver's fullScreen WebView preview): the
|
|
469
|
+
// native shake module doesn't exist on web, and Yaver's own container
|
|
470
|
+
// overlay is OCCLUDED behind the WebView modal on iOS. So mount an
|
|
471
|
+
// occlusion-proof DRAGGABLE DOM icon INSIDE the WebView — same pattern as
|
|
472
|
+
// yaver-feedback-web. Lane is stamped by Yaver via
|
|
473
|
+
// window.__yaverLane='browser' (mobile PREVIEW_LANE_SCRIPT). See
|
|
474
|
+
// docs/audits/feedback-sdk-lanes-audit-2026-07-28.md.
|
|
475
|
+
if (enabled) YaverFeedback.mountBrowserLaneIcon();
|
|
476
|
+
|
|
380
477
|
// Wire up BlackBox command handlers for reload + status signals from
|
|
381
478
|
// the agent. Opens an SSE channel the agent uses to:
|
|
382
479
|
// - broadcast reload_bundle so the phone picks up a fresh Hermes
|
|
@@ -385,7 +482,14 @@ export class YaverFeedback {
|
|
|
385
482
|
// assets…", "Done") so the SDK can surface it like a normal
|
|
386
483
|
// "Working…" spinner instead of a silent 60-second freeze.
|
|
387
484
|
if (enabled) {
|
|
388
|
-
|
|
485
|
+
// onCommand appends to a static array and hands back an unsubscribe.
|
|
486
|
+
// Dropping that unsubscribe meant every re-init stacked another handler
|
|
487
|
+
// on top of the last, so an app that re-initialised (say, a settings
|
|
488
|
+
// toggle) reloaded twice per agent command, then three times, and so on.
|
|
489
|
+
// destroy() never cleared them either. Drop the previous registration
|
|
490
|
+
// first so init() is idempotent here.
|
|
491
|
+
commandUnsubscribe?.();
|
|
492
|
+
commandUnsubscribe = BlackBox.onCommand((cmd) => {
|
|
389
493
|
if (cmd.command === 'reload') {
|
|
390
494
|
if (cfg.onReload) {
|
|
391
495
|
cfg.onReload();
|
|
@@ -447,16 +551,24 @@ export class YaverFeedback {
|
|
|
447
551
|
// SFMG used to call BlackBox.start() inside YaverFeedbackWidget
|
|
448
552
|
// after auth — that path still works (start() is idempotent), so
|
|
449
553
|
// upgrading SDK without removing the manual call is safe.
|
|
554
|
+
// 4. RETRY, rather than checking once. This was a single 500ms
|
|
555
|
+
// timeout, and on a cold start it could not succeed: an app that
|
|
556
|
+
// passes neither agentUrl nor authToken to init() (the normal
|
|
557
|
+
// case — they're restored from storage) gets its session from
|
|
558
|
+
// `void hydrateSession()`, which reads two AsyncStorage keys and
|
|
559
|
+
// then awaits discoverAgent(), a Convex round trip plus LAN
|
|
560
|
+
// probing. That does not finish in 500ms, so the check found no
|
|
561
|
+
// agentUrl, gave up, and BlackBox never started — meaning no SSE
|
|
562
|
+
// channel, so the agent's `reload_bundle` command had nowhere to
|
|
563
|
+
// land and Hermes hot reload silently did nothing until the user
|
|
564
|
+
// shook the device and drove discovery by hand.
|
|
565
|
+
//
|
|
566
|
+
// The both-conditions guard is what keeps the 401 retry storm
|
|
567
|
+
// from coming back, so it is preserved exactly; we only re-check
|
|
568
|
+
// it over time instead of once. Bounded so a device that never
|
|
569
|
+
// signs in doesn't poll forever.
|
|
450
570
|
if (cfg.autoStartBlackBox !== false) {
|
|
451
|
-
|
|
452
|
-
if (config?.agentUrl && (config?.authToken || p2pAuthToken)) {
|
|
453
|
-
try {
|
|
454
|
-
BlackBox.start();
|
|
455
|
-
} catch (err) {
|
|
456
|
-
console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}, 500);
|
|
571
|
+
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
460
572
|
}
|
|
461
573
|
}
|
|
462
574
|
|
|
@@ -471,6 +583,40 @@ export class YaverFeedback {
|
|
|
471
583
|
// pass-through wrapper they insert into their own error chain
|
|
472
584
|
}
|
|
473
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Opt-in global crash handler. This wraps the existing React Native
|
|
588
|
+
* ErrorUtils handler and still calls it, so Sentry/Crashlytics/Bugsnag can
|
|
589
|
+
* remain the system of record. The SDK only uploads a crash report and,
|
|
590
|
+
* when configured, asks the agent to create a fix task.
|
|
591
|
+
*/
|
|
592
|
+
static installCrashHandler(): void {
|
|
593
|
+
if (crashHandlerInstalled) return;
|
|
594
|
+
try {
|
|
595
|
+
const errorUtils = (globalThis as any)?.ErrorUtils;
|
|
596
|
+
if (!errorUtils?.getGlobalHandler || !errorUtils?.setGlobalHandler) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const next = errorUtils.getGlobalHandler();
|
|
600
|
+
errorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => {
|
|
601
|
+
YaverFeedback.attachError(error, {
|
|
602
|
+
source: 'global-handler',
|
|
603
|
+
crashAware: true,
|
|
604
|
+
});
|
|
605
|
+
if (errorBuffer.length > 0) {
|
|
606
|
+
errorBuffer[errorBuffer.length - 1].isFatal = isFatal ?? true;
|
|
607
|
+
}
|
|
608
|
+
void YaverFeedback.reportCrash(error, {
|
|
609
|
+
isFatal: isFatal ?? true,
|
|
610
|
+
source: 'global-handler',
|
|
611
|
+
});
|
|
612
|
+
next?.(error, isFatal);
|
|
613
|
+
});
|
|
614
|
+
crashHandlerInstalled = true;
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.warn('[YaverFeedback] installCrashHandler failed:', err);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
474
620
|
/**
|
|
475
621
|
* Run agent discovery in the background.
|
|
476
622
|
* Called automatically from init() when no agentUrl is provided.
|
|
@@ -564,6 +710,12 @@ export class YaverFeedback {
|
|
|
564
710
|
} else {
|
|
565
711
|
await YaverFeedback.discoverAgent();
|
|
566
712
|
}
|
|
713
|
+
// Signing in is the event the auto-start was waiting for. If its bounded
|
|
714
|
+
// retry already gave up (device left signed out past the window), this is
|
|
715
|
+
// what brings the command channel up without requiring an app restart.
|
|
716
|
+
if (config.autoStartBlackBox !== false && !BlackBox.isStreaming) {
|
|
717
|
+
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
718
|
+
}
|
|
567
719
|
}
|
|
568
720
|
|
|
569
721
|
/** Returns true once the SDK has a session token it can use. */
|
|
@@ -571,6 +723,59 @@ export class YaverFeedback {
|
|
|
571
723
|
return Boolean(config?.authToken);
|
|
572
724
|
}
|
|
573
725
|
|
|
726
|
+
/**
|
|
727
|
+
* On-device App Store screenshot capture (Engine 2). Walks the routes
|
|
728
|
+
* the host hands us, screenshots each, and uploads to the agent which
|
|
729
|
+
* runs the App Store Connect backend. agentUrl / authToken / relay
|
|
730
|
+
* password default to the SDK's resolved session; the host only has to
|
|
731
|
+
* supply a navigation ref + the ordered route list.
|
|
732
|
+
*
|
|
733
|
+
* YaverFeedback.captureStoreScreenshots({
|
|
734
|
+
* app: 'sfmg',
|
|
735
|
+
* navigationRef,
|
|
736
|
+
* routes: ['/(tabs)/dashboard', '/(tabs)/messages', '/(tabs)/clients'],
|
|
737
|
+
* submit: true,
|
|
738
|
+
* })
|
|
739
|
+
*/
|
|
740
|
+
static async captureStoreScreenshots(
|
|
741
|
+
opts: Omit<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'> &
|
|
742
|
+
Partial<Pick<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'>>,
|
|
743
|
+
): Promise<CaptureStoreScreenshotsResult> {
|
|
744
|
+
const agentUrl = opts.agentUrl ?? config?.agentUrl ?? '';
|
|
745
|
+
const authToken = opts.authToken ?? config?.authToken ?? p2pAuthToken ?? '';
|
|
746
|
+
const relayPassword = opts.relayPassword ?? p2pRelayPassword;
|
|
747
|
+
if (!agentUrl || !authToken) {
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
captured: 0,
|
|
751
|
+
uploaded: 0,
|
|
752
|
+
message: 'YaverFeedback not connected to an agent — init() with agentUrl + authToken first.',
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return captureStoreScreenshots({ ...opts, agentUrl, authToken, relayPassword });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Let the mobile app / CLI kick THIS device into self-capturing. Registers
|
|
760
|
+
* a BlackBox command listener that fires captureStoreScreenshots when the
|
|
761
|
+
* agent pushes a `capture_store_shots` command (its `data` may override
|
|
762
|
+
* `submit` / `locale`). Returns an unsubscribe fn. Call once after init,
|
|
763
|
+
* passing the app's navigation ref + the routes to walk.
|
|
764
|
+
*/
|
|
765
|
+
static enableStoreShotsOnCommand(
|
|
766
|
+
opts: Omit<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'>,
|
|
767
|
+
): () => void {
|
|
768
|
+
return BlackBox.onCommand((cmd) => {
|
|
769
|
+
if (cmd?.command !== 'capture_store_shots') return;
|
|
770
|
+
const data = (cmd as any).data ?? {};
|
|
771
|
+
void YaverFeedback.captureStoreScreenshots({
|
|
772
|
+
...opts,
|
|
773
|
+
submit: typeof data.submit === 'boolean' ? data.submit : opts.submit,
|
|
774
|
+
locale: typeof data.locale === 'string' ? data.locale : opts.locale,
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
574
779
|
/**
|
|
575
780
|
* Request the embedded FeedbackModal to show the login screen. Works by
|
|
576
781
|
* emitting an event the modal listens for — avoids forcing the host app
|
|
@@ -779,32 +984,145 @@ export class YaverFeedback {
|
|
|
779
984
|
}
|
|
780
985
|
BlackBox.unwrapConsole(); // ensure console is restored even if BlackBox wasn't started
|
|
781
986
|
errorBuffer = [];
|
|
782
|
-
|
|
783
|
-
shakeDetector.stop();
|
|
784
|
-
shakeDetector = null;
|
|
785
|
-
}
|
|
987
|
+
YaverFeedback.stopShakeDetector();
|
|
786
988
|
} else {
|
|
787
989
|
// === ENABLE ===
|
|
788
990
|
if (blackBoxWasStreaming) {
|
|
789
991
|
BlackBox.start(); // restart with previous config
|
|
790
992
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
shakeDetector = new ShakeDetector();
|
|
794
|
-
shakeDetector.start(() => {
|
|
795
|
-
YaverFeedback.notifyShake();
|
|
796
|
-
if (config?.reportingOnly) {
|
|
797
|
-
YaverFeedback.sendAutoReport();
|
|
798
|
-
} else {
|
|
799
|
-
YaverFeedback.startReport();
|
|
800
|
-
}
|
|
801
|
-
});
|
|
802
|
-
}
|
|
993
|
+
enabled = true; // syncShakeDetector reads this
|
|
994
|
+
YaverFeedback.syncShakeDetector();
|
|
803
995
|
}
|
|
804
996
|
|
|
805
997
|
enabled = value;
|
|
806
998
|
}
|
|
807
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* Turn shake-to-report on or off without tearing the SDK down.
|
|
1002
|
+
*
|
|
1003
|
+
* `trigger` is read only inside init(), so before this the only ways to
|
|
1004
|
+
* stop listening for a shake were setEnabled(false) — which also kills the
|
|
1005
|
+
* flight recorder and the agent command channel — or a re-init, which used
|
|
1006
|
+
* to stack a duplicate command handler. Neither is what "turn the shake
|
|
1007
|
+
* catcher off" should cost.
|
|
1008
|
+
*
|
|
1009
|
+
* Persists onto config.disableShakeGesture, so a later setEnabled(true)
|
|
1010
|
+
* honours it rather than resurrecting the listener.
|
|
1011
|
+
*/
|
|
1012
|
+
static setShakeEnabled(value: boolean): void {
|
|
1013
|
+
if (!config) return;
|
|
1014
|
+
config.disableShakeGesture = !value;
|
|
1015
|
+
YaverFeedback.syncShakeDetector();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Whether the shake listener is currently armed. */
|
|
1019
|
+
static isShakeEnabled(): boolean {
|
|
1020
|
+
return shakeDetector !== null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Bring the shake listener in line with the current config. Idempotent —
|
|
1025
|
+
* safe to call whenever `enabled`, `trigger`, or `disableShakeGesture`
|
|
1026
|
+
* moves.
|
|
1027
|
+
*/
|
|
1028
|
+
private static syncShakeDetector(): void {
|
|
1029
|
+
const want =
|
|
1030
|
+
enabled && config?.trigger === 'shake' && !config?.disableShakeGesture;
|
|
1031
|
+
if (want && !shakeDetector) {
|
|
1032
|
+
shakeDetector = new ShakeDetector();
|
|
1033
|
+
shakeDetector.start(() => {
|
|
1034
|
+
YaverFeedback.notifyShake();
|
|
1035
|
+
if (config?.reportingOnly) {
|
|
1036
|
+
YaverFeedback.sendAutoReport();
|
|
1037
|
+
} else {
|
|
1038
|
+
YaverFeedback.startReport();
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
} else if (!want && shakeDetector) {
|
|
1042
|
+
YaverFeedback.stopShakeDetector();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private static stopShakeDetector(): void {
|
|
1047
|
+
if (shakeDetector) {
|
|
1048
|
+
shakeDetector.stop();
|
|
1049
|
+
shakeDetector = null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** Cancel a pending BlackBox auto-start retry chain. */
|
|
1054
|
+
private static cancelBlackBoxAutoStart(): void {
|
|
1055
|
+
if (autoStartTimer) {
|
|
1056
|
+
clearTimeout(autoStartTimer);
|
|
1057
|
+
autoStartTimer = null;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Start BlackBox as soon as we have BOTH an agentUrl and a token, polling
|
|
1063
|
+
* until then.
|
|
1064
|
+
*
|
|
1065
|
+
* Both conditions are mandatory and deliberate: starting without a token
|
|
1066
|
+
* makes the SSE channel 401 and retry with backoff, which is the tight
|
|
1067
|
+
* string-concat + JSON-parse loop that used to SIGSEGV Hermes on iOS 18.3.1
|
|
1068
|
+
* during Screenshot & Fix. This only re-checks that same guard over time.
|
|
1069
|
+
*
|
|
1070
|
+
* Bounded at ~60s: long enough for AsyncStorage hydration plus a Convex
|
|
1071
|
+
* discovery round trip on a cold, off-LAN start, short enough that a device
|
|
1072
|
+
* whose user never signs in stops polling. Giving up here costs nothing —
|
|
1073
|
+
* setAuthToken() and startReport() both reschedule, so signing in later
|
|
1074
|
+
* still brings the channel up.
|
|
1075
|
+
*/
|
|
1076
|
+
private static scheduleBlackBoxAutoStart(attempt = 0): void {
|
|
1077
|
+
const MAX_ATTEMPTS = 60;
|
|
1078
|
+
const FIRST_DELAY_MS = 500;
|
|
1079
|
+
const RETRY_DELAY_MS = 1000;
|
|
1080
|
+
// Re-run discovery every Nth tick rather than every tick: it is a Convex
|
|
1081
|
+
// round trip plus LAN probing, so once a second would hammer both. Every
|
|
1082
|
+
// 3s is responsive enough that waking the dev machine reconnects the phone
|
|
1083
|
+
// on its own, without the user wondering whether to restart the app.
|
|
1084
|
+
const REDISCOVER_EVERY = 3;
|
|
1085
|
+
|
|
1086
|
+
YaverFeedback.cancelBlackBoxAutoStart();
|
|
1087
|
+
if (!enabled || attempt >= MAX_ATTEMPTS) return;
|
|
1088
|
+
|
|
1089
|
+
const timer = setTimeout(() => {
|
|
1090
|
+
autoStartTimer = null;
|
|
1091
|
+
// A destroy() or setEnabled(false) between scheduling and firing.
|
|
1092
|
+
if (!enabled || !config) return;
|
|
1093
|
+
if (BlackBox.isStreaming) return;
|
|
1094
|
+
|
|
1095
|
+
if (config.agentUrl && (config.authToken || p2pAuthToken)) {
|
|
1096
|
+
try {
|
|
1097
|
+
// Pass the host's BlackBox config through. This used to call
|
|
1098
|
+
// start() bare, so a host that configured the flight recorder
|
|
1099
|
+
// — as the README itself shows — had it silently replaced by
|
|
1100
|
+
// defaults, and appName came out ''.
|
|
1101
|
+
BlackBox.start(config.blackBox);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
|
|
1104
|
+
}
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// No agent yet. Re-attempt discovery instead of only re-reading the
|
|
1109
|
+
// flag: hydrateSession() runs discoverAgent() exactly once at init, so
|
|
1110
|
+
// if the dev machine was unreachable at that moment — asleep, phone
|
|
1111
|
+
// still on cellular before the relay came up, agent not yet serving —
|
|
1112
|
+
// agentUrl stayed null for the whole process lifetime and only an app
|
|
1113
|
+
// restart could recover it. discoverAgent() self-guards on "already
|
|
1114
|
+
// have a URL" and "no token", so calling it again is cheap and safe.
|
|
1115
|
+
if (!config.agentUrl && attempt > 0 && attempt % REDISCOVER_EVERY === 0) {
|
|
1116
|
+
void YaverFeedback.discoverAgent();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
YaverFeedback.scheduleBlackBoxAutoStart(attempt + 1);
|
|
1120
|
+
}, attempt === 0 ? FIRST_DELAY_MS : RETRY_DELAY_MS);
|
|
1121
|
+
|
|
1122
|
+
autoStartTimer = timer;
|
|
1123
|
+
unrefTimer(timer);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
808
1126
|
/** Returns whether the SDK is currently enabled. */
|
|
809
1127
|
static isEnabled(): boolean {
|
|
810
1128
|
return enabled;
|
|
@@ -883,6 +1201,113 @@ export class YaverFeedback {
|
|
|
883
1201
|
};
|
|
884
1202
|
}
|
|
885
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* Upload a crash-aware feedback bundle. Apps can call this from their own
|
|
1206
|
+
* error boundary/global handler. If crashReporting.autoFix is true, the SDK
|
|
1207
|
+
* also triggers the agent's feedback-fix task; reload delivery still happens
|
|
1208
|
+
* through the existing BlackBox command stream.
|
|
1209
|
+
*/
|
|
1210
|
+
static async reportCrash(
|
|
1211
|
+
error: Error,
|
|
1212
|
+
opts?: {
|
|
1213
|
+
isFatal?: boolean;
|
|
1214
|
+
source?: 'manual' | 'global-handler';
|
|
1215
|
+
metadata?: Record<string, unknown>;
|
|
1216
|
+
autoFix?: boolean;
|
|
1217
|
+
},
|
|
1218
|
+
): Promise<{ reportId?: string; taskId?: string }> {
|
|
1219
|
+
if (!config || !enabled || !config.crashReporting?.enabled) return {};
|
|
1220
|
+
if (crashReportInFlight) return {};
|
|
1221
|
+
crashReportInFlight = true;
|
|
1222
|
+
try {
|
|
1223
|
+
YaverFeedback.attachError(error, {
|
|
1224
|
+
...(config.crashReporting.metadata || {}),
|
|
1225
|
+
...(opts?.metadata || {}),
|
|
1226
|
+
crashAware: true,
|
|
1227
|
+
});
|
|
1228
|
+
if (errorBuffer.length > 0) {
|
|
1229
|
+
errorBuffer[errorBuffer.length - 1].isFatal = opts?.isFatal ?? true;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (!config.agentUrl) {
|
|
1233
|
+
await YaverFeedback.discoverAgent();
|
|
1234
|
+
}
|
|
1235
|
+
if (!config.agentUrl) {
|
|
1236
|
+
console.warn('[YaverFeedback] No agent URL — cannot send crash report.');
|
|
1237
|
+
return {};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const { Platform, Dimensions } = require('react-native');
|
|
1241
|
+
const { uploadFeedback } = require('./upload');
|
|
1242
|
+
const { width, height } = Dimensions.get('window');
|
|
1243
|
+
let screenshotPath: string | undefined;
|
|
1244
|
+
if (config.crashReporting.captureScreenshot !== false) {
|
|
1245
|
+
try {
|
|
1246
|
+
const { captureScreenshot } = require('./capture');
|
|
1247
|
+
screenshotPath = await captureScreenshot();
|
|
1248
|
+
} catch {}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const autoFix = opts?.autoFix ?? config.crashReporting.autoFix === true;
|
|
1252
|
+
const identity = resolveReportIdentity({
|
|
1253
|
+
projectName: config?.projectName,
|
|
1254
|
+
bundleId: config?.bundleId,
|
|
1255
|
+
surface: config?.surface,
|
|
1256
|
+
surfaces: config?.surfaces,
|
|
1257
|
+
stack: config?.stack,
|
|
1258
|
+
stacks: config?.stacks,
|
|
1259
|
+
testSurfaces: config?.testSurfaces,
|
|
1260
|
+
feedbackSdk: config?.feedbackSdk,
|
|
1261
|
+
feedbackTransport: config?.feedbackTransport,
|
|
1262
|
+
voiceCapabilities: config?.voiceCapabilities,
|
|
1263
|
+
sttProvider: config?.sttProvider,
|
|
1264
|
+
ttsProvider: config?.ttsProvider,
|
|
1265
|
+
});
|
|
1266
|
+
const bundle = {
|
|
1267
|
+
metadata: {
|
|
1268
|
+
timestamp: new Date().toISOString(),
|
|
1269
|
+
reportKind: 'crash',
|
|
1270
|
+
deviceInfo: {
|
|
1271
|
+
platform: Platform.OS,
|
|
1272
|
+
osVersion: String(Platform.Version),
|
|
1273
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
1274
|
+
screenWidth: width,
|
|
1275
|
+
screenHeight: height,
|
|
1276
|
+
appName: identity.appName,
|
|
1277
|
+
},
|
|
1278
|
+
app: identity.app,
|
|
1279
|
+
project: identity.project,
|
|
1280
|
+
userNote: '[Crash report]',
|
|
1281
|
+
crash: {
|
|
1282
|
+
message: error.message,
|
|
1283
|
+
isFatal: opts?.isFatal ?? true,
|
|
1284
|
+
source: opts?.source ?? 'manual',
|
|
1285
|
+
autoFixRequested: autoFix,
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1288
|
+
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
1289
|
+
errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const uploaded = await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle, p2pRelayPassword);
|
|
1293
|
+
const reportId =
|
|
1294
|
+
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
1295
|
+
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
1296
|
+
let taskId: string | undefined;
|
|
1297
|
+
if (autoFix && reportId) {
|
|
1298
|
+
const client = p2pClient ?? new P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
|
|
1299
|
+
const fix = await client.triggerFix(reportId);
|
|
1300
|
+
taskId = fix?.taskId;
|
|
1301
|
+
}
|
|
1302
|
+
return { reportId, taskId };
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
console.warn('[YaverFeedback] Crash report failed:', err);
|
|
1305
|
+
return {};
|
|
1306
|
+
} finally {
|
|
1307
|
+
crashReportInFlight = false;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
886
1311
|
/**
|
|
887
1312
|
* Returns the P2P client instance.
|
|
888
1313
|
* Available after init if agentUrl is set, or after first successful discovery.
|
|
@@ -1033,17 +1458,33 @@ export class YaverFeedback {
|
|
|
1033
1458
|
// Screenshot capture may fail (e.g. no view ref) — continue without it
|
|
1034
1459
|
}
|
|
1035
1460
|
|
|
1461
|
+
const identity = resolveReportIdentity({
|
|
1462
|
+
projectName: config?.projectName,
|
|
1463
|
+
bundleId: config?.bundleId,
|
|
1464
|
+
surface: config?.surface,
|
|
1465
|
+
surfaces: config?.surfaces,
|
|
1466
|
+
stack: config?.stack,
|
|
1467
|
+
stacks: config?.stacks,
|
|
1468
|
+
testSurfaces: config?.testSurfaces,
|
|
1469
|
+
feedbackSdk: config?.feedbackSdk,
|
|
1470
|
+
feedbackTransport: config?.feedbackTransport,
|
|
1471
|
+
voiceCapabilities: config?.voiceCapabilities,
|
|
1472
|
+
sttProvider: config?.sttProvider,
|
|
1473
|
+
ttsProvider: config?.ttsProvider,
|
|
1474
|
+
});
|
|
1036
1475
|
const bundle = {
|
|
1037
1476
|
metadata: {
|
|
1038
1477
|
timestamp: new Date().toISOString(),
|
|
1039
|
-
|
|
1478
|
+
deviceInfo: {
|
|
1040
1479
|
platform: Platform.OS,
|
|
1041
1480
|
osVersion: String(Platform.Version),
|
|
1042
1481
|
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
1043
1482
|
screenWidth: width,
|
|
1044
1483
|
screenHeight: height,
|
|
1484
|
+
appName: identity.appName,
|
|
1045
1485
|
},
|
|
1046
|
-
app:
|
|
1486
|
+
app: identity.app,
|
|
1487
|
+
project: identity.project,
|
|
1047
1488
|
userNote: '[Auto-report via shake]',
|
|
1048
1489
|
},
|
|
1049
1490
|
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
@@ -1211,6 +1652,17 @@ export class YaverFeedback {
|
|
|
1211
1652
|
shakeDetector.stop();
|
|
1212
1653
|
shakeDetector = null;
|
|
1213
1654
|
}
|
|
1655
|
+
// Drop the agent command handler and un-monkey-patch console. Without
|
|
1656
|
+
// these, destroy() left the SDK half-alive: console stayed wrapped, and
|
|
1657
|
+
// the reload handler kept firing on a "destroyed" SDK — then a later
|
|
1658
|
+
// init() stacked a second one on top.
|
|
1659
|
+
commandUnsubscribe?.();
|
|
1660
|
+
commandUnsubscribe = null;
|
|
1661
|
+
// Before `enabled = false` / `config = null` below, so an in-flight retry
|
|
1662
|
+
// can't fire against a torn-down config.
|
|
1663
|
+
YaverFeedback.cancelBlackBoxAutoStart();
|
|
1664
|
+
BlackBox.stop();
|
|
1665
|
+
BlackBox.unwrapConsole();
|
|
1214
1666
|
firstShakeFired = false;
|
|
1215
1667
|
enabled = false;
|
|
1216
1668
|
config = null;
|