apiforgejs 1.0.0 → 1.0.2
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/package.json +11 -3
- package/src/database.js +24 -7
- package/src/ui.html +19 -11
package/package.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiforgejs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "API observability & intelligence SDK for Express.js — local-first, privacy-first",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"keywords": [
|
|
7
|
-
"api",
|
|
8
|
-
"
|
|
7
|
+
"api",
|
|
8
|
+
"observability",
|
|
9
|
+
"monitoring",
|
|
10
|
+
"express",
|
|
11
|
+
"metrics",
|
|
12
|
+
"performance",
|
|
13
|
+
"analytics",
|
|
14
|
+
"middleware",
|
|
15
|
+
"latency",
|
|
16
|
+
"local-first"
|
|
9
17
|
],
|
|
10
18
|
"author": "APIForge",
|
|
11
19
|
"license": "MIT",
|
package/src/database.js
CHANGED
|
@@ -229,6 +229,19 @@ class ApiForgeDatabase {
|
|
|
229
229
|
this._begin.run();
|
|
230
230
|
try {
|
|
231
231
|
for (const r of routes) stmt.run(r.route, r.method);
|
|
232
|
+
// Remove routes no longer in the router that never had traffic
|
|
233
|
+
if (routes.length > 0) {
|
|
234
|
+
const keys = routes.map(r => `${r.route}|${r.method}`);
|
|
235
|
+
const ph = keys.map(() => '?').join(', ');
|
|
236
|
+
this.db.prepare(`
|
|
237
|
+
DELETE FROM known_routes
|
|
238
|
+
WHERE route || '|' || method NOT IN (${ph})
|
|
239
|
+
AND NOT EXISTS (
|
|
240
|
+
SELECT 1 FROM api_metrics m
|
|
241
|
+
WHERE m.route = known_routes.route AND m.method = known_routes.method
|
|
242
|
+
)
|
|
243
|
+
`).run(...keys);
|
|
244
|
+
}
|
|
232
245
|
this._commit.run();
|
|
233
246
|
} catch (err) {
|
|
234
247
|
this._rollback.run();
|
|
@@ -258,13 +271,17 @@ class ApiForgeDatabase {
|
|
|
258
271
|
|
|
259
272
|
getReleases() {
|
|
260
273
|
return this.db.prepare(`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
274
|
+
WITH release_times AS (
|
|
275
|
+
SELECT release_tag, MIN(bucket_ts) AS release_ts
|
|
276
|
+
FROM api_metrics
|
|
277
|
+
WHERE release_tag IS NOT NULL AND release_tag != ''
|
|
278
|
+
GROUP BY release_tag
|
|
279
|
+
)
|
|
280
|
+
SELECT rt.release_tag,
|
|
281
|
+
rt.release_ts,
|
|
282
|
+
(SELECT COUNT(*) FROM known_routes WHERE first_seen <= rt.release_ts + 60) AS routes_affected
|
|
283
|
+
FROM release_times rt
|
|
284
|
+
ORDER BY rt.release_ts DESC
|
|
268
285
|
LIMIT 20
|
|
269
286
|
`).all();
|
|
270
287
|
}
|
package/src/ui.html
CHANGED
|
@@ -421,6 +421,7 @@ function mapInsights(insights) {
|
|
|
421
421
|
function mapReleases(releases) {
|
|
422
422
|
return (releases || []).map(r => ({
|
|
423
423
|
tag: r.release_tag,
|
|
424
|
+
ts: r.release_ts,
|
|
424
425
|
summary: `${r.routes_affected || 0} route${r.routes_affected !== 1 ? 's' : ''} recorded`,
|
|
425
426
|
age: formatAge(r.release_ts),
|
|
426
427
|
by: 'local',
|
|
@@ -717,7 +718,7 @@ function StatusStackChart({ data, height = 200 }) {
|
|
|
717
718
|
}
|
|
718
719
|
|
|
719
720
|
// ─── Overview ─────────────────────────────────────────────────────────────────
|
|
720
|
-
function Overview({ timeRange, setRoute, setParams }) {
|
|
721
|
+
function Overview({ timeRange, setRoute, setParams, lastUpdated }) {
|
|
721
722
|
const { ENDPOINTS, RELEASES, INSIGHTS, SUMMARY } = window.AF_DATA;
|
|
722
723
|
const [globalTs, setGlobalTs] = useState(null);
|
|
723
724
|
const hours = TIME_HOURS[timeRange] || 24;
|
|
@@ -726,7 +727,7 @@ function Overview({ timeRange, setRoute, setParams }) {
|
|
|
726
727
|
setGlobalTs(null);
|
|
727
728
|
fetch(`/api/global-timeseries?hours=${hours}`)
|
|
728
729
|
.then(r => r.json()).then(d => setGlobalTs(d)).catch(() => setGlobalTs([]));
|
|
729
|
-
}, [hours]);
|
|
730
|
+
}, [hours, lastUpdated]);
|
|
730
731
|
|
|
731
732
|
const chartData = globalTs ? tsBucketsToChart(globalTs, hours) : null;
|
|
732
733
|
const points = Math.max(chartData?.p90?.length || 0, 2);
|
|
@@ -739,11 +740,18 @@ function Overview({ timeRange, setRoute, setParams }) {
|
|
|
739
740
|
const globalCalls = chartData?.calls || Array(fallbackPts).fill(0);
|
|
740
741
|
const xLabelsFinal = xLabels.length > 0 ? xLabels : Array.from({length:globalP90.length}, (_,i) => `${i}`);
|
|
741
742
|
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
743
|
+
const MARKER_COLORS = ['#b91c1c','#15803d','#2563eb','#b45309','#7c3aed'];
|
|
744
|
+
const nowTs = Date.now() / 1000;
|
|
745
|
+
const releaseMarkers = globalTs && globalTs.length > 0
|
|
746
|
+
? [...(RELEASES || [])]
|
|
747
|
+
.filter(r => r.ts != null && r.ts >= nowTs - hours * 3600)
|
|
748
|
+
.reverse()
|
|
749
|
+
.map((r, i) => {
|
|
750
|
+
const idx = globalTs.reduce((best, b, j) =>
|
|
751
|
+
Math.abs(b.bucket_ts - r.ts) < Math.abs(globalTs[best].bucket_ts - r.ts) ? j : best, 0);
|
|
752
|
+
return { idx, label: r.tag, color: MARKER_COLORS[i % MARKER_COLORS.length] };
|
|
753
|
+
})
|
|
754
|
+
: [];
|
|
747
755
|
|
|
748
756
|
const topSlow = [...ENDPOINTS].filter(e => !e.untracked && e.base_p90 > 0).sort((a,b) => b.base_p90-a.base_p90).slice(0,5);
|
|
749
757
|
const topCalled = [...ENDPOINTS].sort((a,b) => b.calls24h-a.calls24h).slice(0,5);
|
|
@@ -1048,7 +1056,7 @@ function Endpoints({ setRoute, setParams }) {
|
|
|
1048
1056
|
}
|
|
1049
1057
|
|
|
1050
1058
|
// ─── Endpoint detail ──────────────────────────────────────────────────────────
|
|
1051
|
-
function EndpointDetail({ id, timeRange, setRoute, setParams }) {
|
|
1059
|
+
function EndpointDetail({ id, timeRange, setRoute, setParams, lastUpdated }) {
|
|
1052
1060
|
const { ENDPOINTS, INSIGHTS } = window.AF_DATA;
|
|
1053
1061
|
const ep = ENDPOINTS.find(e => e.id === id) || ENDPOINTS[0];
|
|
1054
1062
|
const [tab, setTab] = useState('performance');
|
|
@@ -1063,7 +1071,7 @@ function EndpointDetail({ id, timeRange, setRoute, setParams }) {
|
|
|
1063
1071
|
setTs(null);
|
|
1064
1072
|
fetch(`/api/timeseries?route=${encodeURIComponent(route)}&method=${encodeURIComponent(method)}&hours=${hours}`)
|
|
1065
1073
|
.then(r => r.json()).then(d => setTs(d)).catch(() => setTs([]));
|
|
1066
|
-
}, [id, hours]);
|
|
1074
|
+
}, [id, hours, lastUpdated]);
|
|
1067
1075
|
|
|
1068
1076
|
if (!ep) return <div className="empty-state">Endpoint not found.</div>;
|
|
1069
1077
|
|
|
@@ -1626,9 +1634,9 @@ function App() {
|
|
|
1626
1634
|
lastUpdated={lastUpdated} onRefresh={() => fetchData.current()}/>
|
|
1627
1635
|
<div className="content">
|
|
1628
1636
|
<div className="content-inner">
|
|
1629
|
-
{route === 'overview' && <Overview timeRange={timeRange} setRoute={setRoute} setParams={setParams}/>}
|
|
1637
|
+
{route === 'overview' && <Overview timeRange={timeRange} setRoute={setRoute} setParams={setParams} lastUpdated={lastUpdated}/>}
|
|
1630
1638
|
{route === 'endpoints' && <Endpoints setRoute={setRoute} setParams={setParams}/>}
|
|
1631
|
-
{route === 'endpoint' && <EndpointDetail id={params.id} timeRange={timeRange} setRoute={setRoute} setParams={setParams}/>}
|
|
1639
|
+
{route === 'endpoint' && <EndpointDetail id={params.id} timeRange={timeRange} setRoute={setRoute} setParams={setParams} lastUpdated={lastUpdated}/>}
|
|
1632
1640
|
{route === 'insights' && <Insights setRoute={setRoute} setParams={setParams}/>}
|
|
1633
1641
|
{route === 'releases' && <Releases/>}
|
|
1634
1642
|
{route === 'settings' && <Settings/>}
|