yo-bug 0.1.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/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/cli.ts +17 -0
- package/bin/install.ts +34 -0
- package/bin/mcp.ts +2 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +19 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/install.d.ts +3 -0
- package/dist/bin/install.d.ts.map +1 -0
- package/dist/bin/install.js +28 -0
- package/dist/bin/install.js.map +1 -0
- package/dist/bin/mcp.d.ts +3 -0
- package/dist/bin/mcp.d.ts.map +1 -0
- package/dist/bin/mcp.js +3 -0
- package/dist/bin/mcp.js.map +1 -0
- package/dist/src/detect/dev-server.d.ts +11 -0
- package/dist/src/detect/dev-server.d.ts.map +1 -0
- package/dist/src/detect/dev-server.js +92 -0
- package/dist/src/detect/dev-server.js.map +1 -0
- package/dist/src/detect/spawn-dev.d.ts +4 -0
- package/dist/src/detect/spawn-dev.d.ts.map +1 -0
- package/dist/src/detect/spawn-dev.js +37 -0
- package/dist/src/detect/spawn-dev.js.map +1 -0
- package/dist/src/install/detect-ai-tool.d.ts +7 -0
- package/dist/src/install/detect-ai-tool.d.ts.map +1 -0
- package/dist/src/install/detect-ai-tool.js +38 -0
- package/dist/src/install/detect-ai-tool.js.map +1 -0
- package/dist/src/install/write-mcp-config.d.ts +6 -0
- package/dist/src/install/write-mcp-config.d.ts.map +1 -0
- package/dist/src/install/write-mcp-config.js +47 -0
- package/dist/src/install/write-mcp-config.js.map +1 -0
- package/dist/src/mcp/index.d.ts +2 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +263 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/tools/checklist.d.ts +22 -0
- package/dist/src/mcp/tools/checklist.d.ts.map +1 -0
- package/dist/src/mcp/tools/checklist.js +58 -0
- package/dist/src/mcp/tools/checklist.js.map +1 -0
- package/dist/src/mcp/tools/get-feedback.d.ts +13 -0
- package/dist/src/mcp/tools/get-feedback.d.ts.map +1 -0
- package/dist/src/mcp/tools/get-feedback.js +27 -0
- package/dist/src/mcp/tools/get-feedback.js.map +1 -0
- package/dist/src/mcp/tools/list-feedbacks.d.ts +20 -0
- package/dist/src/mcp/tools/list-feedbacks.d.ts.map +1 -0
- package/dist/src/mcp/tools/list-feedbacks.js +29 -0
- package/dist/src/mcp/tools/list-feedbacks.js.map +1 -0
- package/dist/src/mcp/tools/resolve-feedback.d.ts +8 -0
- package/dist/src/mcp/tools/resolve-feedback.d.ts.map +1 -0
- package/dist/src/mcp/tools/resolve-feedback.js +28 -0
- package/dist/src/mcp/tools/resolve-feedback.js.map +1 -0
- package/dist/src/mcp/tools/start-session.d.ts +11 -0
- package/dist/src/mcp/tools/start-session.d.ts.map +1 -0
- package/dist/src/mcp/tools/start-session.js +56 -0
- package/dist/src/mcp/tools/start-session.js.map +1 -0
- package/dist/src/mcp/tools/stop-session.d.ts +6 -0
- package/dist/src/mcp/tools/stop-session.d.ts.map +1 -0
- package/dist/src/mcp/tools/stop-session.js +80 -0
- package/dist/src/mcp/tools/stop-session.js.map +1 -0
- package/dist/src/mcp/tools/test-history.d.ts +33 -0
- package/dist/src/mcp/tools/test-history.d.ts.map +1 -0
- package/dist/src/mcp/tools/test-history.js +66 -0
- package/dist/src/mcp/tools/test-history.js.map +1 -0
- package/dist/src/server/feedback-api.d.ts +4 -0
- package/dist/src/server/feedback-api.d.ts.map +1 -0
- package/dist/src/server/feedback-api.js +152 -0
- package/dist/src/server/feedback-api.js.map +1 -0
- package/dist/src/server/html-injector.d.ts +10 -0
- package/dist/src/server/html-injector.d.ts.map +1 -0
- package/dist/src/server/html-injector.js +34 -0
- package/dist/src/server/html-injector.js.map +1 -0
- package/dist/src/server/proxy-server.d.ts +8 -0
- package/dist/src/server/proxy-server.d.ts.map +1 -0
- package/dist/src/server/proxy-server.js +87 -0
- package/dist/src/server/proxy-server.js.map +1 -0
- package/dist/src/server/sdk-serve.d.ts +3 -0
- package/dist/src/server/sdk-serve.d.ts.map +1 -0
- package/dist/src/server/sdk-serve.js +20 -0
- package/dist/src/server/sdk-serve.js.map +1 -0
- package/dist/src/storage/store.d.ts +21 -0
- package/dist/src/storage/store.d.ts.map +1 -0
- package/dist/src/storage/store.js +138 -0
- package/dist/src/storage/store.js.map +1 -0
- package/dist/src/storage/types.d.ts +74 -0
- package/dist/src/storage/types.d.ts.map +1 -0
- package/dist/src/storage/types.js +2 -0
- package/dist/src/storage/types.js.map +1 -0
- package/dist/vibe-feedback.js +399 -0
- package/package.json +67 -0
- package/src/client/annotation-mode/canvas-editor.ts +178 -0
- package/src/client/annotation-mode/history.ts +32 -0
- package/src/client/annotation-mode/region-selector.ts +123 -0
- package/src/client/annotation-mode/screenshot.ts +17 -0
- package/src/client/annotation-mode/toolbar.ts +139 -0
- package/src/client/annotation-mode/tools/arrow.ts +57 -0
- package/src/client/annotation-mode/tools/base-tool.ts +25 -0
- package/src/client/annotation-mode/tools/circle.ts +37 -0
- package/src/client/annotation-mode/tools/freehand.ts +23 -0
- package/src/client/annotation-mode/tools/rect.ts +32 -0
- package/src/client/annotation-mode/tools/text.ts +93 -0
- package/src/client/api/client.ts +48 -0
- package/src/client/capture/action-recorder.ts +157 -0
- package/src/client/capture/console-interceptor.ts +65 -0
- package/src/client/capture/context-buffer.ts +23 -0
- package/src/client/capture/error-interceptor.ts +52 -0
- package/src/client/capture/network-interceptor.ts +143 -0
- package/src/client/core/event-bus.ts +20 -0
- package/src/client/core/i18n.ts +83 -0
- package/src/client/core/sdk.ts +373 -0
- package/src/client/core/shadow-host.ts +27 -0
- package/src/client/element-mode/highlighter.ts +79 -0
- package/src/client/element-mode/inspector.ts +73 -0
- package/src/client/element-mode/selector.ts +22 -0
- package/src/client/index.ts +10 -0
- package/src/client/styles/sdk.css.ts +222 -0
- package/src/client/ui/checklist-panel.ts +279 -0
- package/src/client/ui/feedback-panel.ts +149 -0
- package/src/client/ui/floating-button.ts +103 -0
- package/src/client/ui/toast.ts +17 -0
- package/src/client/ui/verify-panel.ts +111 -0
- package/src/detect/dev-server.ts +110 -0
- package/src/detect/spawn-dev.ts +50 -0
- package/src/install/detect-ai-tool.ts +49 -0
- package/src/install/write-mcp-config.ts +49 -0
- package/src/mcp/index.ts +327 -0
- package/src/mcp/tools/checklist.ts +61 -0
- package/src/mcp/tools/get-feedback.ts +34 -0
- package/src/mcp/tools/list-feedbacks.ts +37 -0
- package/src/mcp/tools/resolve-feedback.ts +34 -0
- package/src/mcp/tools/start-session.ts +65 -0
- package/src/mcp/tools/stop-session.ts +93 -0
- package/src/mcp/tools/test-history.ts +97 -0
- package/src/server/feedback-api.ts +164 -0
- package/src/server/html-injector.ts +41 -0
- package/src/server/proxy-server.ts +107 -0
- package/src/server/sdk-serve.ts +24 -0
- package/src/storage/store.ts +172 -0
- package/src/storage/types.ts +68 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseTool } from './base-tool.js';
|
|
2
|
+
|
|
3
|
+
export class FreehandTool extends BaseTool {
|
|
4
|
+
onMouseDown(x: number, y: number): void {
|
|
5
|
+
this.drawing = true;
|
|
6
|
+
this.ctx.beginPath();
|
|
7
|
+
this.ctx.moveTo(x, y);
|
|
8
|
+
this.ctx.strokeStyle = this.color;
|
|
9
|
+
this.ctx.lineWidth = this.lineWidth;
|
|
10
|
+
this.ctx.lineCap = 'round';
|
|
11
|
+
this.ctx.lineJoin = 'round';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onMouseMove(x: number, y: number): void {
|
|
15
|
+
if (!this.drawing) return;
|
|
16
|
+
this.ctx.lineTo(x, y);
|
|
17
|
+
this.ctx.stroke();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onMouseUp(_x: number, _y: number): void {
|
|
21
|
+
this.drawing = false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseTool } from './base-tool.js';
|
|
2
|
+
|
|
3
|
+
export class RectTool extends BaseTool {
|
|
4
|
+
private startX = 0;
|
|
5
|
+
private startY = 0;
|
|
6
|
+
private snapshot: ImageData | null = null;
|
|
7
|
+
|
|
8
|
+
onMouseDown(x: number, y: number): void {
|
|
9
|
+
this.drawing = true;
|
|
10
|
+
this.startX = x;
|
|
11
|
+
this.startY = y;
|
|
12
|
+
this.snapshot = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
onMouseMove(x: number, y: number): void {
|
|
16
|
+
if (!this.drawing || !this.snapshot) return;
|
|
17
|
+
this.ctx.putImageData(this.snapshot, 0, 0);
|
|
18
|
+
this.ctx.strokeStyle = this.color;
|
|
19
|
+
this.ctx.lineWidth = this.lineWidth;
|
|
20
|
+
this.ctx.strokeRect(
|
|
21
|
+
this.startX, this.startY,
|
|
22
|
+
x - this.startX, y - this.startY
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onMouseUp(x: number, y: number): void {
|
|
27
|
+
if (!this.drawing) return;
|
|
28
|
+
this.onMouseMove(x, y);
|
|
29
|
+
this.drawing = false;
|
|
30
|
+
this.snapshot = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { BaseTool } from './base-tool.js';
|
|
2
|
+
|
|
3
|
+
export class TextTool extends BaseTool {
|
|
4
|
+
private input: HTMLInputElement | null = null;
|
|
5
|
+
private container: HTMLElement;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
ctx: CanvasRenderingContext2D,
|
|
9
|
+
color: string,
|
|
10
|
+
lineWidth: number,
|
|
11
|
+
container: HTMLElement
|
|
12
|
+
) {
|
|
13
|
+
super(ctx, color, lineWidth);
|
|
14
|
+
this.container = container;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onMouseDown(x: number, y: number, cssX?: number, cssY?: number): void {
|
|
18
|
+
this.removeInput();
|
|
19
|
+
|
|
20
|
+
// Use CSS coordinates for input positioning, canvas coordinates for drawing
|
|
21
|
+
const posX = cssX ?? x;
|
|
22
|
+
const posY = cssY ?? y;
|
|
23
|
+
|
|
24
|
+
this.input = document.createElement('input');
|
|
25
|
+
this.input.type = 'text';
|
|
26
|
+
this.input.placeholder = 'Type here...';
|
|
27
|
+
this.input.style.cssText = `
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: ${posX}px;
|
|
30
|
+
top: ${posY - 12}px;
|
|
31
|
+
min-width: 120px;
|
|
32
|
+
background: rgba(0,0,0,0.8);
|
|
33
|
+
border: 2px solid ${this.color};
|
|
34
|
+
color: ${this.color};
|
|
35
|
+
font-size: 16px;
|
|
36
|
+
padding: 4px 8px;
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
outline: none;
|
|
39
|
+
z-index: 10;
|
|
40
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
// Prevent canvas editor from receiving keyboard events
|
|
44
|
+
this.input.addEventListener('keydown', (e) => {
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
if (e.key === 'Enter') {
|
|
47
|
+
this.commitText(x, y);
|
|
48
|
+
} else if (e.key === 'Escape') {
|
|
49
|
+
this.removeInput();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.input.addEventListener('blur', () => {
|
|
54
|
+
this.commitText(x, y);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.container.appendChild(this.input);
|
|
58
|
+
// Focus after a tick to ensure it's in the DOM
|
|
59
|
+
requestAnimationFrame(() => this.input?.focus());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onMouseMove(): void {}
|
|
63
|
+
onMouseUp(): void {}
|
|
64
|
+
|
|
65
|
+
private commitText(canvasX: number, canvasY: number): void {
|
|
66
|
+
if (!this.input) return;
|
|
67
|
+
const text = this.input.value.trim();
|
|
68
|
+
if (text) {
|
|
69
|
+
const fontSize = Math.max(18, this.lineWidth * 6);
|
|
70
|
+
this.ctx.font = `bold ${fontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`;
|
|
71
|
+
this.ctx.fillStyle = this.color;
|
|
72
|
+
// Draw shadow for readability
|
|
73
|
+
this.ctx.shadowColor = 'rgba(0,0,0,0.5)';
|
|
74
|
+
this.ctx.shadowBlur = 3;
|
|
75
|
+
this.ctx.shadowOffsetX = 1;
|
|
76
|
+
this.ctx.shadowOffsetY = 1;
|
|
77
|
+
this.ctx.fillText(text, canvasX, canvasY);
|
|
78
|
+
// Reset shadow
|
|
79
|
+
this.ctx.shadowColor = 'transparent';
|
|
80
|
+
this.ctx.shadowBlur = 0;
|
|
81
|
+
this.ctx.shadowOffsetX = 0;
|
|
82
|
+
this.ctx.shadowOffsetY = 0;
|
|
83
|
+
}
|
|
84
|
+
this.removeInput();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private removeInput(): void {
|
|
88
|
+
if (this.input) {
|
|
89
|
+
this.input.remove();
|
|
90
|
+
this.input = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface FeedbackPayload {
|
|
2
|
+
mode: 'element' | 'annotation';
|
|
3
|
+
problemType: string;
|
|
4
|
+
description: string;
|
|
5
|
+
element?: any;
|
|
6
|
+
pageUrl: string;
|
|
7
|
+
pageTitle: string;
|
|
8
|
+
viewport: { width: number; height: number };
|
|
9
|
+
userAgent: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
consoleErrors: any[];
|
|
12
|
+
networkErrors: any[];
|
|
13
|
+
unhandledErrors: any[];
|
|
14
|
+
actionSteps?: any[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ApiClient {
|
|
18
|
+
private baseUrl: string;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
// SDK is served from the proxy, so use same origin
|
|
22
|
+
this.baseUrl = window.location.origin;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async submitFeedback(
|
|
26
|
+
payload: FeedbackPayload,
|
|
27
|
+
screenshotDataUrl?: string
|
|
28
|
+
): Promise<{ id: string }> {
|
|
29
|
+
const formData = new FormData();
|
|
30
|
+
formData.append('feedback', JSON.stringify(payload));
|
|
31
|
+
|
|
32
|
+
if (screenshotDataUrl) {
|
|
33
|
+
const blob = await (await fetch(screenshotDataUrl)).blob();
|
|
34
|
+
formData.append('screenshot', blob, 'annotation.png');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${this.baseUrl}/api/feedback`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: formData,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`Submit failed: ${res.status}`);
|
|
44
|
+
}
|
|
45
|
+
return res.json();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export interface ActionStep {
|
|
2
|
+
type: 'click' | 'input' | 'navigate' | 'scroll' | 'keypress';
|
|
3
|
+
timestamp: number;
|
|
4
|
+
target?: string; // CSS selector
|
|
5
|
+
tagName?: string;
|
|
6
|
+
text?: string; // button text / input value / key
|
|
7
|
+
url?: string; // for navigate
|
|
8
|
+
position?: { x: number; y: number };
|
|
9
|
+
elapsed?: number; // ms since last step
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ActionRecorder {
|
|
13
|
+
private steps: ActionStep[] = [];
|
|
14
|
+
private recording = false;
|
|
15
|
+
private lastTimestamp = 0;
|
|
16
|
+
private maxSteps = 100;
|
|
17
|
+
|
|
18
|
+
// Bound handlers
|
|
19
|
+
private clickHandler: (e: MouseEvent) => void;
|
|
20
|
+
private inputHandler: (e: Event) => void;
|
|
21
|
+
private keyHandler: (e: KeyboardEvent) => void;
|
|
22
|
+
private navHandler: () => void;
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
this.clickHandler = (e) => this.onClick(e);
|
|
26
|
+
this.inputHandler = (e) => this.onInput(e);
|
|
27
|
+
this.keyHandler = (e) => this.onKeypress(e);
|
|
28
|
+
this.navHandler = () => this.onNavigate();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start(): void {
|
|
32
|
+
if (this.recording) return;
|
|
33
|
+
this.recording = true;
|
|
34
|
+
this.steps = [];
|
|
35
|
+
this.lastTimestamp = Date.now();
|
|
36
|
+
|
|
37
|
+
document.addEventListener('click', this.clickHandler, true);
|
|
38
|
+
document.addEventListener('change', this.inputHandler, true);
|
|
39
|
+
document.addEventListener('keydown', this.keyHandler, true);
|
|
40
|
+
window.addEventListener('popstate', this.navHandler);
|
|
41
|
+
window.addEventListener('hashchange', this.navHandler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
stop(): ActionStep[] {
|
|
45
|
+
this.recording = false;
|
|
46
|
+
document.removeEventListener('click', this.clickHandler, true);
|
|
47
|
+
document.removeEventListener('change', this.inputHandler, true);
|
|
48
|
+
document.removeEventListener('keydown', this.keyHandler, true);
|
|
49
|
+
window.removeEventListener('popstate', this.navHandler);
|
|
50
|
+
window.removeEventListener('hashchange', this.navHandler);
|
|
51
|
+
return this.getSteps();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getSteps(): ActionStep[] {
|
|
55
|
+
return [...this.steps];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isRecording(): boolean {
|
|
59
|
+
return this.recording;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private addStep(step: ActionStep): void {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
step.elapsed = now - this.lastTimestamp;
|
|
65
|
+
this.lastTimestamp = now;
|
|
66
|
+
this.steps.push(step);
|
|
67
|
+
if (this.steps.length > this.maxSteps) this.steps.shift();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private onClick(e: MouseEvent): void {
|
|
71
|
+
const el = e.target as HTMLElement;
|
|
72
|
+
if (!el || this.isSDKElement(el)) return;
|
|
73
|
+
|
|
74
|
+
this.addStep({
|
|
75
|
+
type: 'click',
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
target: briefSelector(el),
|
|
78
|
+
tagName: el.tagName.toLowerCase(),
|
|
79
|
+
text: getElementText(el),
|
|
80
|
+
position: { x: Math.round(e.clientX), y: Math.round(e.clientY) },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private onInput(e: Event): void {
|
|
85
|
+
const el = e.target as HTMLInputElement;
|
|
86
|
+
if (!el || this.isSDKElement(el)) return;
|
|
87
|
+
|
|
88
|
+
const value = el.type === 'password' ? '••••' : (el.value || '').slice(0, 50);
|
|
89
|
+
this.addStep({
|
|
90
|
+
type: 'input',
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
target: briefSelector(el),
|
|
93
|
+
tagName: el.tagName.toLowerCase(),
|
|
94
|
+
text: value,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private onKeypress(e: KeyboardEvent): void {
|
|
99
|
+
// Only record special keys, not every character
|
|
100
|
+
if (!['Enter', 'Escape', 'Tab', 'Backspace', 'Delete'].includes(e.key) &&
|
|
101
|
+
!e.ctrlKey && !e.metaKey && !e.altKey) return;
|
|
102
|
+
|
|
103
|
+
const el = e.target as HTMLElement;
|
|
104
|
+
if (this.isSDKElement(el)) return;
|
|
105
|
+
|
|
106
|
+
const combo = [
|
|
107
|
+
e.ctrlKey ? 'Ctrl' : '',
|
|
108
|
+
e.metaKey ? 'Cmd' : '',
|
|
109
|
+
e.altKey ? 'Alt' : '',
|
|
110
|
+
e.shiftKey ? 'Shift' : '',
|
|
111
|
+
e.key,
|
|
112
|
+
].filter(Boolean).join('+');
|
|
113
|
+
|
|
114
|
+
this.addStep({
|
|
115
|
+
type: 'keypress',
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
target: briefSelector(el),
|
|
118
|
+
text: combo,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private onNavigate(): void {
|
|
123
|
+
this.addStep({
|
|
124
|
+
type: 'navigate',
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
url: window.location.href,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private isSDKElement(el: Element): boolean {
|
|
131
|
+
let current: Element | null = el;
|
|
132
|
+
while (current) {
|
|
133
|
+
if (current.id === 'vibe-feedback-host') return true;
|
|
134
|
+
current = current.parentElement;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function briefSelector(el: Element): string {
|
|
141
|
+
if (el.id) return `#${el.id}`;
|
|
142
|
+
const testId = el.getAttribute('data-testid');
|
|
143
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
144
|
+
const cls = el.className && typeof el.className === 'string'
|
|
145
|
+
? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.')
|
|
146
|
+
: '';
|
|
147
|
+
return `${el.tagName.toLowerCase()}${cls}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getElementText(el: HTMLElement): string {
|
|
151
|
+
// Get meaningful text: button label, link text, placeholder, etc.
|
|
152
|
+
const text = el.textContent?.trim() ||
|
|
153
|
+
el.getAttribute('aria-label') ||
|
|
154
|
+
el.getAttribute('placeholder') ||
|
|
155
|
+
el.getAttribute('title') || '';
|
|
156
|
+
return text.slice(0, 60);
|
|
157
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ContextBuffer } from './context-buffer.js';
|
|
2
|
+
|
|
3
|
+
export interface ConsoleEntry {
|
|
4
|
+
type: 'error' | 'warn';
|
|
5
|
+
message: string;
|
|
6
|
+
stack?: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ConsoleInterceptor {
|
|
11
|
+
private origError!: typeof console.error;
|
|
12
|
+
private origWarn!: typeof console.warn;
|
|
13
|
+
private buffer: ContextBuffer<ConsoleEntry>;
|
|
14
|
+
private installed = false;
|
|
15
|
+
|
|
16
|
+
constructor(buffer: ContextBuffer<ConsoleEntry>) {
|
|
17
|
+
this.buffer = buffer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
install(): void {
|
|
21
|
+
if (this.installed) return;
|
|
22
|
+
this.installed = true;
|
|
23
|
+
|
|
24
|
+
this.origError = console.error;
|
|
25
|
+
this.origWarn = console.warn;
|
|
26
|
+
|
|
27
|
+
console.error = (...args: any[]) => {
|
|
28
|
+
this.buffer.push({
|
|
29
|
+
type: 'error',
|
|
30
|
+
message: args.map(serialize).join(' '),
|
|
31
|
+
stack: new Error().stack,
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
});
|
|
34
|
+
this.origError.apply(console, args);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
console.warn = (...args: any[]) => {
|
|
38
|
+
this.buffer.push({
|
|
39
|
+
type: 'warn',
|
|
40
|
+
message: args.map(serialize).join(' '),
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
this.origWarn.apply(console, args);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
uninstall(): void {
|
|
48
|
+
if (!this.installed) return;
|
|
49
|
+
console.error = this.origError;
|
|
50
|
+
console.warn = this.origWarn;
|
|
51
|
+
this.installed = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function serialize(arg: any): string {
|
|
56
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`;
|
|
57
|
+
if (typeof arg === 'object') {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.stringify(arg, null, 0).slice(0, 500);
|
|
60
|
+
} catch {
|
|
61
|
+
return String(arg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return String(arg);
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class ContextBuffer<T> {
|
|
2
|
+
private items: T[] = [];
|
|
3
|
+
private maxSize: number;
|
|
4
|
+
|
|
5
|
+
constructor(maxSize = 50) {
|
|
6
|
+
this.maxSize = maxSize;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
push(item: T): void {
|
|
10
|
+
this.items.push(item);
|
|
11
|
+
if (this.items.length > this.maxSize) {
|
|
12
|
+
this.items.shift();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getAll(): T[] {
|
|
17
|
+
return [...this.items];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clear(): void {
|
|
21
|
+
this.items = [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ContextBuffer } from './context-buffer.js';
|
|
2
|
+
|
|
3
|
+
export interface ErrorEntry {
|
|
4
|
+
type: 'unhandled-error' | 'unhandled-rejection';
|
|
5
|
+
message: string;
|
|
6
|
+
stack?: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ErrorInterceptor {
|
|
11
|
+
private buffer: ContextBuffer<ErrorEntry>;
|
|
12
|
+
private errorHandler!: (e: ErrorEvent) => void;
|
|
13
|
+
private rejectionHandler!: (e: PromiseRejectionEvent) => void;
|
|
14
|
+
private installed = false;
|
|
15
|
+
|
|
16
|
+
constructor(buffer: ContextBuffer<ErrorEntry>) {
|
|
17
|
+
this.buffer = buffer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
install(): void {
|
|
21
|
+
if (this.installed) return;
|
|
22
|
+
this.installed = true;
|
|
23
|
+
|
|
24
|
+
this.errorHandler = (e: ErrorEvent) => {
|
|
25
|
+
this.buffer.push({
|
|
26
|
+
type: 'unhandled-error',
|
|
27
|
+
message: e.message,
|
|
28
|
+
stack: e.error?.stack,
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.rejectionHandler = (e: PromiseRejectionEvent) => {
|
|
34
|
+
this.buffer.push({
|
|
35
|
+
type: 'unhandled-rejection',
|
|
36
|
+
message: String(e.reason),
|
|
37
|
+
stack: e.reason?.stack,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
window.addEventListener('error', this.errorHandler);
|
|
43
|
+
window.addEventListener('unhandledrejection', this.rejectionHandler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
uninstall(): void {
|
|
47
|
+
if (!this.installed) return;
|
|
48
|
+
window.removeEventListener('error', this.errorHandler);
|
|
49
|
+
window.removeEventListener('unhandledrejection', this.rejectionHandler);
|
|
50
|
+
this.installed = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ContextBuffer } from './context-buffer.js';
|
|
2
|
+
|
|
3
|
+
export interface NetworkEntry {
|
|
4
|
+
type: 'fetch' | 'xhr';
|
|
5
|
+
method: string;
|
|
6
|
+
url: string;
|
|
7
|
+
status: number;
|
|
8
|
+
statusText: string;
|
|
9
|
+
duration: number;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Check if a URL belongs to the vibe-feedback SDK itself */
|
|
15
|
+
function isVibeFeedbackUrl(url: string): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const u = new URL(url, window.location.origin);
|
|
18
|
+
return u.pathname === '/api/feedback' ||
|
|
19
|
+
u.pathname.startsWith('/api/feedback/') ||
|
|
20
|
+
u.pathname === '/api/checklist' ||
|
|
21
|
+
u.pathname.startsWith('/api/checklist/') ||
|
|
22
|
+
u.pathname === '/api/inspection' ||
|
|
23
|
+
u.pathname === '/vibe-feedback.js';
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class NetworkInterceptor {
|
|
30
|
+
private origFetch!: typeof window.fetch;
|
|
31
|
+
private origXHROpen!: typeof XMLHttpRequest.prototype.open;
|
|
32
|
+
private origXHRSend!: typeof XMLHttpRequest.prototype.send;
|
|
33
|
+
private buffer: ContextBuffer<NetworkEntry>;
|
|
34
|
+
private installed = false;
|
|
35
|
+
|
|
36
|
+
constructor(buffer: ContextBuffer<NetworkEntry>) {
|
|
37
|
+
this.buffer = buffer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
install(): void {
|
|
41
|
+
if (this.installed) return;
|
|
42
|
+
this.installed = true;
|
|
43
|
+
this.patchFetch();
|
|
44
|
+
this.patchXHR();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
uninstall(): void {
|
|
48
|
+
if (!this.installed) return;
|
|
49
|
+
window.fetch = this.origFetch;
|
|
50
|
+
XMLHttpRequest.prototype.open = this.origXHROpen;
|
|
51
|
+
XMLHttpRequest.prototype.send = this.origXHRSend;
|
|
52
|
+
this.installed = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private patchFetch(): void {
|
|
56
|
+
this.origFetch = window.fetch;
|
|
57
|
+
const buffer = this.buffer;
|
|
58
|
+
const origFetch = this.origFetch;
|
|
59
|
+
|
|
60
|
+
window.fetch = async function (input, init?) {
|
|
61
|
+
const url =
|
|
62
|
+
typeof input === 'string'
|
|
63
|
+
? input
|
|
64
|
+
: input instanceof URL
|
|
65
|
+
? input.href
|
|
66
|
+
: input instanceof Request
|
|
67
|
+
? input.url
|
|
68
|
+
: String(input);
|
|
69
|
+
const method = init?.method || 'GET';
|
|
70
|
+
|
|
71
|
+
// Skip our own requests
|
|
72
|
+
if (isVibeFeedbackUrl(url)) {
|
|
73
|
+
return origFetch.call(window, input, init);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
try {
|
|
78
|
+
const response = await origFetch.call(window, input, init);
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
buffer.push({
|
|
81
|
+
type: 'fetch',
|
|
82
|
+
method,
|
|
83
|
+
url: url.slice(0, 200),
|
|
84
|
+
status: response.status,
|
|
85
|
+
statusText: response.statusText,
|
|
86
|
+
duration: Date.now() - start,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return response;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
buffer.push({
|
|
93
|
+
type: 'fetch',
|
|
94
|
+
method,
|
|
95
|
+
url: url.slice(0, 200),
|
|
96
|
+
status: 0,
|
|
97
|
+
statusText: 'Network Error',
|
|
98
|
+
duration: Date.now() - start,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
error: String(err),
|
|
101
|
+
});
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private patchXHR(): void {
|
|
108
|
+
this.origXHROpen = XMLHttpRequest.prototype.open;
|
|
109
|
+
this.origXHRSend = XMLHttpRequest.prototype.send;
|
|
110
|
+
const buffer = this.buffer;
|
|
111
|
+
const origOpen = this.origXHROpen;
|
|
112
|
+
const origSend = this.origXHRSend;
|
|
113
|
+
|
|
114
|
+
XMLHttpRequest.prototype.open = function (method: string, url: string | URL, ...rest: any[]) {
|
|
115
|
+
(this as any).__vf_method = method;
|
|
116
|
+
(this as any).__vf_url = typeof url === 'string' ? url : url.href;
|
|
117
|
+
return origOpen.apply(this, [method, url, ...rest] as any);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
XMLHttpRequest.prototype.send = function (body?: any) {
|
|
121
|
+
const url = (this as any).__vf_url || '';
|
|
122
|
+
if (isVibeFeedbackUrl(url)) {
|
|
123
|
+
return origSend.call(this, body);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const start = Date.now();
|
|
127
|
+
this.addEventListener('loadend', () => {
|
|
128
|
+
if (this.status >= 400 || this.status === 0) {
|
|
129
|
+
buffer.push({
|
|
130
|
+
type: 'xhr',
|
|
131
|
+
method: (this as any).__vf_method || 'GET',
|
|
132
|
+
url: url.slice(0, 200),
|
|
133
|
+
status: this.status,
|
|
134
|
+
statusText: this.statusText,
|
|
135
|
+
duration: Date.now() - start,
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return origSend.call(this, body);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type Handler = (...args: any[]) => void;
|
|
2
|
+
|
|
3
|
+
export class EventBus {
|
|
4
|
+
private handlers = new Map<string, Set<Handler>>();
|
|
5
|
+
|
|
6
|
+
on(event: string, handler: Handler): void {
|
|
7
|
+
if (!this.handlers.has(event)) {
|
|
8
|
+
this.handlers.set(event, new Set());
|
|
9
|
+
}
|
|
10
|
+
this.handlers.get(event)!.add(handler);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
off(event: string, handler: Handler): void {
|
|
14
|
+
this.handlers.get(event)?.delete(handler);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
emit(event: string, ...args: any[]): void {
|
|
18
|
+
this.handlers.get(event)?.forEach((h) => h(...args));
|
|
19
|
+
}
|
|
20
|
+
}
|