yaver-feedback-react-native 0.2.0
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 +690 -0
- package/app.plugin.js +70 -0
- package/package.json +39 -0
- package/src/BlackBox.ts +317 -0
- package/src/ConnectionScreen.tsx +400 -0
- package/src/Discovery.ts +223 -0
- package/src/FeedbackModal.tsx +678 -0
- package/src/FixReport.tsx +313 -0
- package/src/FloatingButton.tsx +860 -0
- package/src/P2PClient.ts +303 -0
- package/src/ShakeDetector.ts +57 -0
- package/src/YaverFeedback.ts +345 -0
- package/src/__tests__/Discovery.test.ts +187 -0
- package/src/__tests__/P2PClient.test.ts +218 -0
- package/src/__tests__/SDKToken.test.ts +268 -0
- package/src/__tests__/YaverFeedback.test.ts +189 -0
- package/src/__tests__/types.test.ts +247 -0
- package/src/capture.ts +84 -0
- package/src/expo.ts +62 -0
- package/src/index.ts +54 -0
- package/src/types.ts +251 -0
- package/src/upload.ts +74 -0
package/app.plugin.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo config plugin for @yaver/feedback-react-native.
|
|
3
|
+
*
|
|
4
|
+
* Adds required native permissions for the feedback SDK:
|
|
5
|
+
* - iOS: Camera + Microphone usage descriptions
|
|
6
|
+
* - Android: CAMERA + RECORD_AUDIO permissions
|
|
7
|
+
*
|
|
8
|
+
* Usage in app.json:
|
|
9
|
+
* { "expo": { "plugins": ["@yaver/feedback-react-native"] } }
|
|
10
|
+
*/
|
|
11
|
+
const {
|
|
12
|
+
withInfoPlist,
|
|
13
|
+
withAndroidManifest,
|
|
14
|
+
createRunOncePlugin,
|
|
15
|
+
} = require("@expo/config-plugins");
|
|
16
|
+
|
|
17
|
+
const pkg = require("./package.json");
|
|
18
|
+
|
|
19
|
+
function withYaverFeedbackIOS(config) {
|
|
20
|
+
return withInfoPlist(config, (config) => {
|
|
21
|
+
if (!config.modResults.NSCameraUsageDescription) {
|
|
22
|
+
config.modResults.NSCameraUsageDescription =
|
|
23
|
+
"Used for visual feedback screenshots during development";
|
|
24
|
+
}
|
|
25
|
+
if (!config.modResults.NSMicrophoneUsageDescription) {
|
|
26
|
+
config.modResults.NSMicrophoneUsageDescription =
|
|
27
|
+
"Used for voice annotations in feedback reports during development";
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withYaverFeedbackAndroid(config) {
|
|
34
|
+
return withAndroidManifest(config, (config) => {
|
|
35
|
+
const manifest = config.modResults.manifest;
|
|
36
|
+
|
|
37
|
+
if (!manifest["uses-permission"]) {
|
|
38
|
+
manifest["uses-permission"] = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const permissions = manifest["uses-permission"];
|
|
42
|
+
const requiredPermissions = [
|
|
43
|
+
"android.permission.CAMERA",
|
|
44
|
+
"android.permission.RECORD_AUDIO",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const perm of requiredPermissions) {
|
|
48
|
+
const exists = permissions.some(
|
|
49
|
+
(p) => p.$?.["android:name"] === perm
|
|
50
|
+
);
|
|
51
|
+
if (!exists) {
|
|
52
|
+
permissions.push({ $: { "android:name": perm } });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return config;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function withYaverFeedback(config) {
|
|
61
|
+
config = withYaverFeedbackIOS(config);
|
|
62
|
+
config = withYaverFeedbackAndroid(config);
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = createRunOncePlugin(
|
|
67
|
+
withYaverFeedback,
|
|
68
|
+
pkg.name,
|
|
69
|
+
pkg.version
|
|
70
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yaver-feedback-react-native",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Visual feedback SDK for Yaver — shake-to-report, screen recording, voice annotations for vibe coding",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"app.plugin.js"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/kivanccakmak/yaver.io",
|
|
15
|
+
"directory": "sdk/feedback/react-native"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": ">=18.0.0",
|
|
19
|
+
"react-native": ">=0.70.0",
|
|
20
|
+
"@react-native-async-storage/async-storage": ">=1.17.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"@expo/config-plugins": { "optional": true },
|
|
24
|
+
"expo-constants": { "optional": true }
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"jest": "^29.0.0",
|
|
28
|
+
"@types/jest": "^29.0.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "jest"
|
|
33
|
+
},
|
|
34
|
+
"jest": {
|
|
35
|
+
"preset": "react-native",
|
|
36
|
+
"testPathPattern": "__tests__"
|
|
37
|
+
},
|
|
38
|
+
"keywords": ["yaver", "feedback", "bug-report", "screen-recording", "vibe-coding", "react-native"]
|
|
39
|
+
}
|
package/src/BlackBox.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { CapturedError } from './types';
|
|
3
|
+
import { YaverFeedback } from './YaverFeedback';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Black box event types streamed from the device to the agent.
|
|
7
|
+
* These mirror the Go BlackBoxEvent struct on the agent side.
|
|
8
|
+
*/
|
|
9
|
+
export interface BlackBoxEvent {
|
|
10
|
+
type: 'log' | 'error' | 'navigation' | 'lifecycle' | 'network' | 'state' | 'render';
|
|
11
|
+
level?: 'info' | 'warn' | 'error';
|
|
12
|
+
message: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
stack?: string[];
|
|
15
|
+
isFatal?: boolean;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
source?: string;
|
|
18
|
+
duration?: number;
|
|
19
|
+
route?: string;
|
|
20
|
+
prevRoute?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Configuration for the black box stream. */
|
|
24
|
+
export interface BlackBoxConfig {
|
|
25
|
+
/** Device identifier (defaults to a generated UUID). */
|
|
26
|
+
deviceId?: string;
|
|
27
|
+
/** Application name for the agent to display. */
|
|
28
|
+
appName?: string;
|
|
29
|
+
/** Flush interval in ms — how often buffered events are sent. Default: 2000. */
|
|
30
|
+
flushInterval?: number;
|
|
31
|
+
/** Max events to buffer before flushing. Default: 50. */
|
|
32
|
+
maxBufferSize?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Flight-recorder-style streaming from the device to the Yaver agent.
|
|
37
|
+
*
|
|
38
|
+
* Captures logs, errors, navigation, lifecycle events, network requests,
|
|
39
|
+
* and state changes — then streams them continuously to the agent's
|
|
40
|
+
* `/blackbox/events` endpoint.
|
|
41
|
+
*
|
|
42
|
+
* The agent uses this context when the developer asks for a fix — it
|
|
43
|
+
* already knows what the app was doing.
|
|
44
|
+
*
|
|
45
|
+
* **Does not hijack any global handlers.** All capture is explicit:
|
|
46
|
+
* - Call `BlackBox.log()` / `.warn()` / `.error()` for console-style logs
|
|
47
|
+
* - Call `BlackBox.navigation()` for screen changes
|
|
48
|
+
* - Call `BlackBox.networkRequest()` for HTTP activity
|
|
49
|
+
* - Use `BlackBox.wrapConsole()` to intercept console.log/warn/error
|
|
50
|
+
* (only if you explicitly opt in — no auto-hooking)
|
|
51
|
+
*/
|
|
52
|
+
export class BlackBox {
|
|
53
|
+
private static baseUrl: string | null = null;
|
|
54
|
+
private static authToken: string | null = null;
|
|
55
|
+
private static deviceId: string = '';
|
|
56
|
+
private static appName: string = '';
|
|
57
|
+
private static buffer: BlackBoxEvent[] = [];
|
|
58
|
+
private static flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
59
|
+
private static flushInterval = 2000;
|
|
60
|
+
private static maxBufferSize = 50;
|
|
61
|
+
private static started = false;
|
|
62
|
+
private static originalConsole: {
|
|
63
|
+
log: typeof console.log;
|
|
64
|
+
warn: typeof console.warn;
|
|
65
|
+
error: typeof console.error;
|
|
66
|
+
} | null = null;
|
|
67
|
+
|
|
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
|
+
const feedbackConfig = YaverFeedback.getConfig();
|
|
76
|
+
if (!feedbackConfig?.agentUrl) {
|
|
77
|
+
console.warn('[BlackBox] No agent URL. Call YaverFeedback.init() first or set agentUrl.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
|
|
82
|
+
BlackBox.authToken = feedbackConfig.authToken;
|
|
83
|
+
BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
|
|
84
|
+
BlackBox.appName = config?.appName ?? '';
|
|
85
|
+
BlackBox.flushInterval = config?.flushInterval ?? 2000;
|
|
86
|
+
BlackBox.maxBufferSize = config?.maxBufferSize ?? 50;
|
|
87
|
+
BlackBox.buffer = [];
|
|
88
|
+
BlackBox.started = true;
|
|
89
|
+
|
|
90
|
+
// Start periodic flush
|
|
91
|
+
if (BlackBox.flushTimer) clearInterval(BlackBox.flushTimer);
|
|
92
|
+
BlackBox.flushTimer = setInterval(() => BlackBox.flush(), BlackBox.flushInterval);
|
|
93
|
+
|
|
94
|
+
// Log the session start
|
|
95
|
+
BlackBox.push({
|
|
96
|
+
type: 'lifecycle',
|
|
97
|
+
message: 'Black box streaming started',
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Stop the black box stream and flush remaining events. */
|
|
103
|
+
static stop(): void {
|
|
104
|
+
if (!BlackBox.started) return;
|
|
105
|
+
BlackBox.push({
|
|
106
|
+
type: 'lifecycle',
|
|
107
|
+
message: 'Black box streaming stopped',
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
BlackBox.flush();
|
|
111
|
+
if (BlackBox.flushTimer) {
|
|
112
|
+
clearInterval(BlackBox.flushTimer);
|
|
113
|
+
BlackBox.flushTimer = null;
|
|
114
|
+
}
|
|
115
|
+
BlackBox.started = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether the black box is currently streaming. */
|
|
119
|
+
static get isStreaming(): boolean {
|
|
120
|
+
return BlackBox.started;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Logging ─────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
static log(message: string, source?: string, metadata?: Record<string, unknown>): void {
|
|
126
|
+
BlackBox.push({ type: 'log', level: 'info', message, timestamp: Date.now(), source, metadata });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static warn(message: string, source?: string, metadata?: Record<string, unknown>): void {
|
|
130
|
+
BlackBox.push({ type: 'log', level: 'warn', message, timestamp: Date.now(), source, metadata });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static error(message: string, source?: string, metadata?: Record<string, unknown>): void {
|
|
134
|
+
BlackBox.push({ type: 'log', level: 'error', message, timestamp: Date.now(), source, metadata });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Errors ──────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** Record a caught error with stack trace. Also adds to the feedback error buffer. */
|
|
140
|
+
static captureError(err: Error, isFatal = false, metadata?: Record<string, unknown>): void {
|
|
141
|
+
const stack = (err.stack ?? '').split('\n').filter((l: string) => l.trim());
|
|
142
|
+
BlackBox.push({
|
|
143
|
+
type: 'error',
|
|
144
|
+
message: err.message,
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
stack,
|
|
147
|
+
isFatal,
|
|
148
|
+
metadata,
|
|
149
|
+
});
|
|
150
|
+
// Also feed into the feedback SDK error buffer
|
|
151
|
+
YaverFeedback.attachError(err, metadata);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Navigation ──────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/** Record a screen/route navigation event. */
|
|
157
|
+
static navigation(route: string, prevRoute?: string, metadata?: Record<string, unknown>): void {
|
|
158
|
+
BlackBox.push({
|
|
159
|
+
type: 'navigation',
|
|
160
|
+
message: `Navigate: ${prevRoute ? prevRoute + ' -> ' : ''}${route}`,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
route,
|
|
163
|
+
prevRoute,
|
|
164
|
+
metadata,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Lifecycle ───────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Record an app lifecycle event (mount, unmount, background, foreground). */
|
|
171
|
+
static lifecycle(event: string, metadata?: Record<string, unknown>): void {
|
|
172
|
+
BlackBox.push({ type: 'lifecycle', message: event, timestamp: Date.now(), metadata });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Network ─────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/** Record a network request/response. */
|
|
178
|
+
static networkRequest(
|
|
179
|
+
method: string,
|
|
180
|
+
url: string,
|
|
181
|
+
status?: number,
|
|
182
|
+
durationMs?: number,
|
|
183
|
+
metadata?: Record<string, unknown>,
|
|
184
|
+
): void {
|
|
185
|
+
const msg = status != null
|
|
186
|
+
? `${method} ${url} → ${status}`
|
|
187
|
+
: `${method} ${url}`;
|
|
188
|
+
BlackBox.push({
|
|
189
|
+
type: 'network',
|
|
190
|
+
message: msg,
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
duration: durationMs,
|
|
193
|
+
metadata,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── State ───────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/** Record a state change event (Redux action, context update, etc.). */
|
|
200
|
+
static stateChange(description: string, metadata?: Record<string, unknown>): void {
|
|
201
|
+
BlackBox.push({ type: 'state', message: description, timestamp: Date.now(), metadata });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Render ──────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/** Record a render/re-render event with optional duration. */
|
|
207
|
+
static render(component: string, durationMs?: number, metadata?: Record<string, unknown>): void {
|
|
208
|
+
BlackBox.push({
|
|
209
|
+
type: 'render',
|
|
210
|
+
message: component,
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
duration: durationMs,
|
|
213
|
+
metadata,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Console wrapping (opt-in) ───────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Wrap `console.log`, `console.warn`, and `console.error` to also
|
|
221
|
+
* stream them to the black box. **Call this explicitly** — the SDK
|
|
222
|
+
* never auto-hooks console.
|
|
223
|
+
*
|
|
224
|
+
* Call `BlackBox.unwrapConsole()` to restore originals.
|
|
225
|
+
*/
|
|
226
|
+
static wrapConsole(): void {
|
|
227
|
+
if (BlackBox.originalConsole) return; // Already wrapped
|
|
228
|
+
BlackBox.originalConsole = {
|
|
229
|
+
log: console.log,
|
|
230
|
+
warn: console.warn,
|
|
231
|
+
error: console.error,
|
|
232
|
+
};
|
|
233
|
+
console.log = (...args: unknown[]) => {
|
|
234
|
+
BlackBox.originalConsole!.log(...args);
|
|
235
|
+
BlackBox.log(args.map(String).join(' '));
|
|
236
|
+
};
|
|
237
|
+
console.warn = (...args: unknown[]) => {
|
|
238
|
+
BlackBox.originalConsole!.warn(...args);
|
|
239
|
+
BlackBox.warn(args.map(String).join(' '));
|
|
240
|
+
};
|
|
241
|
+
console.error = (...args: unknown[]) => {
|
|
242
|
+
BlackBox.originalConsole!.error(...args);
|
|
243
|
+
BlackBox.error(args.map(String).join(' '));
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Restore original console methods. */
|
|
248
|
+
static unwrapConsole(): void {
|
|
249
|
+
if (!BlackBox.originalConsole) return;
|
|
250
|
+
console.log = BlackBox.originalConsole.log;
|
|
251
|
+
console.warn = BlackBox.originalConsole.warn;
|
|
252
|
+
console.error = BlackBox.originalConsole.error;
|
|
253
|
+
BlackBox.originalConsole = null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Error handler wrapper (opt-in) ──────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns a pass-through error handler that streams errors to the
|
|
260
|
+
* black box AND calls the next handler. Same pattern as
|
|
261
|
+
* `YaverFeedback.wrapErrorHandler`, but streams in real-time.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* const existing = ErrorUtils.getGlobalHandler();
|
|
265
|
+
* ErrorUtils.setGlobalHandler(BlackBox.wrapErrorHandler(existing));
|
|
266
|
+
*/
|
|
267
|
+
static wrapErrorHandler(
|
|
268
|
+
next?: ((error: Error, isFatal?: boolean) => void) | null,
|
|
269
|
+
): (error: Error, isFatal?: boolean) => void {
|
|
270
|
+
return (error: Error, isFatal?: boolean) => {
|
|
271
|
+
BlackBox.captureError(error, isFatal ?? false);
|
|
272
|
+
next?.(error, isFatal);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Internal ────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
private static push(event: BlackBoxEvent): void {
|
|
279
|
+
if (!BlackBox.started) return;
|
|
280
|
+
BlackBox.buffer.push(event);
|
|
281
|
+
if (BlackBox.buffer.length >= BlackBox.maxBufferSize) {
|
|
282
|
+
BlackBox.flush();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private static async flush(): Promise<void> {
|
|
287
|
+
if (!BlackBox.baseUrl || !BlackBox.authToken || BlackBox.buffer.length === 0) return;
|
|
288
|
+
|
|
289
|
+
const events = BlackBox.buffer;
|
|
290
|
+
BlackBox.buffer = [];
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await fetch(`${BlackBox.baseUrl}/blackbox/events`, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: {
|
|
296
|
+
Authorization: `Bearer ${BlackBox.authToken}`,
|
|
297
|
+
'Content-Type': 'application/json',
|
|
298
|
+
'X-Device-ID': BlackBox.deviceId,
|
|
299
|
+
'X-Platform': Platform.OS,
|
|
300
|
+
'X-App-Name': BlackBox.appName,
|
|
301
|
+
},
|
|
302
|
+
body: JSON.stringify(events),
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
// Re-add failed events to buffer (capped to avoid memory growth)
|
|
306
|
+
if (BlackBox.buffer.length + events.length <= BlackBox.maxBufferSize * 2) {
|
|
307
|
+
BlackBox.buffer = [...events, ...BlackBox.buffer];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private static generateDeviceId(): string {
|
|
313
|
+
return 'xxxxxxxx'.replace(/x/g, () =>
|
|
314
|
+
Math.floor(Math.random() * 16).toString(16),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|