yaver-feedback-react-native 0.9.0 → 0.9.3
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/README.md +102 -1
- package/app.plugin.js +126 -9
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -8
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +505 -65
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +178 -2
- package/dist/P2PClient.js +493 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/ShakeDetector.js +2 -0
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +132 -0
- package/dist/YaverFeedback.js +633 -65
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- 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__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
- package/dist/__tests__/ReportIdentity.test.js +180 -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__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -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/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +1 -5
- package/dist/_core/device.js +5 -14
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +33 -7
- 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 +208 -8
- package/package.json +13 -4
- package/src/AuthOverlay.tsx +20 -106
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -8
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +619 -79
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +580 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +1 -0
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +631 -68
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- 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__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +212 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -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/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +5 -16
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +61 -11
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +209 -8
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
package/src/YaverFeedback.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
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,
|
|
10
15
|
getToken,
|
|
11
16
|
getSelectedDeviceId,
|
|
12
17
|
listReachableDevices,
|
|
13
|
-
mintGuestSdkToken,
|
|
14
18
|
clearToken,
|
|
15
19
|
clearSelectedDeviceId,
|
|
16
20
|
DEFAULT_CONVEX_SITE_URL,
|
|
@@ -22,6 +26,22 @@ import {
|
|
|
22
26
|
setQuickIconColorPreset,
|
|
23
27
|
QuickIconColorPreset,
|
|
24
28
|
} from './preferences';
|
|
29
|
+
import { resolveSDKDogfood, type SDKDogfoodStatus } from './dogfoodPolicy';
|
|
30
|
+
import { YaverDeviceDogfood, type DeviceDogfoodOptions, type DeviceDogfoodSession, type DeviceDogfoodState } from './deviceDogfood';
|
|
31
|
+
|
|
32
|
+
export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
|
|
33
|
+
/** Hint used to preselect the matching project returned by the owner machine. */
|
|
34
|
+
projectName?: string;
|
|
35
|
+
/** Framework fallback when the machine has not classified the project yet. */
|
|
36
|
+
framework?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
40
|
+
const maybeNodeTimer = timer as unknown as { unref?: () => void };
|
|
41
|
+
if (typeof maybeNodeTimer.unref === 'function') {
|
|
42
|
+
maybeNodeTimer.unref();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
25
45
|
|
|
26
46
|
// Is this JS runtime the Yaver mobile app's super-host bridge? The
|
|
27
47
|
// YaverInfo native module is only registered inside Yaver's container
|
|
@@ -146,10 +166,19 @@ if (IS_HOST_MODE) {
|
|
|
146
166
|
let config: FeedbackConfig | null = null;
|
|
147
167
|
let enabled = false;
|
|
148
168
|
let p2pClient: P2PClient | null = null;
|
|
169
|
+
let renderP2PClient: P2PClient | null = null;
|
|
149
170
|
let shakeDetector: ShakeDetector | null = null;
|
|
171
|
+
/** Unsubscribe for the BlackBox command handler registered by init(). Held so
|
|
172
|
+
* a re-init (or destroy) can drop the old one instead of stacking. */
|
|
173
|
+
let commandUnsubscribe: (() => void) | null = null;
|
|
150
174
|
let p2pAuthToken: string | null = null;
|
|
151
175
|
let p2pRelayPassword: string = '';
|
|
176
|
+
/** Pending BlackBox auto-start retry. Held so destroy()/re-init can cancel it
|
|
177
|
+
* instead of leaving a timer chain running against a torn-down config. */
|
|
178
|
+
let autoStartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
152
179
|
let reportLaunchInFlight = false;
|
|
180
|
+
let crashReportInFlight = false;
|
|
181
|
+
let dogfoodOnboarding: DogfoodOnboardingOptions | null = null;
|
|
153
182
|
|
|
154
183
|
/** Resolve the user's relay password by validating their auth token
|
|
155
184
|
* against Convex. Used whenever we (re)build the P2PClient so a
|
|
@@ -196,6 +225,7 @@ async function resolveRelayPassword(authToken: string, convexUrl?: string): Prom
|
|
|
196
225
|
/** Ring buffer of captured errors. */
|
|
197
226
|
let errorBuffer: CapturedError[] = [];
|
|
198
227
|
let maxErrors = 5;
|
|
228
|
+
let crashHandlerInstalled = false;
|
|
199
229
|
|
|
200
230
|
/** Track whether BlackBox was running before disable (to restart on enable). */
|
|
201
231
|
let blackBoxWasStreaming = false;
|
|
@@ -221,42 +251,35 @@ const flagCache: Map<string, { value: unknown; at: number }> = new Map();
|
|
|
221
251
|
*/
|
|
222
252
|
export class YaverFeedback {
|
|
223
253
|
private static async resolveP2PAuthToken(): Promise<string | null> {
|
|
224
|
-
|
|
225
|
-
if (!config.preferredDeviceId) return config.authToken;
|
|
226
|
-
const devices = await listReachableDevices(config.authToken);
|
|
227
|
-
const all = [...devices.owned, ...devices.shared];
|
|
228
|
-
const selected = all.find((device) => device.deviceId === config?.preferredDeviceId);
|
|
229
|
-
if (!selected || !selected.isGuest || selected.accessScope !== 'shared-scoped') {
|
|
230
|
-
return config.authToken;
|
|
231
|
-
}
|
|
232
|
-
if (!selected.hostUserId) {
|
|
233
|
-
return config.authToken;
|
|
234
|
-
}
|
|
235
|
-
const delegated = await mintGuestSdkToken(
|
|
236
|
-
config.authToken,
|
|
237
|
-
selected.hostUserId,
|
|
238
|
-
selected.deviceId,
|
|
239
|
-
);
|
|
240
|
-
return delegated.token;
|
|
254
|
+
return config?.authToken ?? null;
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
private static async rebuildP2PClient(agentUrl?: string): Promise<void> {
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
const currentConfig = config;
|
|
259
|
+
if (!currentConfig) return;
|
|
260
|
+
const effectiveUrl = agentUrl ?? currentConfig.agentUrl;
|
|
246
261
|
if (!effectiveUrl) {
|
|
247
262
|
p2pClient = null;
|
|
263
|
+
renderP2PClient = null;
|
|
248
264
|
p2pAuthToken = null;
|
|
249
265
|
return;
|
|
250
266
|
}
|
|
251
267
|
const token = await YaverFeedback.resolveP2PAuthToken();
|
|
268
|
+
// reset() can clear config while relay-password resolution is awaiting.
|
|
269
|
+
// Do not resurrect a client from that stale init generation.
|
|
270
|
+
if (config !== currentConfig) return;
|
|
252
271
|
if (!token) {
|
|
253
272
|
p2pClient = null;
|
|
273
|
+
renderP2PClient = null;
|
|
254
274
|
p2pAuthToken = null;
|
|
255
275
|
return;
|
|
256
276
|
}
|
|
257
277
|
p2pAuthToken = token;
|
|
258
278
|
const rp = await resolveRelayPassword(token);
|
|
279
|
+
if (config !== currentConfig) return;
|
|
259
280
|
p2pClient = new P2PClient(effectiveUrl, token, rp);
|
|
281
|
+
const renderUrl = currentConfig.renderAgentUrl || effectiveUrl;
|
|
282
|
+
renderP2PClient = renderUrl === effectiveUrl ? p2pClient : new P2PClient(renderUrl, token, rp);
|
|
260
283
|
}
|
|
261
284
|
|
|
262
285
|
/**
|
|
@@ -266,12 +289,88 @@ export class YaverFeedback {
|
|
|
266
289
|
* If no `agentUrl` is provided, the SDK will attempt auto-discovery
|
|
267
290
|
* via `YaverDiscovery` on the first `startReport()` call.
|
|
268
291
|
*/
|
|
292
|
+
/** The runtime lane, when running on web inside a Yaver preview WebView. */
|
|
293
|
+
static detectWebLane(): 'browser' | 'webrtc' | null {
|
|
294
|
+
try {
|
|
295
|
+
if (Platform.OS !== 'web' || typeof window === 'undefined') return null;
|
|
296
|
+
const l = String((window as unknown as { __yaverLane?: string }).__yaverLane || '').toLowerCase();
|
|
297
|
+
if (l === 'browser' || l === 'webrtc') return l;
|
|
298
|
+
} catch { /* noop */ }
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Mount a draggable DOM floating "Y" icon for the BROWSER lane (RN-web). It
|
|
304
|
+
* lives in the WebView's document.body (position:fixed, max z-index), so it
|
|
305
|
+
* renders ON TOP of the page inside the WebView and is never occluded by the
|
|
306
|
+
* fullScreen preview modal — the one occlusion-proof affordance on iOS.
|
|
307
|
+
* Tap opens the SDK's existing report flow (the RN FeedbackModal renders in
|
|
308
|
+
* the same WebView DOM, also un-occluded). No-op off-web or off-lane, and
|
|
309
|
+
* idempotent (a re-init reuses the existing node).
|
|
310
|
+
*/
|
|
311
|
+
static mountBrowserLaneIcon(): void {
|
|
312
|
+
if (YaverFeedback.detectWebLane() !== 'browser') return;
|
|
313
|
+
try {
|
|
314
|
+
const doc = (globalThis as unknown as { document?: Document }).document;
|
|
315
|
+
if (!doc || !doc.body) return;
|
|
316
|
+
if (doc.getElementById('yaver-feedback-btn')) return; // idempotent
|
|
317
|
+
const btn = doc.createElement('div');
|
|
318
|
+
btn.id = 'yaver-feedback-btn';
|
|
319
|
+
btn.textContent = 'Y';
|
|
320
|
+
btn.title = 'Yaver Feedback — drag to move · tap to report';
|
|
321
|
+
btn.style.cssText =
|
|
322
|
+
'position:fixed;bottom:20px;right:20px;width:44px;height:44px;border-radius:50%;' +
|
|
323
|
+
'background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;' +
|
|
324
|
+
'cursor:pointer;z-index:99999;font-size:18px;font-weight:bold;touch-action:none;' +
|
|
325
|
+
'box-shadow:0 4px 12px rgba(99,102,241,0.4);transition:transform 0.2s;';
|
|
326
|
+
const drag = { on: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
|
|
327
|
+
const w = window as unknown as { innerWidth: number; innerHeight: number };
|
|
328
|
+
const applyPos = (x: number, y: number) => {
|
|
329
|
+
const m = 8;
|
|
330
|
+
const nx = Math.max(m, Math.min(w.innerWidth - 44 - m, x));
|
|
331
|
+
const ny = Math.max(m, Math.min(w.innerHeight - 44 - m, y));
|
|
332
|
+
btn.style.left = nx + 'px'; btn.style.top = ny + 'px';
|
|
333
|
+
btn.style.right = 'auto'; btn.style.bottom = 'auto';
|
|
334
|
+
try { localStorage.setItem('yaver-feedback-btn-pos', JSON.stringify({ x: nx, y: ny })); } catch { /* noop */ }
|
|
335
|
+
};
|
|
336
|
+
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 */ }
|
|
337
|
+
btn.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
338
|
+
drag.on = true; drag.moved = false;
|
|
339
|
+
const r = btn.getBoundingClientRect();
|
|
340
|
+
drag.sx = e.clientX; drag.sy = e.clientY; drag.ox = r.left; drag.oy = r.top;
|
|
341
|
+
btn.setPointerCapture(e.pointerId); btn.style.transition = 'none';
|
|
342
|
+
});
|
|
343
|
+
btn.addEventListener('pointermove', (e: PointerEvent) => {
|
|
344
|
+
if (!drag.on) return;
|
|
345
|
+
const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
|
|
346
|
+
if (Math.abs(dx) + Math.abs(dy) > 5) drag.moved = true;
|
|
347
|
+
if (drag.moved) applyPos(drag.ox + dx, drag.oy + dy);
|
|
348
|
+
});
|
|
349
|
+
const end = (e: PointerEvent) => {
|
|
350
|
+
if (!drag.on) return;
|
|
351
|
+
drag.on = false; btn.style.transition = 'transform 0.2s';
|
|
352
|
+
try { btn.releasePointerCapture(e.pointerId); } catch { /* noop */ }
|
|
353
|
+
if (!drag.moved) { try { void YaverFeedback.startReport(); } catch { /* noop */ } }
|
|
354
|
+
};
|
|
355
|
+
btn.addEventListener('pointerup', end);
|
|
356
|
+
btn.addEventListener('pointercancel', end);
|
|
357
|
+
// Also answer the host's forwarded shake (Yaver injects yaver-feedback:launch).
|
|
358
|
+
const launch = () => { try { void YaverFeedback.startReport(); } catch { /* noop */ } };
|
|
359
|
+
window.addEventListener('yaver-feedback:launch', launch as EventListener);
|
|
360
|
+
(window as unknown as { __yaverFeedbackLaunch?: () => void }).__yaverFeedbackLaunch = launch;
|
|
361
|
+
doc.body.appendChild(btn);
|
|
362
|
+
} catch { /* noop — never break the guest app over a feedback affordance */ }
|
|
363
|
+
}
|
|
364
|
+
|
|
269
365
|
static init(cfg: FeedbackConfig): void {
|
|
270
366
|
config = {
|
|
271
367
|
trigger: 'shake',
|
|
272
368
|
maxRecordingDuration: 120,
|
|
273
369
|
autoLogin: true,
|
|
274
370
|
...cfg,
|
|
371
|
+
agentUrl: cfg.codingAgentUrl || cfg.agentUrl,
|
|
372
|
+
codingDeviceId: cfg.codingDeviceId || cfg.preferredDeviceId,
|
|
373
|
+
renderDeviceId: cfg.renderDeviceId || cfg.codingDeviceId || cfg.preferredDeviceId,
|
|
275
374
|
};
|
|
276
375
|
if (IS_HOST_MODE) {
|
|
277
376
|
// sfmg / talos / etc. running inside Yaver mobile (compile-time
|
|
@@ -293,6 +392,9 @@ export class YaverFeedback {
|
|
|
293
392
|
}
|
|
294
393
|
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
295
394
|
errorBuffer = [];
|
|
395
|
+
if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
|
|
396
|
+
YaverFeedback.installCrashHandler();
|
|
397
|
+
}
|
|
296
398
|
return;
|
|
297
399
|
}
|
|
298
400
|
if (config.disableShakeGesture && (!config.quickIcon || config.quickIcon === 'auto')) {
|
|
@@ -345,11 +447,16 @@ export class YaverFeedback {
|
|
|
345
447
|
// is set, so set a placeholder header here that won't 401 a
|
|
346
448
|
// direct-LAN url and will be overwritten before any relay hop.
|
|
347
449
|
p2pClient = new P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
|
|
450
|
+
const renderUrl = config.renderAgentUrl || config.agentUrl;
|
|
451
|
+
renderP2PClient = renderUrl === config.agentUrl
|
|
452
|
+
? p2pClient
|
|
453
|
+
: new P2PClient(renderUrl, config.authToken ?? '', p2pRelayPassword);
|
|
348
454
|
if (config.authToken) {
|
|
349
455
|
void YaverFeedback.rebuildP2PClient(config.agentUrl);
|
|
350
456
|
}
|
|
351
457
|
} else {
|
|
352
458
|
p2pClient = null;
|
|
459
|
+
renderP2PClient = null;
|
|
353
460
|
// Auto-discover agent in the background when convexUrl or LAN is available
|
|
354
461
|
if (enabled && (config.authToken || config.preferredDeviceId)) {
|
|
355
462
|
YaverFeedback.discoverAgent();
|
|
@@ -359,24 +466,25 @@ export class YaverFeedback {
|
|
|
359
466
|
// Set up error capture buffer size
|
|
360
467
|
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
361
468
|
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
|
-
});
|
|
469
|
+
if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
|
|
470
|
+
YaverFeedback.installCrashHandler();
|
|
378
471
|
}
|
|
379
472
|
|
|
473
|
+
// Wire up shake detection when trigger is 'shake'. Rebuild from scratch:
|
|
474
|
+
// config was just replaced, so the old detector may be closing over stale
|
|
475
|
+
// settings.
|
|
476
|
+
YaverFeedback.stopShakeDetector();
|
|
477
|
+
YaverFeedback.syncShakeDetector();
|
|
478
|
+
|
|
479
|
+
// Browser lane (RN-web inside Yaver's fullScreen WebView preview): the
|
|
480
|
+
// native shake module doesn't exist on web, and Yaver's own container
|
|
481
|
+
// overlay is OCCLUDED behind the WebView modal on iOS. So mount an
|
|
482
|
+
// occlusion-proof DRAGGABLE DOM icon INSIDE the WebView — same pattern as
|
|
483
|
+
// yaver-feedback-web. Lane is stamped by Yaver via
|
|
484
|
+
// window.__yaverLane='browser' (mobile PREVIEW_LANE_SCRIPT). See
|
|
485
|
+
// docs/audits/feedback-sdk-lanes-audit-2026-07-28.md.
|
|
486
|
+
if (enabled) YaverFeedback.mountBrowserLaneIcon();
|
|
487
|
+
|
|
380
488
|
// Wire up BlackBox command handlers for reload + status signals from
|
|
381
489
|
// the agent. Opens an SSE channel the agent uses to:
|
|
382
490
|
// - broadcast reload_bundle so the phone picks up a fresh Hermes
|
|
@@ -385,7 +493,14 @@ export class YaverFeedback {
|
|
|
385
493
|
// assets…", "Done") so the SDK can surface it like a normal
|
|
386
494
|
// "Working…" spinner instead of a silent 60-second freeze.
|
|
387
495
|
if (enabled) {
|
|
388
|
-
|
|
496
|
+
// onCommand appends to a static array and hands back an unsubscribe.
|
|
497
|
+
// Dropping that unsubscribe meant every re-init stacked another handler
|
|
498
|
+
// on top of the last, so an app that re-initialised (say, a settings
|
|
499
|
+
// toggle) reloaded twice per agent command, then three times, and so on.
|
|
500
|
+
// destroy() never cleared them either. Drop the previous registration
|
|
501
|
+
// first so init() is idempotent here.
|
|
502
|
+
commandUnsubscribe?.();
|
|
503
|
+
commandUnsubscribe = BlackBox.onCommand((cmd) => {
|
|
389
504
|
if (cmd.command === 'reload') {
|
|
390
505
|
if (cfg.onReload) {
|
|
391
506
|
cfg.onReload();
|
|
@@ -447,16 +562,24 @@ export class YaverFeedback {
|
|
|
447
562
|
// SFMG used to call BlackBox.start() inside YaverFeedbackWidget
|
|
448
563
|
// after auth — that path still works (start() is idempotent), so
|
|
449
564
|
// upgrading SDK without removing the manual call is safe.
|
|
565
|
+
// 4. RETRY, rather than checking once. This was a single 500ms
|
|
566
|
+
// timeout, and on a cold start it could not succeed: an app that
|
|
567
|
+
// passes neither agentUrl nor authToken to init() (the normal
|
|
568
|
+
// case — they're restored from storage) gets its session from
|
|
569
|
+
// `void hydrateSession()`, which reads two AsyncStorage keys and
|
|
570
|
+
// then awaits discoverAgent(), a Convex round trip plus LAN
|
|
571
|
+
// probing. That does not finish in 500ms, so the check found no
|
|
572
|
+
// agentUrl, gave up, and BlackBox never started — meaning no SSE
|
|
573
|
+
// channel, so the agent's `reload_bundle` command had nowhere to
|
|
574
|
+
// land and Hermes hot reload silently did nothing until the user
|
|
575
|
+
// shook the device and drove discovery by hand.
|
|
576
|
+
//
|
|
577
|
+
// The both-conditions guard is what keeps the 401 retry storm
|
|
578
|
+
// from coming back, so it is preserved exactly; we only re-check
|
|
579
|
+
// it over time instead of once. Bounded so a device that never
|
|
580
|
+
// signs in doesn't poll forever.
|
|
450
581
|
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);
|
|
582
|
+
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
460
583
|
}
|
|
461
584
|
}
|
|
462
585
|
|
|
@@ -471,6 +594,40 @@ export class YaverFeedback {
|
|
|
471
594
|
// pass-through wrapper they insert into their own error chain
|
|
472
595
|
}
|
|
473
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Opt-in global crash handler. This wraps the existing React Native
|
|
599
|
+
* ErrorUtils handler and still calls it, so Sentry/Crashlytics/Bugsnag can
|
|
600
|
+
* remain the system of record. The SDK only uploads a crash report and,
|
|
601
|
+
* when configured, asks the agent to create a fix task.
|
|
602
|
+
*/
|
|
603
|
+
static installCrashHandler(): void {
|
|
604
|
+
if (crashHandlerInstalled) return;
|
|
605
|
+
try {
|
|
606
|
+
const errorUtils = (globalThis as any)?.ErrorUtils;
|
|
607
|
+
if (!errorUtils?.getGlobalHandler || !errorUtils?.setGlobalHandler) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const next = errorUtils.getGlobalHandler();
|
|
611
|
+
errorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => {
|
|
612
|
+
YaverFeedback.attachError(error, {
|
|
613
|
+
source: 'global-handler',
|
|
614
|
+
crashAware: true,
|
|
615
|
+
});
|
|
616
|
+
if (errorBuffer.length > 0) {
|
|
617
|
+
errorBuffer[errorBuffer.length - 1].isFatal = isFatal ?? true;
|
|
618
|
+
}
|
|
619
|
+
void YaverFeedback.reportCrash(error, {
|
|
620
|
+
isFatal: isFatal ?? true,
|
|
621
|
+
source: 'global-handler',
|
|
622
|
+
});
|
|
623
|
+
next?.(error, isFatal);
|
|
624
|
+
});
|
|
625
|
+
crashHandlerInstalled = true;
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.warn('[YaverFeedback] installCrashHandler failed:', err);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
474
631
|
/**
|
|
475
632
|
* Run agent discovery in the background.
|
|
476
633
|
* Called automatically from init() when no agentUrl is provided.
|
|
@@ -564,6 +721,12 @@ export class YaverFeedback {
|
|
|
564
721
|
} else {
|
|
565
722
|
await YaverFeedback.discoverAgent();
|
|
566
723
|
}
|
|
724
|
+
// Signing in is the event the auto-start was waiting for. If its bounded
|
|
725
|
+
// retry already gave up (device left signed out past the window), this is
|
|
726
|
+
// what brings the command channel up without requiring an app restart.
|
|
727
|
+
if (config.autoStartBlackBox !== false && !BlackBox.isStreaming) {
|
|
728
|
+
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
729
|
+
}
|
|
567
730
|
}
|
|
568
731
|
|
|
569
732
|
/** Returns true once the SDK has a session token it can use. */
|
|
@@ -571,6 +734,59 @@ export class YaverFeedback {
|
|
|
571
734
|
return Boolean(config?.authToken);
|
|
572
735
|
}
|
|
573
736
|
|
|
737
|
+
/**
|
|
738
|
+
* On-device App Store screenshot capture (Engine 2). Walks the routes
|
|
739
|
+
* the host hands us, screenshots each, and uploads to the agent which
|
|
740
|
+
* runs the App Store Connect backend. agentUrl / authToken / relay
|
|
741
|
+
* password default to the SDK's resolved session; the host only has to
|
|
742
|
+
* supply a navigation ref + the ordered route list.
|
|
743
|
+
*
|
|
744
|
+
* YaverFeedback.captureStoreScreenshots({
|
|
745
|
+
* app: 'sfmg',
|
|
746
|
+
* navigationRef,
|
|
747
|
+
* routes: ['/(tabs)/dashboard', '/(tabs)/messages', '/(tabs)/clients'],
|
|
748
|
+
* submit: true,
|
|
749
|
+
* })
|
|
750
|
+
*/
|
|
751
|
+
static async captureStoreScreenshots(
|
|
752
|
+
opts: Omit<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'> &
|
|
753
|
+
Partial<Pick<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'>>,
|
|
754
|
+
): Promise<CaptureStoreScreenshotsResult> {
|
|
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 captureStoreScreenshots({ ...opts, agentUrl, authToken, relayPassword });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Let the mobile app / CLI kick THIS device into self-capturing. Registers
|
|
771
|
+
* a BlackBox command listener that fires captureStoreScreenshots when the
|
|
772
|
+
* agent pushes a `capture_store_shots` command (its `data` may override
|
|
773
|
+
* `submit` / `locale`). Returns an unsubscribe fn. Call once after init,
|
|
774
|
+
* passing the app's navigation ref + the routes to walk.
|
|
775
|
+
*/
|
|
776
|
+
static enableStoreShotsOnCommand(
|
|
777
|
+
opts: Omit<CaptureStoreScreenshotsOptions, 'agentUrl' | 'authToken' | 'relayPassword'>,
|
|
778
|
+
): () => void {
|
|
779
|
+
return BlackBox.onCommand((cmd) => {
|
|
780
|
+
if (cmd?.command !== 'capture_store_shots') return;
|
|
781
|
+
const data = (cmd as any).data ?? {};
|
|
782
|
+
void YaverFeedback.captureStoreScreenshots({
|
|
783
|
+
...opts,
|
|
784
|
+
submit: typeof data.submit === 'boolean' ? data.submit : opts.submit,
|
|
785
|
+
locale: typeof data.locale === 'string' ? data.locale : opts.locale,
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
574
790
|
/**
|
|
575
791
|
* Request the embedded FeedbackModal to show the login screen. Works by
|
|
576
792
|
* emitting an event the modal listens for — avoids forcing the host app
|
|
@@ -591,6 +807,27 @@ export class YaverFeedback {
|
|
|
591
807
|
DeviceEventEmitter.emit('yaverFeedback:startMachinePicker');
|
|
592
808
|
}
|
|
593
809
|
|
|
810
|
+
/** Begin the reusable host-app Dogfood wizard. Owner OAuth and machine
|
|
811
|
+
* selection intentionally happen before installation enrollment/runtime
|
|
812
|
+
* controls because starting builds or runners is owner-level authority. */
|
|
813
|
+
static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): void {
|
|
814
|
+
dogfoodOnboarding = options;
|
|
815
|
+
if (!config) YaverFeedback.init({ autoLogin: true } as FeedbackConfig);
|
|
816
|
+
if (!YaverFeedback.isAuthed()) {
|
|
817
|
+
YaverFeedback.showLogin();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
YaverFeedback.showMachinePicker();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
static getDogfoodOnboarding(): DogfoodOnboardingOptions | null {
|
|
824
|
+
return dogfoodOnboarding;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
static clearDogfoodOnboarding(): void {
|
|
828
|
+
dogfoodOnboarding = null;
|
|
829
|
+
}
|
|
830
|
+
|
|
594
831
|
/**
|
|
595
832
|
* Update the selected remote device. Resets the cached agent URL so the
|
|
596
833
|
* next `startReport()` (or FloatingButton press) rediscovers against the
|
|
@@ -598,9 +835,17 @@ export class YaverFeedback {
|
|
|
598
835
|
*/
|
|
599
836
|
static async setPreferredDevice(deviceId: string): Promise<void> {
|
|
600
837
|
if (!config) return;
|
|
838
|
+
const wasUnified = !config.renderDeviceId
|
|
839
|
+
|| config.renderDeviceId === config.codingDeviceId
|
|
840
|
+
|| config.renderDeviceId === config.preferredDeviceId;
|
|
601
841
|
config.preferredDeviceId = deviceId;
|
|
842
|
+
config.codingDeviceId = deviceId;
|
|
843
|
+
if (wasUnified) config.renderDeviceId = deviceId;
|
|
602
844
|
config.agentUrl = undefined;
|
|
845
|
+
config.codingAgentUrl = undefined;
|
|
846
|
+
if (wasUnified) config.renderAgentUrl = undefined;
|
|
603
847
|
p2pClient = null;
|
|
848
|
+
renderP2PClient = null;
|
|
604
849
|
p2pAuthToken = null;
|
|
605
850
|
await YaverFeedback.discoverAgent();
|
|
606
851
|
}
|
|
@@ -610,8 +855,7 @@ export class YaverFeedback {
|
|
|
610
855
|
if (!config?.authToken || !config.preferredDeviceId) return null;
|
|
611
856
|
const preferredDeviceId = config.preferredDeviceId;
|
|
612
857
|
const devices = await listReachableDevices(config.authToken);
|
|
613
|
-
|
|
614
|
-
return all.find((device) => device.deviceId === preferredDeviceId) ?? null;
|
|
858
|
+
return devices.owned.find((device) => device.deviceId === preferredDeviceId) ?? null;
|
|
615
859
|
}
|
|
616
860
|
|
|
617
861
|
/**
|
|
@@ -671,6 +915,7 @@ export class YaverFeedback {
|
|
|
671
915
|
config.agentUrl = undefined;
|
|
672
916
|
}
|
|
673
917
|
p2pClient = null;
|
|
918
|
+
renderP2PClient = null;
|
|
674
919
|
p2pAuthToken = null;
|
|
675
920
|
}
|
|
676
921
|
|
|
@@ -779,32 +1024,145 @@ export class YaverFeedback {
|
|
|
779
1024
|
}
|
|
780
1025
|
BlackBox.unwrapConsole(); // ensure console is restored even if BlackBox wasn't started
|
|
781
1026
|
errorBuffer = [];
|
|
782
|
-
|
|
783
|
-
shakeDetector.stop();
|
|
784
|
-
shakeDetector = null;
|
|
785
|
-
}
|
|
1027
|
+
YaverFeedback.stopShakeDetector();
|
|
786
1028
|
} else {
|
|
787
1029
|
// === ENABLE ===
|
|
788
1030
|
if (blackBoxWasStreaming) {
|
|
789
1031
|
BlackBox.start(); // restart with previous config
|
|
790
1032
|
}
|
|
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
|
-
}
|
|
1033
|
+
enabled = true; // syncShakeDetector reads this
|
|
1034
|
+
YaverFeedback.syncShakeDetector();
|
|
803
1035
|
}
|
|
804
1036
|
|
|
805
1037
|
enabled = value;
|
|
806
1038
|
}
|
|
807
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* Turn shake-to-report on or off without tearing the SDK down.
|
|
1042
|
+
*
|
|
1043
|
+
* `trigger` is read only inside init(), so before this the only ways to
|
|
1044
|
+
* stop listening for a shake were setEnabled(false) — which also kills the
|
|
1045
|
+
* flight recorder and the agent command channel — or a re-init, which used
|
|
1046
|
+
* to stack a duplicate command handler. Neither is what "turn the shake
|
|
1047
|
+
* catcher off" should cost.
|
|
1048
|
+
*
|
|
1049
|
+
* Persists onto config.disableShakeGesture, so a later setEnabled(true)
|
|
1050
|
+
* honours it rather than resurrecting the listener.
|
|
1051
|
+
*/
|
|
1052
|
+
static setShakeEnabled(value: boolean): void {
|
|
1053
|
+
if (!config) return;
|
|
1054
|
+
config.disableShakeGesture = !value;
|
|
1055
|
+
YaverFeedback.syncShakeDetector();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Whether the shake listener is currently armed. */
|
|
1059
|
+
static isShakeEnabled(): boolean {
|
|
1060
|
+
return shakeDetector !== null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Bring the shake listener in line with the current config. Idempotent —
|
|
1065
|
+
* safe to call whenever `enabled`, `trigger`, or `disableShakeGesture`
|
|
1066
|
+
* moves.
|
|
1067
|
+
*/
|
|
1068
|
+
private static syncShakeDetector(): void {
|
|
1069
|
+
const want =
|
|
1070
|
+
enabled && config?.trigger === 'shake' && !config?.disableShakeGesture;
|
|
1071
|
+
if (want && !shakeDetector) {
|
|
1072
|
+
shakeDetector = new ShakeDetector();
|
|
1073
|
+
shakeDetector.start(() => {
|
|
1074
|
+
YaverFeedback.notifyShake();
|
|
1075
|
+
if (config?.reportingOnly) {
|
|
1076
|
+
YaverFeedback.sendAutoReport();
|
|
1077
|
+
} else {
|
|
1078
|
+
YaverFeedback.startReport();
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
} else if (!want && shakeDetector) {
|
|
1082
|
+
YaverFeedback.stopShakeDetector();
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private static stopShakeDetector(): void {
|
|
1087
|
+
if (shakeDetector) {
|
|
1088
|
+
shakeDetector.stop();
|
|
1089
|
+
shakeDetector = null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Cancel a pending BlackBox auto-start retry chain. */
|
|
1094
|
+
private static cancelBlackBoxAutoStart(): void {
|
|
1095
|
+
if (autoStartTimer) {
|
|
1096
|
+
clearTimeout(autoStartTimer);
|
|
1097
|
+
autoStartTimer = null;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Start BlackBox as soon as we have BOTH an agentUrl and a token, polling
|
|
1103
|
+
* until then.
|
|
1104
|
+
*
|
|
1105
|
+
* Both conditions are mandatory and deliberate: starting without a token
|
|
1106
|
+
* makes the SSE channel 401 and retry with backoff, which is the tight
|
|
1107
|
+
* string-concat + JSON-parse loop that used to SIGSEGV Hermes on iOS 18.3.1
|
|
1108
|
+
* during Screenshot & Fix. This only re-checks that same guard over time.
|
|
1109
|
+
*
|
|
1110
|
+
* Bounded at ~60s: long enough for AsyncStorage hydration plus a Convex
|
|
1111
|
+
* discovery round trip on a cold, off-LAN start, short enough that a device
|
|
1112
|
+
* whose user never signs in stops polling. Giving up here costs nothing —
|
|
1113
|
+
* setAuthToken() and startReport() both reschedule, so signing in later
|
|
1114
|
+
* still brings the channel up.
|
|
1115
|
+
*/
|
|
1116
|
+
private static scheduleBlackBoxAutoStart(attempt = 0): void {
|
|
1117
|
+
const MAX_ATTEMPTS = 60;
|
|
1118
|
+
const FIRST_DELAY_MS = 500;
|
|
1119
|
+
const RETRY_DELAY_MS = 1000;
|
|
1120
|
+
// Re-run discovery every Nth tick rather than every tick: it is a Convex
|
|
1121
|
+
// round trip plus LAN probing, so once a second would hammer both. Every
|
|
1122
|
+
// 3s is responsive enough that waking the dev machine reconnects the phone
|
|
1123
|
+
// on its own, without the user wondering whether to restart the app.
|
|
1124
|
+
const REDISCOVER_EVERY = 3;
|
|
1125
|
+
|
|
1126
|
+
YaverFeedback.cancelBlackBoxAutoStart();
|
|
1127
|
+
if (!enabled || attempt >= MAX_ATTEMPTS) return;
|
|
1128
|
+
|
|
1129
|
+
const timer = setTimeout(() => {
|
|
1130
|
+
autoStartTimer = null;
|
|
1131
|
+
// A destroy() or setEnabled(false) between scheduling and firing.
|
|
1132
|
+
if (!enabled || !config) return;
|
|
1133
|
+
if (BlackBox.isStreaming) return;
|
|
1134
|
+
|
|
1135
|
+
if (config.agentUrl && (config.authToken || p2pAuthToken)) {
|
|
1136
|
+
try {
|
|
1137
|
+
// Pass the host's BlackBox config through. This used to call
|
|
1138
|
+
// start() bare, so a host that configured the flight recorder
|
|
1139
|
+
// — as the README itself shows — had it silently replaced by
|
|
1140
|
+
// defaults, and appName came out ''.
|
|
1141
|
+
BlackBox.start(config.blackBox);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
|
|
1144
|
+
}
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// No agent yet. Re-attempt discovery instead of only re-reading the
|
|
1149
|
+
// flag: hydrateSession() runs discoverAgent() exactly once at init, so
|
|
1150
|
+
// if the dev machine was unreachable at that moment — asleep, phone
|
|
1151
|
+
// still on cellular before the relay came up, agent not yet serving —
|
|
1152
|
+
// agentUrl stayed null for the whole process lifetime and only an app
|
|
1153
|
+
// restart could recover it. discoverAgent() self-guards on "already
|
|
1154
|
+
// have a URL" and "no token", so calling it again is cheap and safe.
|
|
1155
|
+
if (!config.agentUrl && attempt > 0 && attempt % REDISCOVER_EVERY === 0) {
|
|
1156
|
+
void YaverFeedback.discoverAgent();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
YaverFeedback.scheduleBlackBoxAutoStart(attempt + 1);
|
|
1160
|
+
}, attempt === 0 ? FIRST_DELAY_MS : RETRY_DELAY_MS);
|
|
1161
|
+
|
|
1162
|
+
autoStartTimer = timer;
|
|
1163
|
+
unrefTimer(timer);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
808
1166
|
/** Returns whether the SDK is currently enabled. */
|
|
809
1167
|
static isEnabled(): boolean {
|
|
810
1168
|
return enabled;
|
|
@@ -815,6 +1173,63 @@ export class YaverFeedback {
|
|
|
815
1173
|
return config;
|
|
816
1174
|
}
|
|
817
1175
|
|
|
1176
|
+
/** Current third-party SDK mode. Fails closed unless enabled + account match. */
|
|
1177
|
+
static getDogfoodStatus(): SDKDogfoodStatus {
|
|
1178
|
+
return resolveSDKDogfood(config?.dogfood);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** One-call, account-free Dogfood bootstrap for third-party apps. On first
|
|
1182
|
+
* launch it creates/proves the installation key and returns pending; after
|
|
1183
|
+
* owner approval the same call obtains a short-lived scoped Yaver session
|
|
1184
|
+
* and enables Dogfood UX without the host app implementing OAuth. */
|
|
1185
|
+
static async enableDeviceDogfood(options: DeviceDogfoodOptions): Promise<{
|
|
1186
|
+
status: DeviceDogfoodState;
|
|
1187
|
+
installationId: string;
|
|
1188
|
+
session: DeviceDogfoodSession | null;
|
|
1189
|
+
}> {
|
|
1190
|
+
const client = new YaverDeviceDogfood(options);
|
|
1191
|
+
let status = await client.status();
|
|
1192
|
+
if (status === 'unregistered' || status === 'pending' || status === 'cancelled' || status === 'revoked' || status === 'superseded') {
|
|
1193
|
+
const enrolled = status === 'unregistered' || status === 'pending'
|
|
1194
|
+
? await client.enroll(Platform.OS)
|
|
1195
|
+
: await client.reRegister(Platform.OS);
|
|
1196
|
+
status = enrolled.status;
|
|
1197
|
+
}
|
|
1198
|
+
const info = await client.enrollmentInfo();
|
|
1199
|
+
const session = status === 'active' ? await client.session() : null;
|
|
1200
|
+
if (!config) YaverFeedback.init({ autoLogin: false } as FeedbackConfig);
|
|
1201
|
+
if (config) {
|
|
1202
|
+
config.dogfood = {
|
|
1203
|
+
enabled: status === 'active' && !!session,
|
|
1204
|
+
appId: options.appId,
|
|
1205
|
+
installationId: info.installationId,
|
|
1206
|
+
installationStatus: status === 'active' && session ? 'active' : status === 'unregistered' ? 'pending' : status,
|
|
1207
|
+
label: options.label,
|
|
1208
|
+
};
|
|
1209
|
+
// A normal Yaver OAuth session remains authoritative for the runtime
|
|
1210
|
+
// wizard. Only account-free hosts adopt the narrow installation token.
|
|
1211
|
+
if (session && !config.authToken) await YaverFeedback.setAuthToken(session.token);
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
1215
|
+
DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: !!session, status });
|
|
1216
|
+
} catch { /* noop */ }
|
|
1217
|
+
return { status, installationId: info.installationId, session };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** Confirmed by the Y badge/UI before this is called. Reverts this SDK to
|
|
1221
|
+
* normal Feedback mode for the remainder of the app run. */
|
|
1222
|
+
static async exitDogfoodMode(): Promise<void> {
|
|
1223
|
+
const cfg = config?.dogfood;
|
|
1224
|
+
if (!cfg) return;
|
|
1225
|
+
cfg.enabled = false;
|
|
1226
|
+
await cfg.onExit?.();
|
|
1227
|
+
try {
|
|
1228
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
1229
|
+
DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: false, exited: true });
|
|
1230
|
+
} catch { /* noop */ }
|
|
1231
|
+
}
|
|
1232
|
+
|
|
818
1233
|
/** Returns the resolved relay password the SDK is currently using.
|
|
819
1234
|
* Empty string when no relay routing is in play (direct LAN agent
|
|
820
1235
|
* URLs need no password). Callers attaching it to relay-routed
|
|
@@ -883,6 +1298,113 @@ export class YaverFeedback {
|
|
|
883
1298
|
};
|
|
884
1299
|
}
|
|
885
1300
|
|
|
1301
|
+
/**
|
|
1302
|
+
* Upload a crash-aware feedback bundle. Apps can call this from their own
|
|
1303
|
+
* error boundary/global handler. If crashReporting.autoFix is true, the SDK
|
|
1304
|
+
* also triggers the agent's feedback-fix task; reload delivery still happens
|
|
1305
|
+
* through the existing BlackBox command stream.
|
|
1306
|
+
*/
|
|
1307
|
+
static async reportCrash(
|
|
1308
|
+
error: Error,
|
|
1309
|
+
opts?: {
|
|
1310
|
+
isFatal?: boolean;
|
|
1311
|
+
source?: 'manual' | 'global-handler';
|
|
1312
|
+
metadata?: Record<string, unknown>;
|
|
1313
|
+
autoFix?: boolean;
|
|
1314
|
+
},
|
|
1315
|
+
): Promise<{ reportId?: string; taskId?: string }> {
|
|
1316
|
+
if (!config || !enabled || !config.crashReporting?.enabled) return {};
|
|
1317
|
+
if (crashReportInFlight) return {};
|
|
1318
|
+
crashReportInFlight = true;
|
|
1319
|
+
try {
|
|
1320
|
+
YaverFeedback.attachError(error, {
|
|
1321
|
+
...(config.crashReporting.metadata || {}),
|
|
1322
|
+
...(opts?.metadata || {}),
|
|
1323
|
+
crashAware: true,
|
|
1324
|
+
});
|
|
1325
|
+
if (errorBuffer.length > 0) {
|
|
1326
|
+
errorBuffer[errorBuffer.length - 1].isFatal = opts?.isFatal ?? true;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (!config.agentUrl) {
|
|
1330
|
+
await YaverFeedback.discoverAgent();
|
|
1331
|
+
}
|
|
1332
|
+
if (!config.agentUrl) {
|
|
1333
|
+
console.warn('[YaverFeedback] No agent URL — cannot send crash report.');
|
|
1334
|
+
return {};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
const { Platform, Dimensions } = require('react-native');
|
|
1338
|
+
const { uploadFeedback } = require('./upload');
|
|
1339
|
+
const { width, height } = Dimensions.get('window');
|
|
1340
|
+
let screenshotPath: string | undefined;
|
|
1341
|
+
if (config.crashReporting.captureScreenshot !== false) {
|
|
1342
|
+
try {
|
|
1343
|
+
const { captureScreenshot } = require('./capture');
|
|
1344
|
+
screenshotPath = await captureScreenshot();
|
|
1345
|
+
} catch {}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const autoFix = opts?.autoFix ?? config.crashReporting.autoFix === true;
|
|
1349
|
+
const identity = resolveReportIdentity({
|
|
1350
|
+
projectName: config?.projectName,
|
|
1351
|
+
bundleId: config?.bundleId,
|
|
1352
|
+
surface: config?.surface,
|
|
1353
|
+
surfaces: config?.surfaces,
|
|
1354
|
+
stack: config?.stack,
|
|
1355
|
+
stacks: config?.stacks,
|
|
1356
|
+
testSurfaces: config?.testSurfaces,
|
|
1357
|
+
feedbackSdk: config?.feedbackSdk,
|
|
1358
|
+
feedbackTransport: config?.feedbackTransport,
|
|
1359
|
+
voiceCapabilities: config?.voiceCapabilities,
|
|
1360
|
+
sttProvider: config?.sttProvider,
|
|
1361
|
+
ttsProvider: config?.ttsProvider,
|
|
1362
|
+
});
|
|
1363
|
+
const bundle = {
|
|
1364
|
+
metadata: {
|
|
1365
|
+
timestamp: new Date().toISOString(),
|
|
1366
|
+
reportKind: 'crash',
|
|
1367
|
+
deviceInfo: {
|
|
1368
|
+
platform: Platform.OS,
|
|
1369
|
+
osVersion: String(Platform.Version),
|
|
1370
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
1371
|
+
screenWidth: width,
|
|
1372
|
+
screenHeight: height,
|
|
1373
|
+
appName: identity.appName,
|
|
1374
|
+
},
|
|
1375
|
+
app: identity.app,
|
|
1376
|
+
project: identity.project,
|
|
1377
|
+
userNote: '[Crash report]',
|
|
1378
|
+
crash: {
|
|
1379
|
+
message: error.message,
|
|
1380
|
+
isFatal: opts?.isFatal ?? true,
|
|
1381
|
+
source: opts?.source ?? 'manual',
|
|
1382
|
+
autoFixRequested: autoFix,
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
1386
|
+
errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const uploaded = await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle, p2pRelayPassword);
|
|
1390
|
+
const reportId =
|
|
1391
|
+
(uploaded as { id?: string; reportId?: string } | null | undefined)?.id ??
|
|
1392
|
+
(uploaded as { reportId?: string } | null | undefined)?.reportId;
|
|
1393
|
+
let taskId: string | undefined;
|
|
1394
|
+
if (autoFix && reportId) {
|
|
1395
|
+
const client = p2pClient ?? new P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
|
|
1396
|
+
const fix = await client.triggerFix(reportId);
|
|
1397
|
+
taskId = fix?.taskId;
|
|
1398
|
+
}
|
|
1399
|
+
return { reportId, taskId };
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
console.warn('[YaverFeedback] Crash report failed:', err);
|
|
1402
|
+
return {};
|
|
1403
|
+
} finally {
|
|
1404
|
+
crashReportInFlight = false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
886
1408
|
/**
|
|
887
1409
|
* Returns the P2P client instance.
|
|
888
1410
|
* Available after init if agentUrl is set, or after first successful discovery.
|
|
@@ -891,6 +1413,18 @@ export class YaverFeedback {
|
|
|
891
1413
|
return p2pClient;
|
|
892
1414
|
}
|
|
893
1415
|
|
|
1416
|
+
/** Renderer/reload route; intentionally distinct from the coding client. */
|
|
1417
|
+
static getRenderP2PClient(): P2PClient | null {
|
|
1418
|
+
return renderP2PClient ?? p2pClient;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
static getMachineRouting(): { codingDeviceId?: string; renderDeviceId?: string } {
|
|
1422
|
+
return {
|
|
1423
|
+
codingDeviceId: config?.codingDeviceId || config?.preferredDeviceId,
|
|
1424
|
+
renderDeviceId: config?.renderDeviceId || config?.codingDeviceId || config?.preferredDeviceId,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
894
1428
|
// ─── One-stop SaaS replacement methods ─────────────────────────
|
|
895
1429
|
//
|
|
896
1430
|
// These are the three solo-dev SaaS-replacement entry points
|
|
@@ -1010,6 +1544,7 @@ export class YaverFeedback {
|
|
|
1010
1544
|
config.agentUrl = result.url;
|
|
1011
1545
|
const rp = await resolveRelayPassword(config.authToken ?? '');
|
|
1012
1546
|
p2pClient = new P2PClient(result.url, config.authToken ?? '', rp);
|
|
1547
|
+
renderP2PClient = p2pClient;
|
|
1013
1548
|
}
|
|
1014
1549
|
} catch {}
|
|
1015
1550
|
}
|
|
@@ -1033,17 +1568,33 @@ export class YaverFeedback {
|
|
|
1033
1568
|
// Screenshot capture may fail (e.g. no view ref) — continue without it
|
|
1034
1569
|
}
|
|
1035
1570
|
|
|
1571
|
+
const identity = resolveReportIdentity({
|
|
1572
|
+
projectName: config?.projectName,
|
|
1573
|
+
bundleId: config?.bundleId,
|
|
1574
|
+
surface: config?.surface,
|
|
1575
|
+
surfaces: config?.surfaces,
|
|
1576
|
+
stack: config?.stack,
|
|
1577
|
+
stacks: config?.stacks,
|
|
1578
|
+
testSurfaces: config?.testSurfaces,
|
|
1579
|
+
feedbackSdk: config?.feedbackSdk,
|
|
1580
|
+
feedbackTransport: config?.feedbackTransport,
|
|
1581
|
+
voiceCapabilities: config?.voiceCapabilities,
|
|
1582
|
+
sttProvider: config?.sttProvider,
|
|
1583
|
+
ttsProvider: config?.ttsProvider,
|
|
1584
|
+
});
|
|
1036
1585
|
const bundle = {
|
|
1037
1586
|
metadata: {
|
|
1038
1587
|
timestamp: new Date().toISOString(),
|
|
1039
|
-
|
|
1588
|
+
deviceInfo: {
|
|
1040
1589
|
platform: Platform.OS,
|
|
1041
1590
|
osVersion: String(Platform.Version),
|
|
1042
1591
|
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
1043
1592
|
screenWidth: width,
|
|
1044
1593
|
screenHeight: height,
|
|
1594
|
+
appName: identity.appName,
|
|
1045
1595
|
},
|
|
1046
|
-
app:
|
|
1596
|
+
app: identity.app,
|
|
1597
|
+
project: identity.project,
|
|
1047
1598
|
userNote: '[Auto-report via shake]',
|
|
1048
1599
|
},
|
|
1049
1600
|
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
@@ -1211,10 +1762,22 @@ export class YaverFeedback {
|
|
|
1211
1762
|
shakeDetector.stop();
|
|
1212
1763
|
shakeDetector = null;
|
|
1213
1764
|
}
|
|
1765
|
+
// Drop the agent command handler and un-monkey-patch console. Without
|
|
1766
|
+
// these, destroy() left the SDK half-alive: console stayed wrapped, and
|
|
1767
|
+
// the reload handler kept firing on a "destroyed" SDK — then a later
|
|
1768
|
+
// init() stacked a second one on top.
|
|
1769
|
+
commandUnsubscribe?.();
|
|
1770
|
+
commandUnsubscribe = null;
|
|
1771
|
+
// Before `enabled = false` / `config = null` below, so an in-flight retry
|
|
1772
|
+
// can't fire against a torn-down config.
|
|
1773
|
+
YaverFeedback.cancelBlackBoxAutoStart();
|
|
1774
|
+
BlackBox.stop();
|
|
1775
|
+
BlackBox.unwrapConsole();
|
|
1214
1776
|
firstShakeFired = false;
|
|
1215
1777
|
enabled = false;
|
|
1216
1778
|
config = null;
|
|
1217
1779
|
p2pClient = null;
|
|
1780
|
+
renderP2PClient = null;
|
|
1218
1781
|
errorBuffer = [];
|
|
1219
1782
|
}
|
|
1220
1783
|
}
|