yaver-feedback-react-native 0.9.0 → 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 +160 -6
- package/dist/P2PClient.d.ts +64 -1
- package/dist/P2PClient.js +222 -2
- package/dist/ShakeDetector.js +2 -0
- 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/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 +158 -1
- package/package.json +2 -2
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +176 -14
- package/src/P2PClient.ts +235 -2
- package/src/ShakeDetector.ts +1 -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/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +164 -1
package/dist/YaverFeedback.js
CHANGED
|
@@ -6,8 +6,15 @@ const Discovery_1 = require("./Discovery");
|
|
|
6
6
|
const BlackBox_1 = require("./BlackBox");
|
|
7
7
|
const P2PClient_1 = require("./P2PClient");
|
|
8
8
|
const ShakeDetector_1 = require("./ShakeDetector");
|
|
9
|
+
const storeShots_1 = require("./storeShots");
|
|
9
10
|
const auth_1 = require("./auth");
|
|
10
11
|
const preferences_1 = require("./preferences");
|
|
12
|
+
function unrefTimer(timer) {
|
|
13
|
+
const maybeNodeTimer = timer;
|
|
14
|
+
if (typeof maybeNodeTimer.unref === 'function') {
|
|
15
|
+
maybeNodeTimer.unref();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
11
18
|
// Is this JS runtime the Yaver mobile app's super-host bridge? The
|
|
12
19
|
// YaverInfo native module is only registered inside Yaver's container
|
|
13
20
|
// (mobile/ios/Yaver/YaverInfo.{swift,m} + Android counterpart); a
|
|
@@ -134,9 +141,16 @@ let config = null;
|
|
|
134
141
|
let enabled = false;
|
|
135
142
|
let p2pClient = null;
|
|
136
143
|
let shakeDetector = null;
|
|
144
|
+
/** Unsubscribe for the BlackBox command handler registered by init(). Held so
|
|
145
|
+
* a re-init (or destroy) can drop the old one instead of stacking. */
|
|
146
|
+
let commandUnsubscribe = null;
|
|
137
147
|
let p2pAuthToken = null;
|
|
138
148
|
let p2pRelayPassword = '';
|
|
149
|
+
/** Pending BlackBox auto-start retry. Held so destroy()/re-init can cancel it
|
|
150
|
+
* instead of leaving a timer chain running against a torn-down config. */
|
|
151
|
+
let autoStartTimer = null;
|
|
139
152
|
let reportLaunchInFlight = false;
|
|
153
|
+
let crashReportInFlight = false;
|
|
140
154
|
/** Resolve the user's relay password by validating their auth token
|
|
141
155
|
* against Convex. Used whenever we (re)build the P2PClient so a
|
|
142
156
|
* relay-routed agentUrl carries a valid X-Relay-Password — without
|
|
@@ -182,6 +196,7 @@ async function resolveRelayPassword(authToken, convexUrl) {
|
|
|
182
196
|
/** Ring buffer of captured errors. */
|
|
183
197
|
let errorBuffer = [];
|
|
184
198
|
let maxErrors = 5;
|
|
199
|
+
let crashHandlerInstalled = false;
|
|
185
200
|
/** Track whether BlackBox was running before disable (to restart on enable). */
|
|
186
201
|
let blackBoxWasStreaming = false;
|
|
187
202
|
/**
|
|
@@ -245,6 +260,118 @@ class YaverFeedback {
|
|
|
245
260
|
* If no `agentUrl` is provided, the SDK will attempt auto-discovery
|
|
246
261
|
* via `YaverDiscovery` on the first `startReport()` call.
|
|
247
262
|
*/
|
|
263
|
+
/** The runtime lane, when running on web inside a Yaver preview WebView. */
|
|
264
|
+
static detectWebLane() {
|
|
265
|
+
try {
|
|
266
|
+
if (react_native_1.Platform.OS !== 'web' || typeof window === 'undefined')
|
|
267
|
+
return null;
|
|
268
|
+
const l = String(window.__yaverLane || '').toLowerCase();
|
|
269
|
+
if (l === 'browser' || l === 'webrtc')
|
|
270
|
+
return l;
|
|
271
|
+
}
|
|
272
|
+
catch { /* noop */ }
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Mount a draggable DOM floating "Y" icon for the BROWSER lane (RN-web). It
|
|
277
|
+
* lives in the WebView's document.body (position:fixed, max z-index), so it
|
|
278
|
+
* renders ON TOP of the page inside the WebView and is never occluded by the
|
|
279
|
+
* fullScreen preview modal — the one occlusion-proof affordance on iOS.
|
|
280
|
+
* Tap opens the SDK's existing report flow (the RN FeedbackModal renders in
|
|
281
|
+
* the same WebView DOM, also un-occluded). No-op off-web or off-lane, and
|
|
282
|
+
* idempotent (a re-init reuses the existing node).
|
|
283
|
+
*/
|
|
284
|
+
static mountBrowserLaneIcon() {
|
|
285
|
+
if (YaverFeedback.detectWebLane() !== 'browser')
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
const doc = globalThis.document;
|
|
289
|
+
if (!doc || !doc.body)
|
|
290
|
+
return;
|
|
291
|
+
if (doc.getElementById('yaver-feedback-btn'))
|
|
292
|
+
return; // idempotent
|
|
293
|
+
const btn = doc.createElement('div');
|
|
294
|
+
btn.id = 'yaver-feedback-btn';
|
|
295
|
+
btn.textContent = 'Y';
|
|
296
|
+
btn.title = 'Yaver Feedback — drag to move · tap to report';
|
|
297
|
+
btn.style.cssText =
|
|
298
|
+
'position:fixed;bottom:20px;right:20px;width:44px;height:44px;border-radius:50%;' +
|
|
299
|
+
'background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;' +
|
|
300
|
+
'cursor:pointer;z-index:99999;font-size:18px;font-weight:bold;touch-action:none;' +
|
|
301
|
+
'box-shadow:0 4px 12px rgba(99,102,241,0.4);transition:transform 0.2s;';
|
|
302
|
+
const drag = { on: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
|
|
303
|
+
const w = window;
|
|
304
|
+
const applyPos = (x, y) => {
|
|
305
|
+
const m = 8;
|
|
306
|
+
const nx = Math.max(m, Math.min(w.innerWidth - 44 - m, x));
|
|
307
|
+
const ny = Math.max(m, Math.min(w.innerHeight - 44 - m, y));
|
|
308
|
+
btn.style.left = nx + 'px';
|
|
309
|
+
btn.style.top = ny + 'px';
|
|
310
|
+
btn.style.right = 'auto';
|
|
311
|
+
btn.style.bottom = 'auto';
|
|
312
|
+
try {
|
|
313
|
+
localStorage.setItem('yaver-feedback-btn-pos', JSON.stringify({ x: nx, y: ny }));
|
|
314
|
+
}
|
|
315
|
+
catch { /* noop */ }
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
const s = localStorage.getItem('yaver-feedback-btn-pos');
|
|
319
|
+
if (s) {
|
|
320
|
+
const p = JSON.parse(s);
|
|
321
|
+
if (typeof p?.x === 'number')
|
|
322
|
+
applyPos(p.x, p.y);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch { /* noop */ }
|
|
326
|
+
btn.addEventListener('pointerdown', (e) => {
|
|
327
|
+
drag.on = true;
|
|
328
|
+
drag.moved = false;
|
|
329
|
+
const r = btn.getBoundingClientRect();
|
|
330
|
+
drag.sx = e.clientX;
|
|
331
|
+
drag.sy = e.clientY;
|
|
332
|
+
drag.ox = r.left;
|
|
333
|
+
drag.oy = r.top;
|
|
334
|
+
btn.setPointerCapture(e.pointerId);
|
|
335
|
+
btn.style.transition = 'none';
|
|
336
|
+
});
|
|
337
|
+
btn.addEventListener('pointermove', (e) => {
|
|
338
|
+
if (!drag.on)
|
|
339
|
+
return;
|
|
340
|
+
const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
|
|
341
|
+
if (Math.abs(dx) + Math.abs(dy) > 5)
|
|
342
|
+
drag.moved = true;
|
|
343
|
+
if (drag.moved)
|
|
344
|
+
applyPos(drag.ox + dx, drag.oy + dy);
|
|
345
|
+
});
|
|
346
|
+
const end = (e) => {
|
|
347
|
+
if (!drag.on)
|
|
348
|
+
return;
|
|
349
|
+
drag.on = false;
|
|
350
|
+
btn.style.transition = 'transform 0.2s';
|
|
351
|
+
try {
|
|
352
|
+
btn.releasePointerCapture(e.pointerId);
|
|
353
|
+
}
|
|
354
|
+
catch { /* noop */ }
|
|
355
|
+
if (!drag.moved) {
|
|
356
|
+
try {
|
|
357
|
+
void YaverFeedback.startReport();
|
|
358
|
+
}
|
|
359
|
+
catch { /* noop */ }
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
btn.addEventListener('pointerup', end);
|
|
363
|
+
btn.addEventListener('pointercancel', end);
|
|
364
|
+
// Also answer the host's forwarded shake (Yaver injects yaver-feedback:launch).
|
|
365
|
+
const launch = () => { try {
|
|
366
|
+
void YaverFeedback.startReport();
|
|
367
|
+
}
|
|
368
|
+
catch { /* noop */ } };
|
|
369
|
+
window.addEventListener('yaver-feedback:launch', launch);
|
|
370
|
+
window.__yaverFeedbackLaunch = launch;
|
|
371
|
+
doc.body.appendChild(btn);
|
|
372
|
+
}
|
|
373
|
+
catch { /* noop — never break the guest app over a feedback affordance */ }
|
|
374
|
+
}
|
|
248
375
|
static init(cfg) {
|
|
249
376
|
config = {
|
|
250
377
|
trigger: 'shake',
|
|
@@ -272,6 +399,9 @@ class YaverFeedback {
|
|
|
272
399
|
}
|
|
273
400
|
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
274
401
|
errorBuffer = [];
|
|
402
|
+
if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
|
|
403
|
+
YaverFeedback.installCrashHandler();
|
|
404
|
+
}
|
|
275
405
|
return;
|
|
276
406
|
}
|
|
277
407
|
if (config.disableShakeGesture && (!config.quickIcon || config.quickIcon === 'auto')) {
|
|
@@ -335,23 +465,23 @@ class YaverFeedback {
|
|
|
335
465
|
// Set up error capture buffer size
|
|
336
466
|
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
337
467
|
errorBuffer = [];
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
shakeDetector.stop();
|
|
341
|
-
shakeDetector = null;
|
|
342
|
-
}
|
|
343
|
-
if (enabled && config.trigger === 'shake' && !config.disableShakeGesture) {
|
|
344
|
-
shakeDetector = new ShakeDetector_1.ShakeDetector();
|
|
345
|
-
shakeDetector.start(() => {
|
|
346
|
-
YaverFeedback.notifyShake();
|
|
347
|
-
if (config?.reportingOnly) {
|
|
348
|
-
YaverFeedback.sendAutoReport();
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
YaverFeedback.startReport();
|
|
352
|
-
}
|
|
353
|
-
});
|
|
468
|
+
if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
|
|
469
|
+
YaverFeedback.installCrashHandler();
|
|
354
470
|
}
|
|
471
|
+
// Wire up shake detection when trigger is 'shake'. Rebuild from scratch:
|
|
472
|
+
// config was just replaced, so the old detector may be closing over stale
|
|
473
|
+
// settings.
|
|
474
|
+
YaverFeedback.stopShakeDetector();
|
|
475
|
+
YaverFeedback.syncShakeDetector();
|
|
476
|
+
// Browser lane (RN-web inside Yaver's fullScreen WebView preview): the
|
|
477
|
+
// native shake module doesn't exist on web, and Yaver's own container
|
|
478
|
+
// overlay is OCCLUDED behind the WebView modal on iOS. So mount an
|
|
479
|
+
// occlusion-proof DRAGGABLE DOM icon INSIDE the WebView — same pattern as
|
|
480
|
+
// yaver-feedback-web. Lane is stamped by Yaver via
|
|
481
|
+
// window.__yaverLane='browser' (mobile PREVIEW_LANE_SCRIPT). See
|
|
482
|
+
// docs/audits/feedback-sdk-lanes-audit-2026-07-28.md.
|
|
483
|
+
if (enabled)
|
|
484
|
+
YaverFeedback.mountBrowserLaneIcon();
|
|
355
485
|
// Wire up BlackBox command handlers for reload + status signals from
|
|
356
486
|
// the agent. Opens an SSE channel the agent uses to:
|
|
357
487
|
// - broadcast reload_bundle so the phone picks up a fresh Hermes
|
|
@@ -360,7 +490,14 @@ class YaverFeedback {
|
|
|
360
490
|
// assets…", "Done") so the SDK can surface it like a normal
|
|
361
491
|
// "Working…" spinner instead of a silent 60-second freeze.
|
|
362
492
|
if (enabled) {
|
|
363
|
-
|
|
493
|
+
// onCommand appends to a static array and hands back an unsubscribe.
|
|
494
|
+
// Dropping that unsubscribe meant every re-init stacked another handler
|
|
495
|
+
// on top of the last, so an app that re-initialised (say, a settings
|
|
496
|
+
// toggle) reloaded twice per agent command, then three times, and so on.
|
|
497
|
+
// destroy() never cleared them either. Drop the previous registration
|
|
498
|
+
// first so init() is idempotent here.
|
|
499
|
+
commandUnsubscribe?.();
|
|
500
|
+
commandUnsubscribe = BlackBox_1.BlackBox.onCommand((cmd) => {
|
|
364
501
|
if (cmd.command === 'reload') {
|
|
365
502
|
if (cfg.onReload) {
|
|
366
503
|
cfg.onReload();
|
|
@@ -423,17 +560,24 @@ class YaverFeedback {
|
|
|
423
560
|
// SFMG used to call BlackBox.start() inside YaverFeedbackWidget
|
|
424
561
|
// after auth — that path still works (start() is idempotent), so
|
|
425
562
|
// upgrading SDK without removing the manual call is safe.
|
|
563
|
+
// 4. RETRY, rather than checking once. This was a single 500ms
|
|
564
|
+
// timeout, and on a cold start it could not succeed: an app that
|
|
565
|
+
// passes neither agentUrl nor authToken to init() (the normal
|
|
566
|
+
// case — they're restored from storage) gets its session from
|
|
567
|
+
// `void hydrateSession()`, which reads two AsyncStorage keys and
|
|
568
|
+
// then awaits discoverAgent(), a Convex round trip plus LAN
|
|
569
|
+
// probing. That does not finish in 500ms, so the check found no
|
|
570
|
+
// agentUrl, gave up, and BlackBox never started — meaning no SSE
|
|
571
|
+
// channel, so the agent's `reload_bundle` command had nowhere to
|
|
572
|
+
// land and Hermes hot reload silently did nothing until the user
|
|
573
|
+
// shook the device and drove discovery by hand.
|
|
574
|
+
//
|
|
575
|
+
// The both-conditions guard is what keeps the 401 retry storm
|
|
576
|
+
// from coming back, so it is preserved exactly; we only re-check
|
|
577
|
+
// it over time instead of once. Bounded so a device that never
|
|
578
|
+
// signs in doesn't poll forever.
|
|
426
579
|
if (cfg.autoStartBlackBox !== false) {
|
|
427
|
-
|
|
428
|
-
if (config?.agentUrl && (config?.authToken || p2pAuthToken)) {
|
|
429
|
-
try {
|
|
430
|
-
BlackBox_1.BlackBox.start();
|
|
431
|
-
}
|
|
432
|
-
catch (err) {
|
|
433
|
-
console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}, 500);
|
|
580
|
+
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
437
581
|
}
|
|
438
582
|
}
|
|
439
583
|
// NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
|
|
@@ -446,6 +590,41 @@ class YaverFeedback {
|
|
|
446
590
|
// - YaverFeedback.wrapErrorHandler(existingHandler) to create a
|
|
447
591
|
// pass-through wrapper they insert into their own error chain
|
|
448
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Opt-in global crash handler. This wraps the existing React Native
|
|
595
|
+
* ErrorUtils handler and still calls it, so Sentry/Crashlytics/Bugsnag can
|
|
596
|
+
* remain the system of record. The SDK only uploads a crash report and,
|
|
597
|
+
* when configured, asks the agent to create a fix task.
|
|
598
|
+
*/
|
|
599
|
+
static installCrashHandler() {
|
|
600
|
+
if (crashHandlerInstalled)
|
|
601
|
+
return;
|
|
602
|
+
try {
|
|
603
|
+
const errorUtils = globalThis?.ErrorUtils;
|
|
604
|
+
if (!errorUtils?.getGlobalHandler || !errorUtils?.setGlobalHandler) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const next = errorUtils.getGlobalHandler();
|
|
608
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
609
|
+
YaverFeedback.attachError(error, {
|
|
610
|
+
source: 'global-handler',
|
|
611
|
+
crashAware: true,
|
|
612
|
+
});
|
|
613
|
+
if (errorBuffer.length > 0) {
|
|
614
|
+
errorBuffer[errorBuffer.length - 1].isFatal = isFatal ?? true;
|
|
615
|
+
}
|
|
616
|
+
void YaverFeedback.reportCrash(error, {
|
|
617
|
+
isFatal: isFatal ?? true,
|
|
618
|
+
source: 'global-handler',
|
|
619
|
+
});
|
|
620
|
+
next?.(error, isFatal);
|
|
621
|
+
});
|
|
622
|
+
crashHandlerInstalled = true;
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
console.warn('[YaverFeedback] installCrashHandler failed:', err);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
449
628
|
/**
|
|
450
629
|
* Run agent discovery in the background.
|
|
451
630
|
* Called automatically from init() when no agentUrl is provided.
|
|
@@ -547,11 +726,64 @@ class YaverFeedback {
|
|
|
547
726
|
else {
|
|
548
727
|
await YaverFeedback.discoverAgent();
|
|
549
728
|
}
|
|
729
|
+
// Signing in is the event the auto-start was waiting for. If its bounded
|
|
730
|
+
// retry already gave up (device left signed out past the window), this is
|
|
731
|
+
// what brings the command channel up without requiring an app restart.
|
|
732
|
+
if (config.autoStartBlackBox !== false && !BlackBox_1.BlackBox.isStreaming) {
|
|
733
|
+
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
734
|
+
}
|
|
550
735
|
}
|
|
551
736
|
/** Returns true once the SDK has a session token it can use. */
|
|
552
737
|
static isAuthed() {
|
|
553
738
|
return Boolean(config?.authToken);
|
|
554
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* On-device App Store screenshot capture (Engine 2). Walks the routes
|
|
742
|
+
* the host hands us, screenshots each, and uploads to the agent which
|
|
743
|
+
* runs the App Store Connect backend. agentUrl / authToken / relay
|
|
744
|
+
* password default to the SDK's resolved session; the host only has to
|
|
745
|
+
* supply a navigation ref + the ordered route list.
|
|
746
|
+
*
|
|
747
|
+
* YaverFeedback.captureStoreScreenshots({
|
|
748
|
+
* app: 'sfmg',
|
|
749
|
+
* navigationRef,
|
|
750
|
+
* routes: ['/(tabs)/dashboard', '/(tabs)/messages', '/(tabs)/clients'],
|
|
751
|
+
* submit: true,
|
|
752
|
+
* })
|
|
753
|
+
*/
|
|
754
|
+
static async captureStoreScreenshots(opts) {
|
|
755
|
+
const agentUrl = opts.agentUrl ?? config?.agentUrl ?? '';
|
|
756
|
+
const authToken = opts.authToken ?? config?.authToken ?? p2pAuthToken ?? '';
|
|
757
|
+
const relayPassword = opts.relayPassword ?? p2pRelayPassword;
|
|
758
|
+
if (!agentUrl || !authToken) {
|
|
759
|
+
return {
|
|
760
|
+
ok: false,
|
|
761
|
+
captured: 0,
|
|
762
|
+
uploaded: 0,
|
|
763
|
+
message: 'YaverFeedback not connected to an agent — init() with agentUrl + authToken first.',
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return (0, storeShots_1.captureStoreScreenshots)({ ...opts, agentUrl, authToken, relayPassword });
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Let the mobile app / CLI kick THIS device into self-capturing. Registers
|
|
770
|
+
* a BlackBox command listener that fires captureStoreScreenshots when the
|
|
771
|
+
* agent pushes a `capture_store_shots` command (its `data` may override
|
|
772
|
+
* `submit` / `locale`). Returns an unsubscribe fn. Call once after init,
|
|
773
|
+
* passing the app's navigation ref + the routes to walk.
|
|
774
|
+
*/
|
|
775
|
+
static enableStoreShotsOnCommand(opts) {
|
|
776
|
+
return BlackBox_1.BlackBox.onCommand((cmd) => {
|
|
777
|
+
if (cmd?.command !== 'capture_store_shots')
|
|
778
|
+
return;
|
|
779
|
+
const data = cmd.data ?? {};
|
|
780
|
+
void YaverFeedback.captureStoreScreenshots({
|
|
781
|
+
...opts,
|
|
782
|
+
submit: typeof data.submit === 'boolean' ? data.submit : opts.submit,
|
|
783
|
+
locale: typeof data.locale === 'string' ? data.locale : opts.locale,
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
555
787
|
/**
|
|
556
788
|
* Request the embedded FeedbackModal to show the login screen. Works by
|
|
557
789
|
* emitting an event the modal listens for — avoids forcing the host app
|
|
@@ -747,32 +979,138 @@ class YaverFeedback {
|
|
|
747
979
|
}
|
|
748
980
|
BlackBox_1.BlackBox.unwrapConsole(); // ensure console is restored even if BlackBox wasn't started
|
|
749
981
|
errorBuffer = [];
|
|
750
|
-
|
|
751
|
-
shakeDetector.stop();
|
|
752
|
-
shakeDetector = null;
|
|
753
|
-
}
|
|
982
|
+
YaverFeedback.stopShakeDetector();
|
|
754
983
|
}
|
|
755
984
|
else {
|
|
756
985
|
// === ENABLE ===
|
|
757
986
|
if (blackBoxWasStreaming) {
|
|
758
987
|
BlackBox_1.BlackBox.start(); // restart with previous config
|
|
759
988
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
shakeDetector = new ShakeDetector_1.ShakeDetector();
|
|
763
|
-
shakeDetector.start(() => {
|
|
764
|
-
YaverFeedback.notifyShake();
|
|
765
|
-
if (config?.reportingOnly) {
|
|
766
|
-
YaverFeedback.sendAutoReport();
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
YaverFeedback.startReport();
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
}
|
|
989
|
+
enabled = true; // syncShakeDetector reads this
|
|
990
|
+
YaverFeedback.syncShakeDetector();
|
|
773
991
|
}
|
|
774
992
|
enabled = value;
|
|
775
993
|
}
|
|
994
|
+
/**
|
|
995
|
+
* Turn shake-to-report on or off without tearing the SDK down.
|
|
996
|
+
*
|
|
997
|
+
* `trigger` is read only inside init(), so before this the only ways to
|
|
998
|
+
* stop listening for a shake were setEnabled(false) — which also kills the
|
|
999
|
+
* flight recorder and the agent command channel — or a re-init, which used
|
|
1000
|
+
* to stack a duplicate command handler. Neither is what "turn the shake
|
|
1001
|
+
* catcher off" should cost.
|
|
1002
|
+
*
|
|
1003
|
+
* Persists onto config.disableShakeGesture, so a later setEnabled(true)
|
|
1004
|
+
* honours it rather than resurrecting the listener.
|
|
1005
|
+
*/
|
|
1006
|
+
static setShakeEnabled(value) {
|
|
1007
|
+
if (!config)
|
|
1008
|
+
return;
|
|
1009
|
+
config.disableShakeGesture = !value;
|
|
1010
|
+
YaverFeedback.syncShakeDetector();
|
|
1011
|
+
}
|
|
1012
|
+
/** Whether the shake listener is currently armed. */
|
|
1013
|
+
static isShakeEnabled() {
|
|
1014
|
+
return shakeDetector !== null;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Bring the shake listener in line with the current config. Idempotent —
|
|
1018
|
+
* safe to call whenever `enabled`, `trigger`, or `disableShakeGesture`
|
|
1019
|
+
* moves.
|
|
1020
|
+
*/
|
|
1021
|
+
static syncShakeDetector() {
|
|
1022
|
+
const want = enabled && config?.trigger === 'shake' && !config?.disableShakeGesture;
|
|
1023
|
+
if (want && !shakeDetector) {
|
|
1024
|
+
shakeDetector = new ShakeDetector_1.ShakeDetector();
|
|
1025
|
+
shakeDetector.start(() => {
|
|
1026
|
+
YaverFeedback.notifyShake();
|
|
1027
|
+
if (config?.reportingOnly) {
|
|
1028
|
+
YaverFeedback.sendAutoReport();
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
YaverFeedback.startReport();
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
else if (!want && shakeDetector) {
|
|
1036
|
+
YaverFeedback.stopShakeDetector();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
static stopShakeDetector() {
|
|
1040
|
+
if (shakeDetector) {
|
|
1041
|
+
shakeDetector.stop();
|
|
1042
|
+
shakeDetector = null;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/** Cancel a pending BlackBox auto-start retry chain. */
|
|
1046
|
+
static cancelBlackBoxAutoStart() {
|
|
1047
|
+
if (autoStartTimer) {
|
|
1048
|
+
clearTimeout(autoStartTimer);
|
|
1049
|
+
autoStartTimer = null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Start BlackBox as soon as we have BOTH an agentUrl and a token, polling
|
|
1054
|
+
* until then.
|
|
1055
|
+
*
|
|
1056
|
+
* Both conditions are mandatory and deliberate: starting without a token
|
|
1057
|
+
* makes the SSE channel 401 and retry with backoff, which is the tight
|
|
1058
|
+
* string-concat + JSON-parse loop that used to SIGSEGV Hermes on iOS 18.3.1
|
|
1059
|
+
* during Screenshot & Fix. This only re-checks that same guard over time.
|
|
1060
|
+
*
|
|
1061
|
+
* Bounded at ~60s: long enough for AsyncStorage hydration plus a Convex
|
|
1062
|
+
* discovery round trip on a cold, off-LAN start, short enough that a device
|
|
1063
|
+
* whose user never signs in stops polling. Giving up here costs nothing —
|
|
1064
|
+
* setAuthToken() and startReport() both reschedule, so signing in later
|
|
1065
|
+
* still brings the channel up.
|
|
1066
|
+
*/
|
|
1067
|
+
static scheduleBlackBoxAutoStart(attempt = 0) {
|
|
1068
|
+
const MAX_ATTEMPTS = 60;
|
|
1069
|
+
const FIRST_DELAY_MS = 500;
|
|
1070
|
+
const RETRY_DELAY_MS = 1000;
|
|
1071
|
+
// Re-run discovery every Nth tick rather than every tick: it is a Convex
|
|
1072
|
+
// round trip plus LAN probing, so once a second would hammer both. Every
|
|
1073
|
+
// 3s is responsive enough that waking the dev machine reconnects the phone
|
|
1074
|
+
// on its own, without the user wondering whether to restart the app.
|
|
1075
|
+
const REDISCOVER_EVERY = 3;
|
|
1076
|
+
YaverFeedback.cancelBlackBoxAutoStart();
|
|
1077
|
+
if (!enabled || attempt >= MAX_ATTEMPTS)
|
|
1078
|
+
return;
|
|
1079
|
+
const timer = setTimeout(() => {
|
|
1080
|
+
autoStartTimer = null;
|
|
1081
|
+
// A destroy() or setEnabled(false) between scheduling and firing.
|
|
1082
|
+
if (!enabled || !config)
|
|
1083
|
+
return;
|
|
1084
|
+
if (BlackBox_1.BlackBox.isStreaming)
|
|
1085
|
+
return;
|
|
1086
|
+
if (config.agentUrl && (config.authToken || p2pAuthToken)) {
|
|
1087
|
+
try {
|
|
1088
|
+
// Pass the host's BlackBox config through. This used to call
|
|
1089
|
+
// start() bare, so a host that configured the flight recorder
|
|
1090
|
+
// — as the README itself shows — had it silently replaced by
|
|
1091
|
+
// defaults, and appName came out ''.
|
|
1092
|
+
BlackBox_1.BlackBox.start(config.blackBox);
|
|
1093
|
+
}
|
|
1094
|
+
catch (err) {
|
|
1095
|
+
console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
// No agent yet. Re-attempt discovery instead of only re-reading the
|
|
1100
|
+
// flag: hydrateSession() runs discoverAgent() exactly once at init, so
|
|
1101
|
+
// if the dev machine was unreachable at that moment — asleep, phone
|
|
1102
|
+
// still on cellular before the relay came up, agent not yet serving —
|
|
1103
|
+
// agentUrl stayed null for the whole process lifetime and only an app
|
|
1104
|
+
// restart could recover it. discoverAgent() self-guards on "already
|
|
1105
|
+
// have a URL" and "no token", so calling it again is cheap and safe.
|
|
1106
|
+
if (!config.agentUrl && attempt > 0 && attempt % REDISCOVER_EVERY === 0) {
|
|
1107
|
+
void YaverFeedback.discoverAgent();
|
|
1108
|
+
}
|
|
1109
|
+
YaverFeedback.scheduleBlackBoxAutoStart(attempt + 1);
|
|
1110
|
+
}, attempt === 0 ? FIRST_DELAY_MS : RETRY_DELAY_MS);
|
|
1111
|
+
autoStartTimer = timer;
|
|
1112
|
+
unrefTimer(timer);
|
|
1113
|
+
}
|
|
776
1114
|
/** Returns whether the SDK is currently enabled. */
|
|
777
1115
|
static isEnabled() {
|
|
778
1116
|
return enabled;
|
|
@@ -843,6 +1181,104 @@ class YaverFeedback {
|
|
|
843
1181
|
next?.(error, isFatal);
|
|
844
1182
|
};
|
|
845
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Upload a crash-aware feedback bundle. Apps can call this from their own
|
|
1186
|
+
* error boundary/global handler. If crashReporting.autoFix is true, the SDK
|
|
1187
|
+
* also triggers the agent's feedback-fix task; reload delivery still happens
|
|
1188
|
+
* through the existing BlackBox command stream.
|
|
1189
|
+
*/
|
|
1190
|
+
static async reportCrash(error, opts) {
|
|
1191
|
+
if (!config || !enabled || !config.crashReporting?.enabled)
|
|
1192
|
+
return {};
|
|
1193
|
+
if (crashReportInFlight)
|
|
1194
|
+
return {};
|
|
1195
|
+
crashReportInFlight = true;
|
|
1196
|
+
try {
|
|
1197
|
+
YaverFeedback.attachError(error, {
|
|
1198
|
+
...(config.crashReporting.metadata || {}),
|
|
1199
|
+
...(opts?.metadata || {}),
|
|
1200
|
+
crashAware: true,
|
|
1201
|
+
});
|
|
1202
|
+
if (errorBuffer.length > 0) {
|
|
1203
|
+
errorBuffer[errorBuffer.length - 1].isFatal = opts?.isFatal ?? true;
|
|
1204
|
+
}
|
|
1205
|
+
if (!config.agentUrl) {
|
|
1206
|
+
await YaverFeedback.discoverAgent();
|
|
1207
|
+
}
|
|
1208
|
+
if (!config.agentUrl) {
|
|
1209
|
+
console.warn('[YaverFeedback] No agent URL — cannot send crash report.');
|
|
1210
|
+
return {};
|
|
1211
|
+
}
|
|
1212
|
+
const { Platform, Dimensions } = require('react-native');
|
|
1213
|
+
const { uploadFeedback } = require('./upload');
|
|
1214
|
+
const { width, height } = Dimensions.get('window');
|
|
1215
|
+
let screenshotPath;
|
|
1216
|
+
if (config.crashReporting.captureScreenshot !== false) {
|
|
1217
|
+
try {
|
|
1218
|
+
const { captureScreenshot } = require('./capture');
|
|
1219
|
+
screenshotPath = await captureScreenshot();
|
|
1220
|
+
}
|
|
1221
|
+
catch { }
|
|
1222
|
+
}
|
|
1223
|
+
const autoFix = opts?.autoFix ?? config.crashReporting.autoFix === true;
|
|
1224
|
+
const identity = (0, P2PClient_1.resolveReportIdentity)({
|
|
1225
|
+
projectName: config?.projectName,
|
|
1226
|
+
bundleId: config?.bundleId,
|
|
1227
|
+
surface: config?.surface,
|
|
1228
|
+
surfaces: config?.surfaces,
|
|
1229
|
+
stack: config?.stack,
|
|
1230
|
+
stacks: config?.stacks,
|
|
1231
|
+
testSurfaces: config?.testSurfaces,
|
|
1232
|
+
feedbackSdk: config?.feedbackSdk,
|
|
1233
|
+
feedbackTransport: config?.feedbackTransport,
|
|
1234
|
+
voiceCapabilities: config?.voiceCapabilities,
|
|
1235
|
+
sttProvider: config?.sttProvider,
|
|
1236
|
+
ttsProvider: config?.ttsProvider,
|
|
1237
|
+
});
|
|
1238
|
+
const bundle = {
|
|
1239
|
+
metadata: {
|
|
1240
|
+
timestamp: new Date().toISOString(),
|
|
1241
|
+
reportKind: 'crash',
|
|
1242
|
+
deviceInfo: {
|
|
1243
|
+
platform: Platform.OS,
|
|
1244
|
+
osVersion: String(Platform.Version),
|
|
1245
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
1246
|
+
screenWidth: width,
|
|
1247
|
+
screenHeight: height,
|
|
1248
|
+
appName: identity.appName,
|
|
1249
|
+
},
|
|
1250
|
+
app: identity.app,
|
|
1251
|
+
project: identity.project,
|
|
1252
|
+
userNote: '[Crash report]',
|
|
1253
|
+
crash: {
|
|
1254
|
+
message: error.message,
|
|
1255
|
+
isFatal: opts?.isFatal ?? true,
|
|
1256
|
+
source: opts?.source ?? 'manual',
|
|
1257
|
+
autoFixRequested: autoFix,
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
1261
|
+
errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
|
|
1262
|
+
};
|
|
1263
|
+
const uploaded = await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle, p2pRelayPassword);
|
|
1264
|
+
const reportId = uploaded?.id ??
|
|
1265
|
+
uploaded?.reportId;
|
|
1266
|
+
let taskId;
|
|
1267
|
+
if (autoFix && reportId) {
|
|
1268
|
+
const client = p2pClient ?? new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
|
|
1269
|
+
const fix = await client.triggerFix(reportId);
|
|
1270
|
+
taskId = fix?.taskId;
|
|
1271
|
+
}
|
|
1272
|
+
return { reportId, taskId };
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
console.warn('[YaverFeedback] Crash report failed:', err);
|
|
1276
|
+
return {};
|
|
1277
|
+
}
|
|
1278
|
+
finally {
|
|
1279
|
+
crashReportInFlight = false;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
846
1282
|
/**
|
|
847
1283
|
* Returns the P2P client instance.
|
|
848
1284
|
* Available after init if agentUrl is set, or after first successful discovery.
|
|
@@ -981,17 +1417,33 @@ class YaverFeedback {
|
|
|
981
1417
|
catch {
|
|
982
1418
|
// Screenshot capture may fail (e.g. no view ref) — continue without it
|
|
983
1419
|
}
|
|
1420
|
+
const identity = (0, P2PClient_1.resolveReportIdentity)({
|
|
1421
|
+
projectName: config?.projectName,
|
|
1422
|
+
bundleId: config?.bundleId,
|
|
1423
|
+
surface: config?.surface,
|
|
1424
|
+
surfaces: config?.surfaces,
|
|
1425
|
+
stack: config?.stack,
|
|
1426
|
+
stacks: config?.stacks,
|
|
1427
|
+
testSurfaces: config?.testSurfaces,
|
|
1428
|
+
feedbackSdk: config?.feedbackSdk,
|
|
1429
|
+
feedbackTransport: config?.feedbackTransport,
|
|
1430
|
+
voiceCapabilities: config?.voiceCapabilities,
|
|
1431
|
+
sttProvider: config?.sttProvider,
|
|
1432
|
+
ttsProvider: config?.ttsProvider,
|
|
1433
|
+
});
|
|
984
1434
|
const bundle = {
|
|
985
1435
|
metadata: {
|
|
986
1436
|
timestamp: new Date().toISOString(),
|
|
987
|
-
|
|
1437
|
+
deviceInfo: {
|
|
988
1438
|
platform: Platform.OS,
|
|
989
1439
|
osVersion: String(Platform.Version),
|
|
990
1440
|
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
991
1441
|
screenWidth: width,
|
|
992
1442
|
screenHeight: height,
|
|
1443
|
+
appName: identity.appName,
|
|
993
1444
|
},
|
|
994
|
-
app:
|
|
1445
|
+
app: identity.app,
|
|
1446
|
+
project: identity.project,
|
|
995
1447
|
userNote: '[Auto-report via shake]',
|
|
996
1448
|
},
|
|
997
1449
|
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
@@ -1143,6 +1595,17 @@ class YaverFeedback {
|
|
|
1143
1595
|
shakeDetector.stop();
|
|
1144
1596
|
shakeDetector = null;
|
|
1145
1597
|
}
|
|
1598
|
+
// Drop the agent command handler and un-monkey-patch console. Without
|
|
1599
|
+
// these, destroy() left the SDK half-alive: console stayed wrapped, and
|
|
1600
|
+
// the reload handler kept firing on a "destroyed" SDK — then a later
|
|
1601
|
+
// init() stacked a second one on top.
|
|
1602
|
+
commandUnsubscribe?.();
|
|
1603
|
+
commandUnsubscribe = null;
|
|
1604
|
+
// Before `enabled = false` / `config = null` below, so an in-flight retry
|
|
1605
|
+
// can't fire against a torn-down config.
|
|
1606
|
+
YaverFeedback.cancelBlackBoxAutoStart();
|
|
1607
|
+
BlackBox_1.BlackBox.stop();
|
|
1608
|
+
BlackBox_1.BlackBox.unwrapConsole();
|
|
1146
1609
|
firstShakeFired = false;
|
|
1147
1610
|
enabled = false;
|
|
1148
1611
|
config = null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|