yaver-feedback-react-native 0.5.3 → 0.5.5
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 +30 -0
- package/dist/AuthOverlay.d.ts +16 -0
- package/dist/AuthOverlay.js +104 -0
- package/dist/BlackBox.d.ts +154 -0
- package/dist/BlackBox.js +395 -0
- package/dist/ConnectionScreen.d.ts +13 -0
- package/dist/ConnectionScreen.js +373 -0
- package/dist/Discovery.d.ts +59 -0
- package/dist/Discovery.js +293 -0
- package/dist/FeedbackModal.d.ts +11 -0
- package/dist/FeedbackModal.js +623 -0
- package/dist/FixReport.d.ts +23 -0
- package/dist/FixReport.js +282 -0
- package/dist/FloatingButton.d.ts +71 -0
- package/dist/FloatingButton.js +778 -0
- package/dist/LoginScreen.d.ts +14 -0
- package/dist/LoginScreen.js +317 -0
- package/dist/MachinePickerScreen.d.ts +19 -0
- package/dist/MachinePickerScreen.js +175 -0
- package/dist/P2PClient.d.ts +136 -0
- package/dist/P2PClient.js +357 -0
- package/dist/ShakeDetector.d.ts +39 -0
- package/dist/ShakeDetector.js +133 -0
- package/dist/YaverFeedback.d.ts +198 -0
- package/dist/YaverFeedback.js +707 -0
- package/dist/YaverUpdates.d.ts +78 -0
- package/dist/YaverUpdates.js +272 -0
- package/dist/__tests__/Discovery.test.d.ts +1 -0
- package/dist/__tests__/Discovery.test.js +164 -0
- package/dist/__tests__/P2PClient.test.d.ts +1 -0
- package/dist/__tests__/P2PClient.test.js +169 -0
- package/dist/__tests__/SDKToken.test.d.ts +1 -0
- package/dist/__tests__/SDKToken.test.js +215 -0
- package/dist/__tests__/YaverFeedback.test.d.ts +1 -0
- package/dist/__tests__/YaverFeedback.test.js +161 -0
- package/dist/__tests__/types.test.d.ts +1 -0
- package/dist/__tests__/types.test.js +219 -0
- package/dist/auth.d.ts +105 -0
- package/dist/auth.js +282 -0
- package/dist/capture.d.ts +27 -0
- package/dist/capture.js +74 -0
- package/dist/expo.d.ts +15 -0
- package/dist/expo.js +62 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +80 -0
- package/dist/types.d.ts +282 -0
- package/dist/types.js +2 -0
- package/dist/upload.d.ts +13 -0
- package/dist/upload.js +59 -0
- package/package.json +6 -3
- package/src/ShakeDetector.ts +22 -1
- package/src/YaverFeedback.ts +29 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.YaverFeedback = void 0;
|
|
4
|
+
const react_native_1 = require("react-native");
|
|
5
|
+
const Discovery_1 = require("./Discovery");
|
|
6
|
+
const BlackBox_1 = require("./BlackBox");
|
|
7
|
+
const P2PClient_1 = require("./P2PClient");
|
|
8
|
+
const ShakeDetector_1 = require("./ShakeDetector");
|
|
9
|
+
const auth_1 = require("./auth");
|
|
10
|
+
// Is this JS runtime the Yaver mobile app's super-host bridge? The
|
|
11
|
+
// YaverInfo native module is only registered inside Yaver's container
|
|
12
|
+
// (mobile/ios/Yaver/YaverInfo.{swift,m} + Android counterpart); a
|
|
13
|
+
// standalone app bundled by its own developer has no such module.
|
|
14
|
+
// When the SDK is loaded through Yaver's Hermes-push guest runtime we
|
|
15
|
+
// deliberately no-op every public entry point — Yaver owns the shake
|
|
16
|
+
// gesture ("Reload / Back to Yaver" overlay), the feedback capture
|
|
17
|
+
// flow, and the BlackBox streaming; running a second copy from inside
|
|
18
|
+
// the guest just produces duplicate UIs and double P2P sessions.
|
|
19
|
+
function isRunningInsideYaverHost() {
|
|
20
|
+
try {
|
|
21
|
+
return !!react_native_1.NativeModules?.YaverInfo;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Suppresses SDK activation when inside Yaver super-host. Callers of
|
|
28
|
+
// YaverFeedback.init / startReport / startBatchRecording / … early-out
|
|
29
|
+
// by checking this first so the SDK's side effects (accelerometer,
|
|
30
|
+
// BlackBox HTTP, SSE command channel, FeedbackModal mount) never start.
|
|
31
|
+
const YAVER_HOST_SUPPRESS = isRunningInsideYaverHost();
|
|
32
|
+
let config = null;
|
|
33
|
+
let enabled = false;
|
|
34
|
+
let p2pClient = null;
|
|
35
|
+
let shakeDetector = null;
|
|
36
|
+
/** Ring buffer of captured errors. */
|
|
37
|
+
let errorBuffer = [];
|
|
38
|
+
let maxErrors = 5;
|
|
39
|
+
/** Track whether BlackBox was running before disable (to restart on enable). */
|
|
40
|
+
let blackBoxWasStreaming = false;
|
|
41
|
+
/**
|
|
42
|
+
* Flag evaluation cache — 30s TTL per `userId|key`. Prevents a
|
|
43
|
+
* tight render loop from hammering /flags/eval when the dev calls
|
|
44
|
+
* `YaverFeedback.getFlag()` every frame.
|
|
45
|
+
*/
|
|
46
|
+
const flagCache = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Main entry point for the Yaver Feedback SDK.
|
|
49
|
+
* Call `YaverFeedback.init()` once at app startup.
|
|
50
|
+
*/
|
|
51
|
+
class YaverFeedback {
|
|
52
|
+
/**
|
|
53
|
+
* Initialize the feedback SDK with the given configuration.
|
|
54
|
+
* Typically called in your app's root component or entry file.
|
|
55
|
+
*
|
|
56
|
+
* If no `agentUrl` is provided, the SDK will attempt auto-discovery
|
|
57
|
+
* via `YaverDiscovery` on the first `startReport()` call.
|
|
58
|
+
*/
|
|
59
|
+
static init(cfg) {
|
|
60
|
+
if (YAVER_HOST_SUPPRESS) {
|
|
61
|
+
// Running inside Yaver's super-host — yield to Yaver's native UX.
|
|
62
|
+
enabled = false;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
config = {
|
|
66
|
+
trigger: 'shake',
|
|
67
|
+
maxRecordingDuration: 120,
|
|
68
|
+
feedbackMode: 'batch',
|
|
69
|
+
agentCommentaryLevel: 0,
|
|
70
|
+
autoLogin: true,
|
|
71
|
+
...cfg,
|
|
72
|
+
};
|
|
73
|
+
// Route the in-SDK login screen to prod yaver.io by default; callers may
|
|
74
|
+
// override for staging via authConvexSiteUrl / authWebBaseUrl.
|
|
75
|
+
(0, auth_1.configureAuthEndpoints)({
|
|
76
|
+
convexSiteUrl: cfg.authConvexSiteUrl,
|
|
77
|
+
webBaseUrl: cfg.authWebBaseUrl,
|
|
78
|
+
});
|
|
79
|
+
// If no explicit convexUrl was set but we have an auth site URL, use it
|
|
80
|
+
// so Discovery.discoverFromConvex() has somewhere to look up the user's
|
|
81
|
+
// machines (works for both LAN-direct and off-LAN relay paths).
|
|
82
|
+
if (!config.convexUrl) {
|
|
83
|
+
config.convexUrl = cfg.authConvexSiteUrl ?? auth_1.DEFAULT_CONVEX_SITE_URL;
|
|
84
|
+
}
|
|
85
|
+
// Default: enabled only in dev mode
|
|
86
|
+
if (cfg.enabled !== undefined) {
|
|
87
|
+
enabled = cfg.enabled;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
enabled = typeof __DEV__ !== 'undefined' ? __DEV__ : false;
|
|
91
|
+
}
|
|
92
|
+
// Hydrate cached auth token + preferred device from AsyncStorage so the
|
|
93
|
+
// SDK reconnects silently on subsequent launches. If autoLogin is false
|
|
94
|
+
// the caller is responsible for providing authToken themselves.
|
|
95
|
+
if (config.autoLogin !== false && enabled) {
|
|
96
|
+
void YaverFeedback.hydrateSession();
|
|
97
|
+
}
|
|
98
|
+
// Create P2P client if we have a URL
|
|
99
|
+
if (config.agentUrl) {
|
|
100
|
+
p2pClient = new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
p2pClient = null;
|
|
104
|
+
// Auto-discover agent in the background when convexUrl or LAN is available
|
|
105
|
+
if (enabled && (config.authToken || config.preferredDeviceId)) {
|
|
106
|
+
YaverFeedback.discoverAgent();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Set up error capture buffer size
|
|
110
|
+
maxErrors = cfg.maxCapturedErrors ?? 5;
|
|
111
|
+
errorBuffer = [];
|
|
112
|
+
// Wire up shake detection when trigger is 'shake'
|
|
113
|
+
if (shakeDetector) {
|
|
114
|
+
shakeDetector.stop();
|
|
115
|
+
shakeDetector = null;
|
|
116
|
+
}
|
|
117
|
+
if (enabled && config.trigger === 'shake') {
|
|
118
|
+
shakeDetector = new ShakeDetector_1.ShakeDetector();
|
|
119
|
+
shakeDetector.start(() => {
|
|
120
|
+
if (config?.reportingOnly) {
|
|
121
|
+
YaverFeedback.sendAutoReport();
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
YaverFeedback.startReport();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Wire up BlackBox command handlers for reload signals from the agent.
|
|
129
|
+
// This enables the vibe coder to trigger reload from the Yaver mobile app
|
|
130
|
+
// and have the third-party app (with this SDK) automatically reload.
|
|
131
|
+
if (enabled) {
|
|
132
|
+
BlackBox_1.BlackBox.onCommand((cmd) => {
|
|
133
|
+
if (cmd.command === 'reload') {
|
|
134
|
+
if (cfg.onReload) {
|
|
135
|
+
cfg.onReload();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
YaverFeedback.defaultReload();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (cmd.command === 'reload_bundle' && cmd.data) {
|
|
142
|
+
const bundleUrl = cmd.data.bundleUrl;
|
|
143
|
+
const assetsUrl = cmd.data.assetsUrl;
|
|
144
|
+
if (cfg.onReloadBundle) {
|
|
145
|
+
cfg.onReloadBundle(bundleUrl, assetsUrl);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
YaverFeedback.defaultReloadBundle(bundleUrl, assetsUrl);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
|
|
154
|
+
// Sentry, Crashlytics, Bugsnag, and other tools all compete for that
|
|
155
|
+
// single slot. Hijacking it would break whichever tool the developer
|
|
156
|
+
// already has installed, depending on init order.
|
|
157
|
+
//
|
|
158
|
+
// Instead, developers use:
|
|
159
|
+
// - YaverFeedback.attachError(err) in catch blocks
|
|
160
|
+
// - YaverFeedback.wrapErrorHandler(existingHandler) to create a
|
|
161
|
+
// pass-through wrapper they insert into their own error chain
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run agent discovery in the background.
|
|
165
|
+
* Called automatically from init() when no agentUrl is provided.
|
|
166
|
+
* Sets config.agentUrl and creates P2PClient on success.
|
|
167
|
+
*/
|
|
168
|
+
static async discoverAgent() {
|
|
169
|
+
if (!config || !enabled)
|
|
170
|
+
return;
|
|
171
|
+
if (config.agentUrl)
|
|
172
|
+
return; // already have a URL
|
|
173
|
+
if (!config.authToken)
|
|
174
|
+
return; // need auth before discovery can succeed
|
|
175
|
+
try {
|
|
176
|
+
const result = await Discovery_1.YaverDiscovery.discover({
|
|
177
|
+
convexUrl: config.convexUrl,
|
|
178
|
+
authToken: config.authToken,
|
|
179
|
+
preferredDeviceId: config.preferredDeviceId,
|
|
180
|
+
});
|
|
181
|
+
if (result && config) {
|
|
182
|
+
config.agentUrl = result.url;
|
|
183
|
+
p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Discovery failed — FloatingButton will show disconnected, user can retry
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pull a cached session token + selected device from AsyncStorage (populated
|
|
192
|
+
* by the in-SDK login + machine-picker screens). When present the SDK can
|
|
193
|
+
* reconnect silently on launch without re-prompting the user. Safe to call
|
|
194
|
+
* multiple times — it only overrides values the caller did not already set.
|
|
195
|
+
*/
|
|
196
|
+
static async hydrateSession() {
|
|
197
|
+
if (!config)
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
if (!config.authToken) {
|
|
201
|
+
const cached = await (0, auth_1.getToken)();
|
|
202
|
+
if (cached) {
|
|
203
|
+
config.authToken = cached;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!config.preferredDeviceId) {
|
|
207
|
+
const cachedDevice = await (0, auth_1.getSelectedDeviceId)();
|
|
208
|
+
if (cachedDevice) {
|
|
209
|
+
config.preferredDeviceId = cachedDevice;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (config.authToken && !config.agentUrl) {
|
|
213
|
+
await YaverFeedback.discoverAgent();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// hydration best-effort
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Update the signed-in session token (e.g. after the in-SDK login screen
|
|
222
|
+
* succeeds). Rebuilds the P2P client and kicks off agent discovery.
|
|
223
|
+
*/
|
|
224
|
+
static async setAuthToken(token) {
|
|
225
|
+
if (!config)
|
|
226
|
+
return;
|
|
227
|
+
config.authToken = token;
|
|
228
|
+
if (config.agentUrl) {
|
|
229
|
+
p2pClient = new P2PClient_1.P2PClient(config.agentUrl, token);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
await YaverFeedback.discoverAgent();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Returns true once the SDK has a session token it can use. */
|
|
236
|
+
static isAuthed() {
|
|
237
|
+
return Boolean(config?.authToken);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Request the embedded FeedbackModal to show the login screen. Works by
|
|
241
|
+
* emitting an event the modal listens for — avoids forcing the host app
|
|
242
|
+
* to mount a second navigator.
|
|
243
|
+
*/
|
|
244
|
+
static showLogin() {
|
|
245
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
246
|
+
DeviceEventEmitter.emit('yaverFeedback:startLogin');
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Request the embedded FeedbackModal to show the machine picker. Requires
|
|
250
|
+
* an active session; no-ops otherwise.
|
|
251
|
+
*/
|
|
252
|
+
static showMachinePicker() {
|
|
253
|
+
if (!YaverFeedback.isAuthed())
|
|
254
|
+
return;
|
|
255
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
256
|
+
DeviceEventEmitter.emit('yaverFeedback:startMachinePicker');
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Update the selected remote device. Resets the cached agent URL so the
|
|
260
|
+
* next `startReport()` (or FloatingButton press) rediscovers against the
|
|
261
|
+
* newly-selected machine.
|
|
262
|
+
*/
|
|
263
|
+
static async setPreferredDevice(deviceId) {
|
|
264
|
+
if (!config)
|
|
265
|
+
return;
|
|
266
|
+
config.preferredDeviceId = deviceId;
|
|
267
|
+
config.agentUrl = undefined;
|
|
268
|
+
p2pClient = null;
|
|
269
|
+
await YaverFeedback.discoverAgent();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
273
|
+
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
274
|
+
*/
|
|
275
|
+
static async signOut() {
|
|
276
|
+
await (0, auth_1.clearToken)();
|
|
277
|
+
await (0, auth_1.clearSelectedDeviceId)();
|
|
278
|
+
if (config) {
|
|
279
|
+
config.authToken = undefined;
|
|
280
|
+
config.preferredDeviceId = undefined;
|
|
281
|
+
config.agentUrl = undefined;
|
|
282
|
+
}
|
|
283
|
+
p2pClient = null;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Manually trigger the feedback collection flow.
|
|
287
|
+
* Opens the feedback modal if the SDK is initialized and enabled.
|
|
288
|
+
*
|
|
289
|
+
* If no agentUrl was configured, runs auto-discovery first.
|
|
290
|
+
*/
|
|
291
|
+
static async startReport() {
|
|
292
|
+
if (!config) {
|
|
293
|
+
console.warn('[YaverFeedback] SDK not initialized. Call YaverFeedback.init() first.');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!enabled) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// If the caller has autoLogin enabled and we have no session yet, show
|
|
300
|
+
// the in-SDK login flow instead of a failing discovery + warning spam.
|
|
301
|
+
if (!config.authToken) {
|
|
302
|
+
if (config.autoLogin !== false) {
|
|
303
|
+
await YaverFeedback.hydrateSession();
|
|
304
|
+
}
|
|
305
|
+
if (!config.authToken) {
|
|
306
|
+
YaverFeedback.showLogin();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Auto-discover if no agent URL was provided
|
|
311
|
+
if (!config.agentUrl) {
|
|
312
|
+
try {
|
|
313
|
+
const result = await Discovery_1.YaverDiscovery.discover({
|
|
314
|
+
convexUrl: config.convexUrl,
|
|
315
|
+
authToken: config.authToken,
|
|
316
|
+
preferredDeviceId: config.preferredDeviceId,
|
|
317
|
+
});
|
|
318
|
+
if (result) {
|
|
319
|
+
config.agentUrl = result.url;
|
|
320
|
+
p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
|
|
321
|
+
}
|
|
322
|
+
else if (config.autoLogin !== false && !config.preferredDeviceId) {
|
|
323
|
+
// No agent discovered and no device picked yet — prompt the user
|
|
324
|
+
// to pick one of their machines (handles the non-LAN case where
|
|
325
|
+
// relay discovery requires knowing which deviceId to target).
|
|
326
|
+
YaverFeedback.showMachinePicker();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
console.warn('[YaverFeedback] No agent found. Check that `yaver serve` is running on the selected machine.');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
console.warn('[YaverFeedback] Auto-discovery failed:', err);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Emit event that the FeedbackModal listens for
|
|
338
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
339
|
+
DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
340
|
+
}
|
|
341
|
+
/** Returns true if the SDK has been initialized. */
|
|
342
|
+
static isInitialized() {
|
|
343
|
+
return config !== null;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Enable or disable the entire feedback SDK at runtime.
|
|
347
|
+
*
|
|
348
|
+
* **Disable (false):**
|
|
349
|
+
* - Stops BlackBox streaming (flushes remaining events first)
|
|
350
|
+
* - Restores console.log/warn/error if wrapped
|
|
351
|
+
* - Clears error buffer
|
|
352
|
+
* - All methods become no-ops (attachError, wrapErrorHandler still safe to call but do nothing)
|
|
353
|
+
* - P2P client is kept alive (no reconnection cost on re-enable)
|
|
354
|
+
*
|
|
355
|
+
* **Enable (true):**
|
|
356
|
+
* - Restarts BlackBox streaming if it was running before disable
|
|
357
|
+
* - Error buffer starts collecting again
|
|
358
|
+
* - All methods become active
|
|
359
|
+
*/
|
|
360
|
+
static setEnabled(value) {
|
|
361
|
+
if (enabled === value)
|
|
362
|
+
return; // No-op if already in desired state
|
|
363
|
+
if (!value) {
|
|
364
|
+
// === DISABLE ===
|
|
365
|
+
blackBoxWasStreaming = BlackBox_1.BlackBox.isStreaming;
|
|
366
|
+
if (BlackBox_1.BlackBox.isStreaming) {
|
|
367
|
+
BlackBox_1.BlackBox.stop(); // flush + stop timer + unwrap console
|
|
368
|
+
}
|
|
369
|
+
BlackBox_1.BlackBox.unwrapConsole(); // ensure console is restored even if BlackBox wasn't started
|
|
370
|
+
errorBuffer = [];
|
|
371
|
+
if (shakeDetector) {
|
|
372
|
+
shakeDetector.stop();
|
|
373
|
+
shakeDetector = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// === ENABLE ===
|
|
378
|
+
if (blackBoxWasStreaming) {
|
|
379
|
+
BlackBox_1.BlackBox.start(); // restart with previous config
|
|
380
|
+
}
|
|
381
|
+
// Restart shake detector if trigger is 'shake'
|
|
382
|
+
if (config?.trigger === 'shake' && !shakeDetector) {
|
|
383
|
+
shakeDetector = new ShakeDetector_1.ShakeDetector();
|
|
384
|
+
shakeDetector.start(() => {
|
|
385
|
+
if (config?.reportingOnly) {
|
|
386
|
+
YaverFeedback.sendAutoReport();
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
YaverFeedback.startReport();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
enabled = value;
|
|
395
|
+
}
|
|
396
|
+
/** Returns whether the SDK is currently enabled. */
|
|
397
|
+
static isEnabled() {
|
|
398
|
+
return enabled;
|
|
399
|
+
}
|
|
400
|
+
/** Returns the current config, or null if not initialized. */
|
|
401
|
+
static getConfig() {
|
|
402
|
+
return config;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Manually attach an error with optional metadata.
|
|
406
|
+
* Use this in catch blocks to give the agent extra context.
|
|
407
|
+
*/
|
|
408
|
+
static attachError(error, metadata) {
|
|
409
|
+
if (!enabled)
|
|
410
|
+
return; // No-op when disabled
|
|
411
|
+
const captured = {
|
|
412
|
+
message: error.message,
|
|
413
|
+
stack: (error.stack ?? '').split('\n').filter((l) => l.trim()),
|
|
414
|
+
isFatal: false,
|
|
415
|
+
timestamp: Date.now(),
|
|
416
|
+
metadata,
|
|
417
|
+
};
|
|
418
|
+
errorBuffer.push(captured);
|
|
419
|
+
if (errorBuffer.length > maxErrors) {
|
|
420
|
+
errorBuffer.shift();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Returns the current captured errors buffer.
|
|
425
|
+
* Called internally when building a FeedbackBundle.
|
|
426
|
+
*/
|
|
427
|
+
static getCapturedErrors() {
|
|
428
|
+
return [...errorBuffer];
|
|
429
|
+
}
|
|
430
|
+
/** Clears the captured errors buffer. */
|
|
431
|
+
static clearCapturedErrors() {
|
|
432
|
+
errorBuffer = [];
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Returns a pass-through error handler that records the error in Yaver's
|
|
436
|
+
* buffer and then calls `next`. Use this to insert Yaver into your
|
|
437
|
+
* existing error handler chain without replacing anything.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* // Works alongside Sentry, Crashlytics, or any other tool:
|
|
441
|
+
* const originalHandler = ErrorUtils.getGlobalHandler();
|
|
442
|
+
* ErrorUtils.setGlobalHandler(
|
|
443
|
+
* YaverFeedback.wrapErrorHandler(originalHandler)
|
|
444
|
+
* );
|
|
445
|
+
* // Sentry can still be initialized after this — it will wrap our
|
|
446
|
+
* // wrapper, and the chain stays intact.
|
|
447
|
+
*/
|
|
448
|
+
static wrapErrorHandler(next) {
|
|
449
|
+
return (error, isFatal) => {
|
|
450
|
+
YaverFeedback.attachError(error);
|
|
451
|
+
if (errorBuffer.length > 0) {
|
|
452
|
+
errorBuffer[errorBuffer.length - 1].isFatal = isFatal ?? false;
|
|
453
|
+
}
|
|
454
|
+
next?.(error, isFatal);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Returns the P2P client instance.
|
|
459
|
+
* Available after init if agentUrl is set, or after first successful discovery.
|
|
460
|
+
*/
|
|
461
|
+
static getP2PClient() {
|
|
462
|
+
return p2pClient;
|
|
463
|
+
}
|
|
464
|
+
/** Returns the current feedback mode. */
|
|
465
|
+
static getFeedbackMode() {
|
|
466
|
+
return config?.feedbackMode ?? 'batch';
|
|
467
|
+
}
|
|
468
|
+
/** Returns the agent commentary level (0-10). */
|
|
469
|
+
static getCommentaryLevel() {
|
|
470
|
+
return config?.agentCommentaryLevel ?? 0;
|
|
471
|
+
}
|
|
472
|
+
// ─── One-stop SaaS replacement methods ─────────────────────────
|
|
473
|
+
//
|
|
474
|
+
// These are the three solo-dev SaaS-replacement entry points
|
|
475
|
+
// wired into YaverFeedback so there's exactly one import path
|
|
476
|
+
// for the dev's app code: track / getFlag / checkUpdate.
|
|
477
|
+
/**
|
|
478
|
+
* Record a business event. Routes through BlackBox so the agent
|
|
479
|
+
* persists it to the analytics ledger (no dashboards — export
|
|
480
|
+
* via CSV or webhook into PostHog).
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```ts
|
|
484
|
+
* YaverFeedback.track('purchase_completed', { amount: '9.99' });
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
static track(name, props, route) {
|
|
488
|
+
if (!enabled)
|
|
489
|
+
return;
|
|
490
|
+
BlackBox_1.BlackBox.track(name, props, route);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Evaluate a single feature flag for a user. Results are cached
|
|
494
|
+
* for 30 seconds inside YaverFeedback so a tight loop evaluating
|
|
495
|
+
* the same key doesn't hammer the agent.
|
|
496
|
+
*
|
|
497
|
+
* @param key — flag key (must exist on the agent)
|
|
498
|
+
* @param defaultValue — returned if the flag is missing / offline
|
|
499
|
+
* @param userId — stable user identifier for rollout bucketing
|
|
500
|
+
*/
|
|
501
|
+
static async getFlag(key, defaultValue, userId = 'anonymous') {
|
|
502
|
+
if (!enabled || !p2pClient)
|
|
503
|
+
return defaultValue;
|
|
504
|
+
const cacheKey = `${userId}|${key}`;
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
const cached = flagCache.get(cacheKey);
|
|
507
|
+
if (cached && now - cached.at < 30000) {
|
|
508
|
+
return cached.value ?? defaultValue;
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const val = await p2pClient.flagsEvaluateOne(key, userId);
|
|
512
|
+
flagCache.set(cacheKey, { value: val ?? defaultValue, at: now });
|
|
513
|
+
return val ?? defaultValue;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return defaultValue;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Bulk evaluate every flag for a user. Cached on the same 30s
|
|
521
|
+
* window as getFlag — use this when boot needs a handful of
|
|
522
|
+
* flags in one roundtrip.
|
|
523
|
+
*/
|
|
524
|
+
static async getFlags(userId = 'anonymous') {
|
|
525
|
+
if (!enabled || !p2pClient)
|
|
526
|
+
return {};
|
|
527
|
+
const cacheKey = `all|${userId}`;
|
|
528
|
+
const now = Date.now();
|
|
529
|
+
const cached = flagCache.get(cacheKey);
|
|
530
|
+
if (cached && now - cached.at < 30000) {
|
|
531
|
+
return cached.value ?? {};
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
const flags = await p2pClient.flagsEvaluate(userId);
|
|
535
|
+
flagCache.set(cacheKey, { value: flags, at: now });
|
|
536
|
+
return flags;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Ask what bundle this device should run. Returns the latest
|
|
544
|
+
* release manifest in the configured channel plus a rollout
|
|
545
|
+
* gate. The dev can then compare against what's currently
|
|
546
|
+
* running and prompt the user to reload.
|
|
547
|
+
*
|
|
548
|
+
* On-disk bundle swap is platform-specific — see
|
|
549
|
+
* `YaverFeedback.onUpdateAvailable` if you want a hook.
|
|
550
|
+
*/
|
|
551
|
+
static async checkUpdate(channel = 'production', deviceId) {
|
|
552
|
+
if (!enabled || !p2pClient)
|
|
553
|
+
return null;
|
|
554
|
+
return p2pClient.releasesLatest(channel, deviceId);
|
|
555
|
+
}
|
|
556
|
+
/** Clear the in-memory flag cache. Useful for tests or after sign-out. */
|
|
557
|
+
static clearFlagCache() {
|
|
558
|
+
flagCache.clear();
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Reporting-only mode: auto-capture screenshot + errors and send
|
|
562
|
+
* to the agent's /feedback endpoint. No modal UI — just shake and go.
|
|
563
|
+
*
|
|
564
|
+
* This is triggered by shake when `reportingOnly: true` is set.
|
|
565
|
+
* The agent receives the report via the same P2P channel and logs it.
|
|
566
|
+
*/
|
|
567
|
+
static async sendAutoReport() {
|
|
568
|
+
if (!config || !enabled)
|
|
569
|
+
return;
|
|
570
|
+
// Resolve agent URL if needed
|
|
571
|
+
if (!config.agentUrl) {
|
|
572
|
+
try {
|
|
573
|
+
const result = await Discovery_1.YaverDiscovery.discover({
|
|
574
|
+
convexUrl: config.convexUrl,
|
|
575
|
+
authToken: config.authToken,
|
|
576
|
+
preferredDeviceId: config.preferredDeviceId,
|
|
577
|
+
});
|
|
578
|
+
if (result) {
|
|
579
|
+
config.agentUrl = result.url;
|
|
580
|
+
p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch { }
|
|
584
|
+
}
|
|
585
|
+
if (!config.agentUrl) {
|
|
586
|
+
console.warn('[YaverFeedback] No agent URL — cannot send auto report.');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const { Platform, Dimensions } = require('react-native');
|
|
591
|
+
const { captureScreenshot } = require('./capture');
|
|
592
|
+
const { uploadFeedback } = require('./upload');
|
|
593
|
+
const { width, height } = Dimensions.get('window');
|
|
594
|
+
// Auto-capture screenshot
|
|
595
|
+
let screenshotPath;
|
|
596
|
+
try {
|
|
597
|
+
screenshotPath = await captureScreenshot();
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
// Screenshot capture may fail (e.g. no view ref) — continue without it
|
|
601
|
+
}
|
|
602
|
+
const bundle = {
|
|
603
|
+
metadata: {
|
|
604
|
+
timestamp: new Date().toISOString(),
|
|
605
|
+
device: {
|
|
606
|
+
platform: Platform.OS,
|
|
607
|
+
osVersion: String(Platform.Version),
|
|
608
|
+
model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
|
|
609
|
+
screenWidth: width,
|
|
610
|
+
screenHeight: height,
|
|
611
|
+
},
|
|
612
|
+
app: {},
|
|
613
|
+
userNote: '[Auto-report via shake]',
|
|
614
|
+
},
|
|
615
|
+
screenshots: screenshotPath ? [screenshotPath] : [],
|
|
616
|
+
errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
|
|
617
|
+
};
|
|
618
|
+
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
619
|
+
console.log('[YaverFeedback] Auto-report sent');
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
console.warn('[YaverFeedback] Auto-report failed:', err);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Default reload handler. Tries three strategies in order:
|
|
627
|
+
*
|
|
628
|
+
* 1. **YaverBundleLoader** — running inside Yaver's native container.
|
|
629
|
+
* Pulls fresh Hermes bundle from agent and reloads the RN bridge.
|
|
630
|
+
*
|
|
631
|
+
* 2. **YaverHotReload** — standalone app with feedback SDK's native module
|
|
632
|
+
* (added via Expo config plugin). Downloads Hermes bundle from agent,
|
|
633
|
+
* saves to Documents, and reloads the RN bridge.
|
|
634
|
+
*
|
|
635
|
+
* 3. **DevSettings.reload()** — standalone dev build connected to Metro.
|
|
636
|
+
*/
|
|
637
|
+
static defaultReload() {
|
|
638
|
+
if (!config?.agentUrl)
|
|
639
|
+
return;
|
|
640
|
+
const bundleUrl = `${config.agentUrl}/dev/native-bundle`;
|
|
641
|
+
const headers = { Authorization: `Bearer ${config.authToken ?? ''}` };
|
|
642
|
+
YaverFeedback.loadBundleAndReload(bundleUrl, headers);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Default reload_bundle handler. Receives a compiled Hermes bundle URL
|
|
646
|
+
* from the agent and loads it via the best available native mechanism.
|
|
647
|
+
*/
|
|
648
|
+
static defaultReloadBundle(bundleUrl, _assetsUrl) {
|
|
649
|
+
if (!config?.agentUrl)
|
|
650
|
+
return;
|
|
651
|
+
const fullUrl = bundleUrl.startsWith('http')
|
|
652
|
+
? bundleUrl
|
|
653
|
+
: `${config.agentUrl}${bundleUrl}`;
|
|
654
|
+
const headers = { Authorization: `Bearer ${config.authToken ?? ''}` };
|
|
655
|
+
YaverFeedback.loadBundleAndReload(fullUrl, headers);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Core bundle reload logic. Tries native loaders in order:
|
|
659
|
+
*
|
|
660
|
+
* 1. YaverBundleLoader (Yaver container — full validation + bridge reload)
|
|
661
|
+
* 2. YaverHotReload (SDK's own native module — download + bridge reload)
|
|
662
|
+
* 3. DevSettings.reload() (Metro dev server fallback)
|
|
663
|
+
*/
|
|
664
|
+
static loadBundleAndReload(bundleUrl, headers) {
|
|
665
|
+
const { NativeModules } = require('react-native');
|
|
666
|
+
// Strategy 1: YaverBundleLoader (running inside Yaver container)
|
|
667
|
+
if (NativeModules.YaverBundleLoader) {
|
|
668
|
+
NativeModules.YaverBundleLoader.loadBundle(bundleUrl, 'main', headers)
|
|
669
|
+
.catch((err) => {
|
|
670
|
+
console.warn('[YaverFeedback] YaverBundleLoader reload failed:', err);
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// Strategy 2: YaverHotReload (SDK's native module, added by Expo config plugin)
|
|
675
|
+
if (NativeModules.YaverHotReload) {
|
|
676
|
+
NativeModules.YaverHotReload.loadBundle(bundleUrl, headers)
|
|
677
|
+
.catch((err) => {
|
|
678
|
+
console.warn('[YaverFeedback] YaverHotReload reload failed:', err);
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Strategy 3: DevSettings.reload() for Metro dev builds
|
|
683
|
+
console.warn('[YaverFeedback] No native bundle loader available. ' +
|
|
684
|
+
'Add "yaver-feedback-react-native" to your app.json plugins to enable hot reload.');
|
|
685
|
+
try {
|
|
686
|
+
const { DevSettings } = require('react-native');
|
|
687
|
+
if (typeof DevSettings?.reload === 'function') {
|
|
688
|
+
DevSettings.reload();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Not in dev mode
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/** Tear down the SDK (stop shake detector, clear state). */
|
|
696
|
+
static destroy() {
|
|
697
|
+
if (shakeDetector) {
|
|
698
|
+
shakeDetector.stop();
|
|
699
|
+
shakeDetector = null;
|
|
700
|
+
}
|
|
701
|
+
enabled = false;
|
|
702
|
+
config = null;
|
|
703
|
+
p2pClient = null;
|
|
704
|
+
errorBuffer = [];
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
exports.YaverFeedback = YaverFeedback;
|