Siddharth Ravikumar commited on
Commit
73001cc
Β·
1 Parent(s): 4355d54

feat: implement phone-home debugging for frontend errors

Browse files
Files changed (2) hide show
  1. backend/app/api/routes.py +7 -0
  2. frontend/js/alt_app.js +142 -124
backend/app/api/routes.py CHANGED
@@ -442,3 +442,10 @@ async def get_logs(lines: int = Query(100, ge=1, le=1000)):
442
  except Exception as e:
443
  return {"error": str(e)}
444
 
 
 
 
 
 
 
 
 
442
  except Exception as e:
443
  return {"error": str(e)}
444
 
445
+
446
+ @router.post("/debug/report_error")
447
+ async def report_error(error_data: dict):
448
+ """Log a frontend-side error to the server log for debugging."""
449
+ logger.error(f"[FRONTEND-ERROR] {json.dumps(error_data)}")
450
+ return {"status": "ok"}
451
+
frontend/js/alt_app.js CHANGED
@@ -20,159 +20,177 @@ let additionalFiles = []; // For add photos modal
20
  // Uses fetch-based SSE instead of EventSource (which can't set headers
21
  // required by HuggingFace's ZeroGPU proxy).
22
  async function callGradioApi(apiName, dataArr) {
23
- const API_PATHS = ['/gradio_api/call/', '/call/'];
24
- let hfToken = '';
25
-
26
- // Fetch config for ZeroGPU token
27
  try {
28
- const confRes = await fetch(`${API_BASE}/config`);
29
- if (confRes.ok) {
30
- const confData = await confRes.json();
31
- hfToken = confData.hf_token || '';
32
- }
33
- } catch (e) {
34
- console.warn('Failed to fetch config', e);
35
- }
36
-
37
- const headers = { 'Content-Type': 'application/json' };
38
- if (hfToken) headers['Authorization'] = `Bearer ${hfToken}`;
39
 
40
- let lastError = null;
41
- let selectedApiBase = '';
42
- let eventId = '';
43
-
44
- // Step 1: Find working endpoint
45
- for (const apiBase of API_PATHS) {
46
  try {
47
- const queueUrl = apiBase + apiName;
48
- console.log(`[Gradio] Attempting POST: ${queueUrl}`);
 
 
 
 
 
 
49
 
50
- const res = await fetch(queueUrl, {
51
- method: 'POST',
52
- headers: headers,
53
- credentials: 'include',
54
- body: JSON.stringify({ data: dataArr })
55
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- if (res.status === 404) {
58
- console.warn(`[Gradio] 404 at ${queueUrl}, trying next fallback...`);
59
- continue;
60
- }
61
 
62
- if (!res.ok) {
63
- throw new Error(`Gradio API Request Failed (${res.status})`);
 
 
 
 
 
 
64
  }
65
-
66
- const eventObj = await res.json();
67
- eventId = eventObj.event_id;
68
- selectedApiBase = apiBase;
69
- console.log(`[Gradio] Success at ${queueUrl}. Event ID: ${eventId}`);
70
- break;
71
- } catch (e) {
72
- lastError = e;
73
- console.error(`[Gradio] Error at ${apiBase}:`, e);
74
  }
75
- }
76
 
77
- if (!selectedApiBase || !eventId) {
78
- throw lastError || new Error('Gradio API could not be reached (All paths failed)');
79
- }
80
 
81
- // Step 2: Stream the result via fetch (not EventSource)
82
- const streamUrl = selectedApiBase + apiName + '/' + eventId;
83
- console.log(`[Gradio] Streaming from: ${streamUrl}`);
84
 
85
- const streamHeaders = { 'Accept': 'text/event-stream' };
86
- if (hfToken) {
87
- streamHeaders['Authorization'] = `Bearer ${hfToken}`;
88
- }
89
 
90
- const sseRes = await fetch(streamUrl, {
91
- method: 'GET',
92
- headers: streamHeaders,
93
- credentials: 'include',
94
- cache: 'no-cache',
95
- });
96
 
97
- if (!sseRes.ok) {
98
- throw new Error('Gradio SSE Stream Failed (' + sseRes.status + ')');
99
- }
100
 
101
- const reader = sseRes.body.getReader();
102
- const decoder = new TextDecoder();
103
- let buffer = '';
104
 
105
- while (true) {
106
- const { done, value } = await reader.read();
107
 
108
- if (value) {
109
- buffer += decoder.decode(value, { stream: true });
110
- }
111
- if (done) {
112
- buffer += decoder.decode();
113
- if (buffer.trim()) {
114
- buffer += '\n\n';
 
115
  }
116
- }
117
 
118
- // Parse SSE events from buffer (events separated by double newline)
119
- const parts = buffer.split('\n\n');
120
- // Keep the last incomplete chunk in the buffer
121
- buffer = parts.pop();
122
 
123
- for (const part of parts) {
124
- let eventType = '';
125
- const dataLines = [];
126
 
127
- for (const line of part.split('\n')) {
128
- if (line.startsWith('event:')) {
129
- eventType = line.substring(6).trim();
130
- } else if (line.startsWith('data:')) {
131
- dataLines.push(line.substring(5).replace(/^ /, ''));
 
132
  }
133
- }
134
 
135
- const dataLine = dataLines.join('\n');
136
- if (!dataLine && !eventType) continue;
137
-
138
- // ZeroGPU format: event type is "complete" or "error",
139
- // data is a raw JSON array of outputs
140
- if (eventType === 'complete') {
141
- if (!done) reader.cancel();
142
- try {
143
- const result = JSON.parse(dataLine);
144
- return Array.isArray(result) ? result : [result];
145
- } catch {
146
- return [dataLine];
 
 
 
 
 
 
 
147
  }
148
- } else if (eventType === 'error') {
149
- if (!done) reader.cancel();
150
- console.error(`[Gradio] SSE Error event:`, part);
151
- const errorDetail = dataLine === 'null' ? 'Server Error (null)' : dataLine;
152
- throw new Error(`${errorDetail}`);
153
- }
154
- // Also handle the non-ZeroGPU format (Gradio standard)
155
- if (dataLine) {
156
- try {
157
- const msg = JSON.parse(dataLine);
158
- if (msg.msg === 'process_completed') {
159
- if (!done) reader.cancel();
160
- if (msg.success) {
161
- return msg.output.data;
162
- } else {
163
- throw new Error(msg.output?.error || 'Server Error');
164
  }
 
 
165
  }
166
- } catch {
167
- // Not JSON or not the expected format, skip
168
  }
169
  }
170
- }
171
 
172
- if (done) break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
-
175
- throw new Error('Gradio stream ended without a result');
176
  }
177
 
178
  // ── Init ──────────────────────────────────────────────────────────────
 
20
  // Uses fetch-based SSE instead of EventSource (which can't set headers
21
  // required by HuggingFace's ZeroGPU proxy).
22
  async function callGradioApi(apiName, dataArr) {
 
 
 
 
23
  try {
24
+ const API_PATHS = ['/gradio_api/call/', '/call/'];
25
+ let hfToken = '';
 
 
 
 
 
 
 
 
 
26
 
27
+ // Fetch config for ZeroGPU token
 
 
 
 
 
28
  try {
29
+ const confRes = await fetch(`${API_BASE}/config`);
30
+ if (confRes.ok) {
31
+ const confData = await confRes.json();
32
+ hfToken = confData.hf_token || '';
33
+ }
34
+ } catch (e) {
35
+ console.warn('Failed to fetch config', e);
36
+ }
37
 
38
+ const headers = { 'Content-Type': 'application/json' };
39
+ if (hfToken) headers['Authorization'] = `Bearer ${hfToken}`;
40
+
41
+ let lastError = null;
42
+ let selectedApiBase = '';
43
+ let eventId = '';
44
+
45
+ // Step 1: Find working endpoint
46
+ for (const apiBase of API_PATHS) {
47
+ try {
48
+ const queueUrl = apiBase + apiName;
49
+ console.log(`[Gradio] Attempting POST: ${queueUrl}`);
50
+
51
+ const res = await fetch(queueUrl, {
52
+ method: 'POST',
53
+ headers: headers,
54
+ credentials: 'include',
55
+ body: JSON.stringify({ data: dataArr })
56
+ });
57
+
58
+ if (res.status === 404) {
59
+ console.warn(`[Gradio] 404 at ${queueUrl}, trying next fallback...`);
60
+ continue;
61
+ }
62
 
63
+ if (!res.ok) {
64
+ throw new Error(`Gradio API Request Failed (${res.status})`);
65
+ }
 
66
 
67
+ const eventObj = await res.json();
68
+ eventId = eventObj.event_id;
69
+ selectedApiBase = apiBase;
70
+ console.log(`[Gradio] Success at ${queueUrl}. Event ID: ${eventId}`);
71
+ break;
72
+ } catch (e) {
73
+ lastError = e;
74
+ console.error(`[Gradio] Error at ${apiBase}:`, e);
75
  }
 
 
 
 
 
 
 
 
 
76
  }
 
77
 
78
+ if (!selectedApiBase || !eventId) {
79
+ throw lastError || new Error('Gradio API could not be reached (All paths failed)');
80
+ }
81
 
82
+ // Step 2: Stream the result via fetch (not EventSource)
83
+ const streamUrl = selectedApiBase + apiName + '/' + eventId;
84
+ console.log(`[Gradio] Streaming from: ${streamUrl}`);
85
 
86
+ const streamHeaders = { 'Accept': 'text/event-stream' };
87
+ if (hfToken) {
88
+ streamHeaders['Authorization'] = `Bearer ${hfToken}`;
89
+ }
90
 
91
+ const sseRes = await fetch(streamUrl, {
92
+ method: 'GET',
93
+ headers: streamHeaders,
94
+ credentials: 'include',
95
+ cache: 'no-cache',
96
+ });
97
 
98
+ if (!sseRes.ok) {
99
+ throw new Error('Gradio SSE Stream Failed (' + sseRes.status + ')');
100
+ }
101
 
102
+ const reader = sseRes.body.getReader();
103
+ const decoder = new TextDecoder();
104
+ let buffer = '';
105
 
106
+ while (true) {
107
+ const { done, value } = await reader.read();
108
 
109
+ if (value) {
110
+ buffer += decoder.decode(value, { stream: true });
111
+ }
112
+ if (done) {
113
+ buffer += decoder.decode();
114
+ if (buffer.trim()) {
115
+ buffer += '\n\n';
116
+ }
117
  }
 
118
 
119
+ // Parse SSE events from buffer (events separated by double newline)
120
+ const parts = buffer.split('\n\n');
121
+ // Keep the last incomplete chunk in the buffer
122
+ buffer = parts.pop();
123
 
124
+ for (const part of parts) {
125
+ let eventType = '';
126
+ const dataLines = [];
127
 
128
+ for (const line of part.split('\n')) {
129
+ if (line.startsWith('event:')) {
130
+ eventType = line.substring(6).trim();
131
+ } else if (line.startsWith('data:')) {
132
+ dataLines.push(line.substring(5).replace(/^ /, ''));
133
+ }
134
  }
 
135
 
136
+ const dataLine = dataLines.join('\n');
137
+ if (!dataLine && !eventType) continue;
138
+
139
+ // ZeroGPU format: event type is "complete" or "error",
140
+ // data is a raw JSON array of outputs
141
+ if (eventType === 'complete') {
142
+ if (!done) reader.cancel();
143
+ try {
144
+ const result = JSON.parse(dataLine);
145
+ return Array.isArray(result) ? result : [result];
146
+ } catch {
147
+ return [dataLine];
148
+ }
149
+ } else if (eventType === 'error') {
150
+ if (!done) reader.cancel();
151
+ console.error(`[Gradio] SSE Error event:`, part);
152
+ const errorDetail = dataLine === 'null' ? 'Server Error (null)' : dataLine;
153
+ // Phone home specifically for SSE error
154
+ throw new Error(`SSE_ERROR: ${errorDetail}`);
155
  }
156
+ // Also handle the non-ZeroGPU format (Gradio standard)
157
+ if (dataLine) {
158
+ try {
159
+ const msg = JSON.parse(dataLine);
160
+ if (msg.msg === 'process_completed') {
161
+ if (!done) reader.cancel();
162
+ if (msg.success) {
163
+ return msg.output.data;
164
+ } else {
165
+ throw new Error(msg.output?.error || 'Server Error');
166
+ }
 
 
 
 
 
167
  }
168
+ } catch {
169
+ // Not JSON or not the expected format, skip
170
  }
 
 
171
  }
172
  }
 
173
 
174
+ if (done) break;
175
+ }
176
+ } catch (err) {
177
+ // Phone-home error report
178
+ try {
179
+ fetch(`${API_BASE}/debug/report_error`, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({
183
+ apiName,
184
+ error: err.message,
185
+ stack: err.stack,
186
+ userAgent: navigator.userAgent,
187
+ url: window.location.href,
188
+ timestamp: new Date().toISOString()
189
+ })
190
+ });
191
+ } catch (e) { /* ignore secondary error */ }
192
+ throw err;
193
  }
 
 
194
  }
195
 
196
  // ── Init ──────────────────────────────────────────────────────────────