yaver-feedback-react-native 0.5.2 → 0.5.4
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 +8 -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 +111 -0
- package/dist/YaverFeedback.d.ts +198 -0
- package/dist/YaverFeedback.js +679 -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 +10 -6
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ Visual feedback SDK for Yaver. Lets testers and developers shake their phone (or
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g yaver-cli
|
|
9
|
+
cd your-app
|
|
10
|
+
yaver feedback setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Manual fallback:
|
|
14
|
+
|
|
7
15
|
```bash
|
|
8
16
|
npm install yaver-feedback-react-native
|
|
9
17
|
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Presentation layer for the SDK's auth + machine-picker modals.
|
|
4
|
+
*
|
|
5
|
+
* Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
|
|
6
|
+
* get in-app login with no extra wiring. It listens for events emitted by
|
|
7
|
+
* `YaverFeedback.showLogin()` / `showMachinePicker()`:
|
|
8
|
+
*
|
|
9
|
+
* yaverFeedback:startLogin → show login modal
|
|
10
|
+
* yaverFeedback:startMachinePicker → show machine picker
|
|
11
|
+
*
|
|
12
|
+
* The overlay closes itself once login/pick succeeds, then re-emits
|
|
13
|
+
* `yaverFeedback:startReport` so the user continues straight into the
|
|
14
|
+
* feedback flow they originally triggered.
|
|
15
|
+
*/
|
|
16
|
+
export declare const AuthOverlay: React.FC;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AuthOverlay = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const LoginScreen_1 = require("./LoginScreen");
|
|
40
|
+
const MachinePickerScreen_1 = require("./MachinePickerScreen");
|
|
41
|
+
const YaverFeedback_1 = require("./YaverFeedback");
|
|
42
|
+
const auth_1 = require("./auth");
|
|
43
|
+
/**
|
|
44
|
+
* Presentation layer for the SDK's auth + machine-picker modals.
|
|
45
|
+
*
|
|
46
|
+
* Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
|
|
47
|
+
* get in-app login with no extra wiring. It listens for events emitted by
|
|
48
|
+
* `YaverFeedback.showLogin()` / `showMachinePicker()`:
|
|
49
|
+
*
|
|
50
|
+
* yaverFeedback:startLogin → show login modal
|
|
51
|
+
* yaverFeedback:startMachinePicker → show machine picker
|
|
52
|
+
*
|
|
53
|
+
* The overlay closes itself once login/pick succeeds, then re-emits
|
|
54
|
+
* `yaverFeedback:startReport` so the user continues straight into the
|
|
55
|
+
* feedback flow they originally triggered.
|
|
56
|
+
*/
|
|
57
|
+
const AuthOverlay = () => {
|
|
58
|
+
const [loginVisible, setLoginVisible] = (0, react_1.useState)(false);
|
|
59
|
+
const [pickerVisible, setPickerVisible] = (0, react_1.useState)(false);
|
|
60
|
+
const [token, setToken] = (0, react_1.useState)(null);
|
|
61
|
+
(0, react_1.useEffect)(() => {
|
|
62
|
+
let mounted = true;
|
|
63
|
+
(async () => {
|
|
64
|
+
const cached = await (0, auth_1.getToken)();
|
|
65
|
+
if (mounted && cached)
|
|
66
|
+
setToken(cached);
|
|
67
|
+
})();
|
|
68
|
+
const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => setLoginVisible(true));
|
|
69
|
+
const pickerSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startMachinePicker', async () => {
|
|
70
|
+
const cached = await (0, auth_1.getToken)();
|
|
71
|
+
if (cached)
|
|
72
|
+
setToken(cached);
|
|
73
|
+
if (cached)
|
|
74
|
+
setPickerVisible(true);
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
mounted = false;
|
|
78
|
+
loginSub.remove();
|
|
79
|
+
pickerSub.remove();
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
const handleLoggedIn = async (newToken) => {
|
|
83
|
+
setToken(newToken);
|
|
84
|
+
await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
|
|
85
|
+
setLoginVisible(false);
|
|
86
|
+
setPickerVisible(true);
|
|
87
|
+
};
|
|
88
|
+
const handleDevicePicked = async (device) => {
|
|
89
|
+
await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
|
|
90
|
+
setPickerVisible(false);
|
|
91
|
+
// Continue straight into the feedback flow the user originally triggered.
|
|
92
|
+
react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
93
|
+
};
|
|
94
|
+
return (<>
|
|
95
|
+
<react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={() => setLoginVisible(false)}>
|
|
96
|
+
<LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={() => setLoginVisible(false)}/>
|
|
97
|
+
</react_native_1.Modal>
|
|
98
|
+
|
|
99
|
+
<react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={() => setPickerVisible(false)}>
|
|
100
|
+
{token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} onPick={handleDevicePicked} onCancel={() => setPickerVisible(false)}/>)}
|
|
101
|
+
</react_native_1.Modal>
|
|
102
|
+
</>);
|
|
103
|
+
};
|
|
104
|
+
exports.AuthOverlay = AuthOverlay;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Black box event types streamed from the device to the agent.
|
|
3
|
+
* These mirror the Go BlackBoxEvent struct on the agent side.
|
|
4
|
+
*/
|
|
5
|
+
export interface BlackBoxEvent {
|
|
6
|
+
type: 'log' | 'error' | 'navigation' | 'lifecycle' | 'network' | 'state' | 'render' | 'track';
|
|
7
|
+
level?: 'info' | 'warn' | 'error';
|
|
8
|
+
message: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
stack?: string[];
|
|
11
|
+
isFatal?: boolean;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
source?: string;
|
|
14
|
+
duration?: number;
|
|
15
|
+
route?: string;
|
|
16
|
+
prevRoute?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Configuration for the black box stream. */
|
|
19
|
+
export interface BlackBoxConfig {
|
|
20
|
+
/** Device identifier (defaults to a generated UUID). */
|
|
21
|
+
deviceId?: string;
|
|
22
|
+
/** Application name for the agent to display. */
|
|
23
|
+
appName?: string;
|
|
24
|
+
/** Flush interval in ms — how often buffered events are sent. Default: 2000. */
|
|
25
|
+
flushInterval?: number;
|
|
26
|
+
/** Max events to buffer before flushing. Default: 50. */
|
|
27
|
+
maxBufferSize?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Flight-recorder-style streaming from the device to the Yaver agent.
|
|
31
|
+
*
|
|
32
|
+
* Captures logs, errors, navigation, lifecycle events, network requests,
|
|
33
|
+
* and state changes — then streams them continuously to the agent's
|
|
34
|
+
* `/blackbox/events` endpoint.
|
|
35
|
+
*
|
|
36
|
+
* The agent uses this context when the developer asks for a fix — it
|
|
37
|
+
* already knows what the app was doing.
|
|
38
|
+
*
|
|
39
|
+
* **Does not hijack any global handlers.** All capture is explicit:
|
|
40
|
+
* - Call `BlackBox.log()` / `.warn()` / `.error()` for console-style logs
|
|
41
|
+
* - Call `BlackBox.navigation()` for screen changes
|
|
42
|
+
* - Call `BlackBox.networkRequest()` for HTTP activity
|
|
43
|
+
* - Use `BlackBox.wrapConsole()` to intercept console.log/warn/error
|
|
44
|
+
* (only if you explicitly opt in — no auto-hooking)
|
|
45
|
+
*/
|
|
46
|
+
/** Command received from the agent via the SSE command channel. */
|
|
47
|
+
export interface BlackBoxCommand {
|
|
48
|
+
command: string;
|
|
49
|
+
data?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** Callback type for handling agent commands. */
|
|
52
|
+
export type CommandHandler = (cmd: BlackBoxCommand) => void;
|
|
53
|
+
export declare class BlackBox {
|
|
54
|
+
private static baseUrl;
|
|
55
|
+
private static authToken;
|
|
56
|
+
private static deviceId;
|
|
57
|
+
private static appName;
|
|
58
|
+
private static buffer;
|
|
59
|
+
private static flushTimer;
|
|
60
|
+
private static flushInterval;
|
|
61
|
+
private static maxBufferSize;
|
|
62
|
+
private static started;
|
|
63
|
+
private static originalConsole;
|
|
64
|
+
private static sseAbortController;
|
|
65
|
+
private static sseReconnectTimer;
|
|
66
|
+
private static commandHandlers;
|
|
67
|
+
private static sseConnected;
|
|
68
|
+
/**
|
|
69
|
+
* Start the black box stream. Call after `YaverFeedback.init()`.
|
|
70
|
+
*
|
|
71
|
+
* The stream sends buffered events to the agent every `flushInterval` ms,
|
|
72
|
+
* or immediately when the buffer reaches `maxBufferSize`.
|
|
73
|
+
*/
|
|
74
|
+
static start(config?: BlackBoxConfig): void;
|
|
75
|
+
/** Stop the black box stream and flush remaining events. */
|
|
76
|
+
static stop(): void;
|
|
77
|
+
/** Whether the black box is currently streaming. */
|
|
78
|
+
static get isStreaming(): boolean;
|
|
79
|
+
static log(message: string, source?: string, metadata?: Record<string, unknown>): void;
|
|
80
|
+
static warn(message: string, source?: string, metadata?: Record<string, unknown>): void;
|
|
81
|
+
static error(message: string, source?: string, metadata?: Record<string, unknown>): void;
|
|
82
|
+
/**
|
|
83
|
+
* Record a business event. Fires into the analytics ledger on
|
|
84
|
+
* the agent — see GET /analytics/events.csv.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* BlackBox.track('purchase_completed', {
|
|
89
|
+
* amount: '9.99',
|
|
90
|
+
* currency: 'USD',
|
|
91
|
+
* plan: 'pro',
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
static track(name: string, props?: Record<string, unknown>, route?: string): void;
|
|
96
|
+
/** Record a caught error with stack trace. Also adds to the feedback error buffer. */
|
|
97
|
+
static captureError(err: Error, isFatal?: boolean, metadata?: Record<string, unknown>): void;
|
|
98
|
+
/** Record a screen/route navigation event. */
|
|
99
|
+
static navigation(route: string, prevRoute?: string, metadata?: Record<string, unknown>): void;
|
|
100
|
+
/** Record an app lifecycle event (mount, unmount, background, foreground). */
|
|
101
|
+
static lifecycle(event: string, metadata?: Record<string, unknown>): void;
|
|
102
|
+
/** Record a network request/response. */
|
|
103
|
+
static networkRequest(method: string, url: string, status?: number, durationMs?: number, metadata?: Record<string, unknown>): void;
|
|
104
|
+
/** Record a state change event (Redux action, context update, etc.). */
|
|
105
|
+
static stateChange(description: string, metadata?: Record<string, unknown>): void;
|
|
106
|
+
/** Record a render/re-render event with optional duration. */
|
|
107
|
+
static render(component: string, durationMs?: number, metadata?: Record<string, unknown>): void;
|
|
108
|
+
/**
|
|
109
|
+
* Wrap `console.log`, `console.warn`, and `console.error` to also
|
|
110
|
+
* stream them to the black box. **Call this explicitly** — the SDK
|
|
111
|
+
* never auto-hooks console.
|
|
112
|
+
*
|
|
113
|
+
* Call `BlackBox.unwrapConsole()` to restore originals.
|
|
114
|
+
*/
|
|
115
|
+
static wrapConsole(): void;
|
|
116
|
+
/** Restore original console methods. */
|
|
117
|
+
static unwrapConsole(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Returns a pass-through error handler that streams errors to the
|
|
120
|
+
* black box AND calls the next handler. Same pattern as
|
|
121
|
+
* `YaverFeedback.wrapErrorHandler`, but streams in real-time.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* const existing = ErrorUtils.getGlobalHandler();
|
|
125
|
+
* ErrorUtils.setGlobalHandler(BlackBox.wrapErrorHandler(existing));
|
|
126
|
+
*/
|
|
127
|
+
static wrapErrorHandler(next?: ((error: Error, isFatal?: boolean) => void) | null): (error: Error, isFatal?: boolean) => void;
|
|
128
|
+
/**
|
|
129
|
+
* Register a handler for commands pushed by the agent.
|
|
130
|
+
* The primary use case is receiving "reload" commands when the vibe coder
|
|
131
|
+
* triggers a reload from the Yaver mobile app.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* BlackBox.onCommand((cmd) => {
|
|
135
|
+
* if (cmd.command === 'reload') {
|
|
136
|
+
* DevSettings.reload(); // or Updates.reloadAsync()
|
|
137
|
+
* }
|
|
138
|
+
* });
|
|
139
|
+
*/
|
|
140
|
+
static onCommand(handler: CommandHandler): () => void;
|
|
141
|
+
/** Whether the SSE command channel is connected. */
|
|
142
|
+
static get isCommandChannelConnected(): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Connect to the agent's /blackbox/stream SSE endpoint.
|
|
145
|
+
* This persistent connection allows the agent to push commands (reload, etc.)
|
|
146
|
+
* back to the SDK. Events are still sent via batch POST /blackbox/events.
|
|
147
|
+
*/
|
|
148
|
+
private static connectSSE;
|
|
149
|
+
private static disconnectSSE;
|
|
150
|
+
private static scheduleSSEReconnect;
|
|
151
|
+
private static push;
|
|
152
|
+
private static flush;
|
|
153
|
+
private static generateDeviceId;
|
|
154
|
+
}
|
package/dist/BlackBox.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BlackBox = void 0;
|
|
4
|
+
const react_native_1 = require("react-native");
|
|
5
|
+
const YaverFeedback_1 = require("./YaverFeedback");
|
|
6
|
+
class BlackBox {
|
|
7
|
+
/**
|
|
8
|
+
* Start the black box stream. Call after `YaverFeedback.init()`.
|
|
9
|
+
*
|
|
10
|
+
* The stream sends buffered events to the agent every `flushInterval` ms,
|
|
11
|
+
* or immediately when the buffer reaches `maxBufferSize`.
|
|
12
|
+
*/
|
|
13
|
+
static start(config) {
|
|
14
|
+
const feedbackConfig = YaverFeedback_1.YaverFeedback.getConfig();
|
|
15
|
+
if (!feedbackConfig?.agentUrl) {
|
|
16
|
+
console.warn('[BlackBox] No agent URL. Call YaverFeedback.init() first or set agentUrl.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
|
|
20
|
+
BlackBox.authToken = feedbackConfig.authToken ?? null;
|
|
21
|
+
BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
|
|
22
|
+
BlackBox.appName = config?.appName ?? '';
|
|
23
|
+
BlackBox.flushInterval = config?.flushInterval ?? 2000;
|
|
24
|
+
BlackBox.maxBufferSize = config?.maxBufferSize ?? 50;
|
|
25
|
+
BlackBox.buffer = [];
|
|
26
|
+
BlackBox.started = true;
|
|
27
|
+
// Start periodic flush
|
|
28
|
+
if (BlackBox.flushTimer)
|
|
29
|
+
clearInterval(BlackBox.flushTimer);
|
|
30
|
+
BlackBox.flushTimer = setInterval(() => BlackBox.flush(), BlackBox.flushInterval);
|
|
31
|
+
// Log the session start
|
|
32
|
+
BlackBox.push({
|
|
33
|
+
type: 'lifecycle',
|
|
34
|
+
message: 'Black box streaming started',
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
});
|
|
37
|
+
// Connect SSE command channel for receiving agent commands (reload, etc.)
|
|
38
|
+
BlackBox.connectSSE();
|
|
39
|
+
}
|
|
40
|
+
/** Stop the black box stream and flush remaining events. */
|
|
41
|
+
static stop() {
|
|
42
|
+
if (!BlackBox.started)
|
|
43
|
+
return;
|
|
44
|
+
BlackBox.push({
|
|
45
|
+
type: 'lifecycle',
|
|
46
|
+
message: 'Black box streaming stopped',
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
});
|
|
49
|
+
BlackBox.flush();
|
|
50
|
+
if (BlackBox.flushTimer) {
|
|
51
|
+
clearInterval(BlackBox.flushTimer);
|
|
52
|
+
BlackBox.flushTimer = null;
|
|
53
|
+
}
|
|
54
|
+
BlackBox.disconnectSSE();
|
|
55
|
+
BlackBox.started = false;
|
|
56
|
+
}
|
|
57
|
+
/** Whether the black box is currently streaming. */
|
|
58
|
+
static get isStreaming() {
|
|
59
|
+
return BlackBox.started;
|
|
60
|
+
}
|
|
61
|
+
// ─── Logging ─────────────────────────────────────────────────────
|
|
62
|
+
static log(message, source, metadata) {
|
|
63
|
+
BlackBox.push({ type: 'log', level: 'info', message, timestamp: Date.now(), source, metadata });
|
|
64
|
+
}
|
|
65
|
+
static warn(message, source, metadata) {
|
|
66
|
+
BlackBox.push({ type: 'log', level: 'warn', message, timestamp: Date.now(), source, metadata });
|
|
67
|
+
}
|
|
68
|
+
static error(message, source, metadata) {
|
|
69
|
+
BlackBox.push({ type: 'log', level: 'error', message, timestamp: Date.now(), source, metadata });
|
|
70
|
+
}
|
|
71
|
+
// ─── Track events ────────────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// Business-event ingest. Routed into the agent's analytics ledger
|
|
74
|
+
// via the same BlackBox stream so the dev doesn't pay Mixpanel
|
|
75
|
+
// / Amplitude for "the user tapped Purchase." Zero dashboards in
|
|
76
|
+
// yaver; export to CSV / webhook into PostHog if you want charts.
|
|
77
|
+
/**
|
|
78
|
+
* Record a business event. Fires into the analytics ledger on
|
|
79
|
+
* the agent — see GET /analytics/events.csv.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* BlackBox.track('purchase_completed', {
|
|
84
|
+
* amount: '9.99',
|
|
85
|
+
* currency: 'USD',
|
|
86
|
+
* plan: 'pro',
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
static track(name, props, route) {
|
|
91
|
+
if (!name)
|
|
92
|
+
return;
|
|
93
|
+
BlackBox.push({
|
|
94
|
+
type: 'track',
|
|
95
|
+
message: name,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
route,
|
|
98
|
+
metadata: props,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// ─── Errors ──────────────────────────────────────────────────────
|
|
102
|
+
/** Record a caught error with stack trace. Also adds to the feedback error buffer. */
|
|
103
|
+
static captureError(err, isFatal = false, metadata) {
|
|
104
|
+
const stack = (err.stack ?? '').split('\n').filter((l) => l.trim());
|
|
105
|
+
BlackBox.push({
|
|
106
|
+
type: 'error',
|
|
107
|
+
message: err.message,
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
stack,
|
|
110
|
+
isFatal,
|
|
111
|
+
metadata,
|
|
112
|
+
});
|
|
113
|
+
// Also feed into the feedback SDK error buffer
|
|
114
|
+
YaverFeedback_1.YaverFeedback.attachError(err, metadata);
|
|
115
|
+
}
|
|
116
|
+
// ─── Navigation ──────────────────────────────────────────────────
|
|
117
|
+
/** Record a screen/route navigation event. */
|
|
118
|
+
static navigation(route, prevRoute, metadata) {
|
|
119
|
+
BlackBox.push({
|
|
120
|
+
type: 'navigation',
|
|
121
|
+
message: `Navigate: ${prevRoute ? prevRoute + ' -> ' : ''}${route}`,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
route,
|
|
124
|
+
prevRoute,
|
|
125
|
+
metadata,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// ─── Lifecycle ───────────────────────────────────────────────────
|
|
129
|
+
/** Record an app lifecycle event (mount, unmount, background, foreground). */
|
|
130
|
+
static lifecycle(event, metadata) {
|
|
131
|
+
BlackBox.push({ type: 'lifecycle', message: event, timestamp: Date.now(), metadata });
|
|
132
|
+
}
|
|
133
|
+
// ─── Network ─────────────────────────────────────────────────────
|
|
134
|
+
/** Record a network request/response. */
|
|
135
|
+
static networkRequest(method, url, status, durationMs, metadata) {
|
|
136
|
+
const msg = status != null
|
|
137
|
+
? `${method} ${url} → ${status}`
|
|
138
|
+
: `${method} ${url}`;
|
|
139
|
+
BlackBox.push({
|
|
140
|
+
type: 'network',
|
|
141
|
+
message: msg,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
duration: durationMs,
|
|
144
|
+
metadata,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// ─── State ───────────────────────────────────────────────────────
|
|
148
|
+
/** Record a state change event (Redux action, context update, etc.). */
|
|
149
|
+
static stateChange(description, metadata) {
|
|
150
|
+
BlackBox.push({ type: 'state', message: description, timestamp: Date.now(), metadata });
|
|
151
|
+
}
|
|
152
|
+
// ─── Render ──────────────────────────────────────────────────────
|
|
153
|
+
/** Record a render/re-render event with optional duration. */
|
|
154
|
+
static render(component, durationMs, metadata) {
|
|
155
|
+
BlackBox.push({
|
|
156
|
+
type: 'render',
|
|
157
|
+
message: component,
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
duration: durationMs,
|
|
160
|
+
metadata,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// ─── Console wrapping (opt-in) ───────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Wrap `console.log`, `console.warn`, and `console.error` to also
|
|
166
|
+
* stream them to the black box. **Call this explicitly** — the SDK
|
|
167
|
+
* never auto-hooks console.
|
|
168
|
+
*
|
|
169
|
+
* Call `BlackBox.unwrapConsole()` to restore originals.
|
|
170
|
+
*/
|
|
171
|
+
static wrapConsole() {
|
|
172
|
+
if (BlackBox.originalConsole)
|
|
173
|
+
return; // Already wrapped
|
|
174
|
+
BlackBox.originalConsole = {
|
|
175
|
+
log: console.log,
|
|
176
|
+
warn: console.warn,
|
|
177
|
+
error: console.error,
|
|
178
|
+
};
|
|
179
|
+
console.log = (...args) => {
|
|
180
|
+
BlackBox.originalConsole.log(...args);
|
|
181
|
+
BlackBox.log(args.map(String).join(' '));
|
|
182
|
+
};
|
|
183
|
+
console.warn = (...args) => {
|
|
184
|
+
BlackBox.originalConsole.warn(...args);
|
|
185
|
+
BlackBox.warn(args.map(String).join(' '));
|
|
186
|
+
};
|
|
187
|
+
console.error = (...args) => {
|
|
188
|
+
BlackBox.originalConsole.error(...args);
|
|
189
|
+
BlackBox.error(args.map(String).join(' '));
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Restore original console methods. */
|
|
193
|
+
static unwrapConsole() {
|
|
194
|
+
if (!BlackBox.originalConsole)
|
|
195
|
+
return;
|
|
196
|
+
console.log = BlackBox.originalConsole.log;
|
|
197
|
+
console.warn = BlackBox.originalConsole.warn;
|
|
198
|
+
console.error = BlackBox.originalConsole.error;
|
|
199
|
+
BlackBox.originalConsole = null;
|
|
200
|
+
}
|
|
201
|
+
// ─── Error handler wrapper (opt-in) ──────────────────────────────
|
|
202
|
+
/**
|
|
203
|
+
* Returns a pass-through error handler that streams errors to the
|
|
204
|
+
* black box AND calls the next handler. Same pattern as
|
|
205
|
+
* `YaverFeedback.wrapErrorHandler`, but streams in real-time.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* const existing = ErrorUtils.getGlobalHandler();
|
|
209
|
+
* ErrorUtils.setGlobalHandler(BlackBox.wrapErrorHandler(existing));
|
|
210
|
+
*/
|
|
211
|
+
static wrapErrorHandler(next) {
|
|
212
|
+
return (error, isFatal) => {
|
|
213
|
+
BlackBox.captureError(error, isFatal ?? false);
|
|
214
|
+
next?.(error, isFatal);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// ─── Command channel (agent → SDK) ──────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Register a handler for commands pushed by the agent.
|
|
220
|
+
* The primary use case is receiving "reload" commands when the vibe coder
|
|
221
|
+
* triggers a reload from the Yaver mobile app.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* BlackBox.onCommand((cmd) => {
|
|
225
|
+
* if (cmd.command === 'reload') {
|
|
226
|
+
* DevSettings.reload(); // or Updates.reloadAsync()
|
|
227
|
+
* }
|
|
228
|
+
* });
|
|
229
|
+
*/
|
|
230
|
+
static onCommand(handler) {
|
|
231
|
+
BlackBox.commandHandlers.push(handler);
|
|
232
|
+
// Return unsubscribe function
|
|
233
|
+
return () => {
|
|
234
|
+
BlackBox.commandHandlers = BlackBox.commandHandlers.filter(h => h !== handler);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** Whether the SSE command channel is connected. */
|
|
238
|
+
static get isCommandChannelConnected() {
|
|
239
|
+
return BlackBox.sseConnected;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Connect to the agent's /blackbox/stream SSE endpoint.
|
|
243
|
+
* This persistent connection allows the agent to push commands (reload, etc.)
|
|
244
|
+
* back to the SDK. Events are still sent via batch POST /blackbox/events.
|
|
245
|
+
*/
|
|
246
|
+
static async connectSSE() {
|
|
247
|
+
if (!BlackBox.baseUrl || !BlackBox.authToken)
|
|
248
|
+
return;
|
|
249
|
+
// Disconnect any existing connection
|
|
250
|
+
BlackBox.disconnectSSE();
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
BlackBox.sseAbortController = controller;
|
|
253
|
+
const url = `${BlackBox.baseUrl}/blackbox/command-stream?device=${encodeURIComponent(BlackBox.deviceId)}`;
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetch(url, {
|
|
256
|
+
method: 'GET',
|
|
257
|
+
headers: {
|
|
258
|
+
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
259
|
+
'X-Device-ID': BlackBox.deviceId,
|
|
260
|
+
'X-Platform': react_native_1.Platform.OS,
|
|
261
|
+
'X-App-Name': BlackBox.appName,
|
|
262
|
+
Accept: 'text/event-stream',
|
|
263
|
+
},
|
|
264
|
+
// @ts-ignore — React Native supports signal on fetch
|
|
265
|
+
signal: controller.signal,
|
|
266
|
+
});
|
|
267
|
+
if (!response.ok || !response.body) {
|
|
268
|
+
BlackBox.scheduleSSEReconnect();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
BlackBox.sseConnected = true;
|
|
272
|
+
// Read SSE stream
|
|
273
|
+
const reader = response.body.getReader();
|
|
274
|
+
const decoder = new TextDecoder();
|
|
275
|
+
let buffer = '';
|
|
276
|
+
while (true) {
|
|
277
|
+
const { done, value } = await reader.read();
|
|
278
|
+
if (done)
|
|
279
|
+
break;
|
|
280
|
+
buffer += decoder.decode(value, { stream: true });
|
|
281
|
+
const lines = buffer.split('\n');
|
|
282
|
+
buffer = lines.pop() ?? '';
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
if (!line.startsWith('data: '))
|
|
285
|
+
continue;
|
|
286
|
+
try {
|
|
287
|
+
const msg = JSON.parse(line.slice(6));
|
|
288
|
+
if (msg.type === 'command' && msg.command) {
|
|
289
|
+
const cmd = msg.command;
|
|
290
|
+
for (const handler of BlackBox.commandHandlers) {
|
|
291
|
+
try {
|
|
292
|
+
handler(cmd);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Handler error — don't break the loop
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Malformed SSE data — skip
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
// AbortError is expected on disconnect
|
|
308
|
+
if (err instanceof Error && err.name === 'AbortError')
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
BlackBox.sseConnected = false;
|
|
313
|
+
}
|
|
314
|
+
// Reconnect if still running
|
|
315
|
+
if (BlackBox.started) {
|
|
316
|
+
BlackBox.scheduleSSEReconnect();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
static disconnectSSE() {
|
|
320
|
+
if (BlackBox.sseAbortController) {
|
|
321
|
+
BlackBox.sseAbortController.abort();
|
|
322
|
+
BlackBox.sseAbortController = null;
|
|
323
|
+
}
|
|
324
|
+
if (BlackBox.sseReconnectTimer) {
|
|
325
|
+
clearTimeout(BlackBox.sseReconnectTimer);
|
|
326
|
+
BlackBox.sseReconnectTimer = null;
|
|
327
|
+
}
|
|
328
|
+
BlackBox.sseConnected = false;
|
|
329
|
+
}
|
|
330
|
+
static scheduleSSEReconnect() {
|
|
331
|
+
if (!BlackBox.started)
|
|
332
|
+
return;
|
|
333
|
+
if (BlackBox.sseReconnectTimer)
|
|
334
|
+
return;
|
|
335
|
+
// Reconnect after 5s
|
|
336
|
+
BlackBox.sseReconnectTimer = setTimeout(() => {
|
|
337
|
+
BlackBox.sseReconnectTimer = null;
|
|
338
|
+
if (BlackBox.started)
|
|
339
|
+
BlackBox.connectSSE();
|
|
340
|
+
}, 5000);
|
|
341
|
+
}
|
|
342
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
343
|
+
static push(event) {
|
|
344
|
+
if (!BlackBox.started)
|
|
345
|
+
return;
|
|
346
|
+
BlackBox.buffer.push(event);
|
|
347
|
+
if (BlackBox.buffer.length >= BlackBox.maxBufferSize) {
|
|
348
|
+
BlackBox.flush();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
static async flush() {
|
|
352
|
+
if (!BlackBox.baseUrl || !BlackBox.authToken || BlackBox.buffer.length === 0)
|
|
353
|
+
return;
|
|
354
|
+
const events = BlackBox.buffer;
|
|
355
|
+
BlackBox.buffer = [];
|
|
356
|
+
try {
|
|
357
|
+
await fetch(`${BlackBox.baseUrl}/blackbox/events`, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: {
|
|
360
|
+
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
'X-Device-ID': BlackBox.deviceId,
|
|
363
|
+
'X-Platform': react_native_1.Platform.OS,
|
|
364
|
+
'X-App-Name': BlackBox.appName,
|
|
365
|
+
},
|
|
366
|
+
body: JSON.stringify(events),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Re-add failed events to buffer (capped to avoid memory growth)
|
|
371
|
+
if (BlackBox.buffer.length + events.length <= BlackBox.maxBufferSize * 2) {
|
|
372
|
+
BlackBox.buffer = [...events, ...BlackBox.buffer];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
static generateDeviceId() {
|
|
377
|
+
return 'xxxxxxxx'.replace(/x/g, () => Math.floor(Math.random() * 16).toString(16));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
exports.BlackBox = BlackBox;
|
|
381
|
+
BlackBox.baseUrl = null;
|
|
382
|
+
BlackBox.authToken = null;
|
|
383
|
+
BlackBox.deviceId = '';
|
|
384
|
+
BlackBox.appName = '';
|
|
385
|
+
BlackBox.buffer = [];
|
|
386
|
+
BlackBox.flushTimer = null;
|
|
387
|
+
BlackBox.flushInterval = 2000;
|
|
388
|
+
BlackBox.maxBufferSize = 50;
|
|
389
|
+
BlackBox.started = false;
|
|
390
|
+
BlackBox.originalConsole = null;
|
|
391
|
+
// SSE command channel — persistent connection to /blackbox/stream
|
|
392
|
+
BlackBox.sseAbortController = null;
|
|
393
|
+
BlackBox.sseReconnectTimer = null;
|
|
394
|
+
BlackBox.commandHandlers = [];
|
|
395
|
+
BlackBox.sseConnected = false;
|