Skip to content

Commit 03869ae

Browse files
committed
Fixes js
1 parent 351e733 commit 03869ae

6 files changed

Lines changed: 128 additions & 110 deletions

File tree

ai-exception-insights-starter/src/main/java/io/github/rexrk/exception/insights/autoconfigure/AiExceptionInsightsAutoConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import io.github.rexrk.exception.insights.capture.*;
5+
import io.github.rexrk.exception.insights.controller.ExceptionInsightsController;
56
import io.github.rexrk.exception.insights.service.ai.AiExplanationService;
67
import io.github.rexrk.exception.insights.service.output.console.ConsoleErrorOutput;
78
import io.github.rexrk.exception.insights.service.output.ErrorOutput;
@@ -139,4 +140,11 @@ public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
139140
};
140141
}
141142

143+
// --- Controller ---
144+
145+
@Bean
146+
public ExceptionInsightsController exceptionInsightsController(InMemoryErrorEventStore store) {
147+
return new ExceptionInsightsController(store);
148+
}
149+
142150
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.github.rexrk.exception.insights.controller;
2+
3+
import io.github.rexrk.exception.insights.model.ErrorEvent;
4+
import io.github.rexrk.exception.insights.store.InMemoryErrorEventStore;
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.*;
7+
8+
import java.util.List;
9+
10+
@RestController
11+
@RequestMapping("/exception-insights")
12+
public class ExceptionInsightsController {
13+
14+
private final InMemoryErrorEventStore store;
15+
16+
public ExceptionInsightsController(InMemoryErrorEventStore store) {
17+
this.store = store;
18+
}
19+
20+
@GetMapping("/events")
21+
public List<ErrorEvent> getEvents(@RequestParam(defaultValue = "20", name = "limit") int limit) {
22+
return store.getRecent(limit);
23+
}
24+
25+
@GetMapping("/events/{id}")
26+
public ResponseEntity<ErrorEvent> getEvent(@PathVariable("id") String id) {
27+
return store.findById(id)
28+
.map(ResponseEntity::ok)
29+
.orElse(ResponseEntity.notFound().build());
30+
}
31+
32+
@DeleteMapping("/events")
33+
public ResponseEntity<Void> clearAll() {
34+
store.clear();
35+
return ResponseEntity.noContent().build();
36+
}
37+
}

ai-exception-insights-starter/src/main/java/io/github/rexrk/exception/insights/model/DashboardEvent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.rexrk.exception.insights.model;
22

33
public record DashboardEvent(
4+
String id,
45
String type,
56
String exceptionClass,
67
String timestamp

ai-exception-insights-starter/src/main/java/io/github/rexrk/exception/insights/service/output/ui/UiErrorOutput.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public UiErrorOutput(SseEmitterRegistry registry) {
1818
@Override
1919
public void onErrorCaptured(ErrorEvent event) {
2020
DashboardEvent dashboardEvent = new DashboardEvent(
21+
event.getId(),
2122
event.getType().name(),
2223
event.getExceptionClass(),
2324
LocalDateTime.now().toString()
@@ -29,6 +30,7 @@ public void onErrorCaptured(ErrorEvent event) {
2930
@Override
3031
public void onAiExplanationReady(ErrorEvent event) {
3132
DashboardEvent dashboardEvent = new DashboardEvent(
33+
event.getId(),
3234
event.getType().name(),
3335
event.getExceptionClass(),
3436
LocalDateTime.now().toString()

demo-app/src/main/resources/application.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ spring:
77
chat:
88
base-url: https://openrouter.ai/api
99
options:
10-
model: stepfun/step-3.5-flash:free
10+
model: inclusionai/ling-2.6-1t:free
1111
temperature: 0.8
1212
frequencyPenalty: 2.0
1313

dev-tools-ui/src/main/resources/static/dashboard.js

Lines changed: 79 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(() => {
2-
const BASE = `${window.location.origin}/dev-tools`;
2+
const BASE_UI = `${window.location.origin}/dev-tools`;
3+
const BASE_API = `${window.location.origin}/exception-insights`;
34

45
const SAMPLE = [
56
{
@@ -9,13 +10,13 @@
910
httpMethod: 'POST', requestUri: '/api/users', requestBody: '{"name":"John"}',
1011
context: { thread: 'http-nio-8080', method: 'UserService.createUser' },
1112
recentLogs: [
12-
{ level: 'WARN', message: 'Validation skipped', loggerName: 'com.example.UserService', threadName: 'http-nio-8080', timestamp: '2026-04-20T10:29:59Z' },
13-
{ level: 'INFO', message: 'Request received: POST /api/users', loggerName: 'com.example.RequestFilter', threadName: 'http-nio-8080', timestamp: '2026-04-20T10:29:58Z' }
13+
{ level: 'WARN', message: 'Validation skipped', loggerName: 'com.example.UserService', threadName: 'http-nio-8080', timestamp: '2026-04-20T10:29:59Z' },
14+
{ level: 'INFO', message: 'Request received: POST /api/users', loggerName: 'com.example.RequestFilter', threadName: 'http-nio-8080', timestamp: '2026-04-20T10:29:58Z' }
1415
],
1516
aiExplanation: {
1617
summary: 'A null value was returned by getUser() and dereferenced without a null check, causing a NullPointerException during user creation.',
1718
causes: ['UserRepository.findById() returned null and was not checked before use', 'The user lookup was skipped due to missing validation in the request pipeline'],
18-
fixes: ['Add a null check after calling getUser() and return a 404 if the user is absent', 'Ensure request validation runs before the service layer is invoked', 'Consider using Optional<User> to make null-safety explicit']
19+
fixes: ['Add a null check after calling getUser() and return a 404 if the user is absent', 'Ensure request validation runs before the service layer is invoked', 'Consider using Optional<User> to make null-safety explicit']
1920
}
2021
},
2122
{
@@ -28,46 +29,16 @@
2829
],
2930
aiExplanation: null
3031
},
31-
{
32-
id: 'g7h8i9', timestamp: '2026-04-20T10:25:00Z', type: 'TRANSACTIONAL',
33-
exceptionClass: 'javax.persistence.OptimisticLockException', message: 'Row was updated or deleted by another transaction',
34-
rootCauseClass: 'javax.persistence.OptimisticLockException', rootCauseMessage: 'Row was updated or deleted by another transaction',
35-
context: { thread: 'http-nio-8081', method: 'OrderService.updateStatus' },
36-
recentLogs: [
37-
{ level: 'WARN', message: 'Optimistic lock conflict detected', loggerName: 'com.example.OrderService', threadName: 'http-nio-8081', timestamp: '2026-04-20T10:24:58Z' }
38-
],
39-
aiExplanation: {
40-
summary: 'Two concurrent transactions attempted to update the same row simultaneously; the second update detected a version mismatch and was rolled back.',
41-
causes: ['Concurrent requests modified the same Order entity without coordination', 'The version column was not refreshed before the second write'],
42-
fixes: ['Implement retry logic with exponential backoff for OptimisticLockException', 'Reduce transaction scope to minimize contention window', 'Use pessimistic locking for high-contention entities if conflicts are frequent']
43-
}
44-
},
45-
{
46-
id: 'j1k2l3', timestamp: '2026-04-20T10:20:00Z', type: 'ASYNC',
47-
exceptionClass: 'java.util.concurrent.TimeoutException', message: 'Task execution timed out after 30s',
48-
rootCauseClass: 'java.util.concurrent.TimeoutException', rootCauseMessage: 'Task execution timed out after 30s',
49-
context: { thread: 'async-executor-3', method: 'EmailService.sendBatch' },
50-
recentLogs: [
51-
{ level: 'WARN', message: 'Async task queue size: 847', loggerName: 'com.example.AsyncConfig', threadName: 'async-executor-3', timestamp: '2026-04-20T10:19:50Z' },
52-
{ level: 'DEBUG', message: 'Starting batch email dispatch', loggerName: 'com.example.EmailService', threadName: 'async-executor-3', timestamp: '2026-04-20T10:19:30Z' }
53-
],
54-
aiExplanation: {
55-
summary: 'The async email batch task exceeded the 30-second execution timeout, likely due to a large queue backed up by slow SMTP responses.',
56-
causes: ['Queue size of 847 items exceeded expected throughput for the timeout window', 'SMTP server response times may have degraded'],
57-
fixes: ['Increase the async task timeout or split batches into smaller chunks', 'Add circuit-breaker logic for slow external SMTP dependencies', 'Monitor queue depth and implement backpressure']
58-
}
59-
}
6032
];
6133

62-
// ── State ────────────────────────────────────────────────
34+
// ── State ────────────────────────────────────────────────
6335
let errors = [];
6436
let selectedId = null;
6537
let eventSource = null;
38+
let demoMode = false;
6639

67-
// ── Helpers ──────────────────────────────────────────────
68-
function shortClass(cls) {
69-
return cls ? cls.split('.').pop() : '';
70-
}
40+
// ── Helpers ───────────────────────────────────────────────
41+
function shortClass(cls) { return cls ? cls.split('.').pop() : ''; }
7142

7243
function fmtTime(ts) {
7344
if (!ts) return '';
@@ -80,14 +51,36 @@
8051
}
8152

8253
function esc(str) {
83-
return String(str)
84-
.replace(/&/g, '&amp;')
85-
.replace(/</g, '&lt;')
86-
.replace(/>/g, '&gt;')
87-
.replace(/"/g, '&quot;');
54+
return String(str ?? '')
55+
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
56+
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
57+
}
58+
59+
// ── API ───────────────────────────────────────────────────
60+
async function fetchById(id) {
61+
const res = await fetch(`${BASE_API}/events/${id}`);
62+
if (!res.ok) throw new Error('not found');
63+
return res.json();
64+
}
65+
66+
async function fetchEvents() {
67+
try {
68+
const res = await fetch(`${BASE_API}/events`);
69+
if (!res.ok) throw new Error('non-2xx');
70+
errors = await res.json();
71+
renderList();
72+
if (errors.length) selectError(errors[0].id);
73+
} catch {
74+
demoMode = true;
75+
errors = SAMPLE;
76+
renderList();
77+
if (errors.length) selectError(errors[0].id);
78+
document.getElementById('sseLabel').textContent = 'Demo mode';
79+
document.getElementById('sseDot').className = 'sse-dot';
80+
}
8881
}
8982

90-
// ── Render: sidebar list ─────────────────────────────────
83+
// ── Render: sidebar list ─────────────────────────────────
9184
function renderList() {
9285
const list = document.getElementById('errorList');
9386

@@ -97,33 +90,23 @@
9790
}
9891

9992
list.innerHTML = errors.map(e => `
100-
<div class="error-item${selectedId === e.id ? ' selected' : ''}" data-id="${esc(e.id)}">
101-
<div class="error-item-row1">
102-
<span class="badge badge-${esc(e.type)}">
103-
${esc(e.type.replace(/_/g, ' '))}
104-
</span>
105-
106-
${e.isNew ? '<span class="new-chip">NEW</span>' : ''}
107-
108-
${!e.aiExplanation ? '<span class="analyzing-chip">analyzing…</span>' : ''}
109-
</div>
110-
111-
<div class="error-exc">
112-
${esc(shortClass(e.exceptionClass))}
113-
</div>
114-
115-
<div class="error-time">
116-
${esc(fmtDateTime(e.timestamp))}
93+
<div class="error-item${selectedId === e.id ? ' selected' : ''}" data-id="${esc(e.id)}">
94+
<div class="error-item-row1">
95+
<span class="badge badge-${esc(e.type)}">${esc(e.type.replace(/_/g, ' '))}</span>
96+
${e.isNew ? '<span class="new-chip">NEW</span>' : ''}
97+
${!e.aiExplanation ? '<span class="analyzing-chip">analyzing…</span>' : ''}
98+
</div>
99+
<div class="error-exc">${esc(shortClass(e.exceptionClass))}</div>
100+
<div class="error-time">${esc(fmtDateTime(e.timestamp))}</div>
117101
</div>
118-
</div>
119-
`).join('');
102+
`).join('');
120103

121104
list.querySelectorAll('.error-item').forEach(el => {
122105
el.addEventListener('click', () => selectError(el.dataset.id));
123106
});
124107
}
125108

126-
// ── Render: main detail panel ────────────────────────────
109+
// ── Render: detail panel ──────────────────────────────────
127110
function renderDetail(e) {
128111
document.getElementById('emptyState').style.display = 'none';
129112
const panel = document.getElementById('detailPanel');
@@ -219,7 +202,7 @@
219202
`;
220203
}
221204

222-
// ── State mutations ──────────────────────────────────────
205+
// ── State mutations ──────────────────────────────────────
223206
function selectError(id) {
224207
selectedId = id;
225208
renderList();
@@ -228,21 +211,9 @@
228211
}
229212

230213
function addError(e) {
231-
const newError = {
232-
...e,
233-
isNew: true
234-
};
235-
236-
errors = [
237-
newError,
238-
...errors.map(item => ({
239-
...item,
240-
isNew: false
241-
}))
242-
];
243-
214+
errors = [{ ...e, isNew: true }, ...errors.map(x => ({ ...x, isNew: false }))];
244215
renderList();
245-
selectError(newError.id);
216+
selectError(e.id);
246217
}
247218

248219
function updateAi(id, aiExplanation) {
@@ -254,35 +225,29 @@
254225
}
255226
}
256227

257-
// ── API ──────────────────────────────────────────────────
258-
async function fetchEvents() {
259-
try {
260-
const res = await fetch(`${BASE}/stream`);
261-
if (!res.ok) throw new Error('non-2xx');
262-
errors = await res.json();
263-
renderList();
264-
if (errors.length) selectError(errors[0].id);
265-
} catch {
266-
// Fall back to demo data when backend is unreachable
267-
errors = SAMPLE;
268-
renderList();
269-
if (errors.length) selectError(errors[0].id);
270-
document.getElementById('sseLabel').textContent = 'Demo mode';
271-
document.getElementById('sseDot').className = 'sse-dot';
272-
}
273-
}
274-
228+
// ── SSE ───────────────────────────────────────────────────
275229
function connectSSE() {
276230
try {
277-
eventSource = new EventSource(`${BASE}/stream`);
278-
279-
eventSource.addEventListener('error-captured', ev => {
280-
addError(JSON.parse(ev.data));
231+
eventSource = new EventSource(`${BASE_UI}/stream`);
232+
233+
eventSource.addEventListener('error-captured', async ev => {
234+
const { id } = JSON.parse(ev.data);
235+
try {
236+
const fullEvent = await fetchById(id);
237+
addError(fullEvent);
238+
} catch (e) {
239+
console.error('Failed to fetch error by id:', id, e);
240+
}
281241
});
282242

283-
eventSource.addEventListener('ai-insight-ready', ev => {
284-
const dto = JSON.parse(ev.data);
285-
updateAi(dto.id, dto.aiExplanation);
243+
eventSource.addEventListener('ai-ready', async ev => {
244+
const { id } = JSON.parse(ev.data);
245+
try {
246+
const fullEvent = await fetchById(id);
247+
updateAi(fullEvent.id, fullEvent.aiExplanation);
248+
} catch (e) {
249+
console.error('Failed to fetch ai update for id:', id, e);
250+
}
286251
});
287252

288253
eventSource.onopen = () => {
@@ -294,17 +259,21 @@
294259
document.getElementById('sseDot').className = 'sse-dot';
295260
document.getElementById('sseLabel').textContent = 'SSE disconnected';
296261
};
262+
297263
} catch {
298-
// SSE not available (e.g. opened as file://)
264+
// SSE not available
299265
}
300266
}
301267

302-
// ── Event listeners ──────────────────────────────────────
268+
// ── Event listeners ──────────────────────────────────────
303269
document.getElementById('btnClear').addEventListener('click', async () => {
270+
if (!demoMode) {
271+
await fetch(`${BASE_API}/events`, { method: 'DELETE' });
272+
}
304273
errors = [];
305274
selectedId = null;
306275
renderList();
307-
document.getElementById('emptyState').style.display = '';
276+
document.getElementById('emptyState').style.display = '';
308277
document.getElementById('detailPanel').style.display = 'none';
309278
});
310279

@@ -315,7 +284,8 @@
315284
document.getElementById('themeLabel').textContent = isDark ? 'Light' : 'Dark';
316285
});
317286

318-
// ── Boot ─────────────────────────────────────────────────
287+
// ── Boot ─────────────────────────────────────────────────
319288
connectSSE();
320289
fetchEvents().then(r => {});
321-
})();
290+
291+
})();

0 commit comments

Comments
 (0)