@commonpub/layer 0.61.0 → 0.62.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
|
-
"@commonpub/editor": "0.7.11",
|
|
58
57
|
"@commonpub/config": "0.19.0",
|
|
58
|
+
"@commonpub/explainer": "0.7.15",
|
|
59
|
+
"@commonpub/docs": "0.6.3",
|
|
59
60
|
"@commonpub/protocol": "0.13.0",
|
|
61
|
+
"@commonpub/server": "2.82.0",
|
|
62
|
+
"@commonpub/editor": "0.7.11",
|
|
60
63
|
"@commonpub/learning": "0.5.2",
|
|
61
|
-
"@commonpub/server": "2.81.0",
|
|
62
64
|
"@commonpub/ui": "0.9.2",
|
|
63
|
-
"@commonpub/
|
|
64
|
-
"@commonpub/schema": "0.34.0",
|
|
65
|
-
"@commonpub/docs": "0.6.3"
|
|
65
|
+
"@commonpub/schema": "0.35.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getMetricsTimeseries, TIMESERIES_METRICS } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
const DAY_MS = 86_400_000;
|
|
6
|
+
|
|
7
|
+
const querySchema = z.object({
|
|
8
|
+
metric: z.enum(Object.keys(TIMESERIES_METRICS) as [string, ...string[]]),
|
|
9
|
+
interval: z.enum(['day', 'week', 'month']).default('day'),
|
|
10
|
+
from: z.string().regex(DAY).optional(),
|
|
11
|
+
to: z.string().regex(DAY).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/public/v1/metrics/timeseries
|
|
16
|
+
*
|
|
17
|
+
* Scope: read:analytics. Daily time-series from the `metrics_daily` rollups.
|
|
18
|
+
* `metric` is one of the registered series (users.total/new, content.total/new/
|
|
19
|
+
* views/likes/comments); `interval` buckets day|week|month. Defaults to the last
|
|
20
|
+
* 90 days. Engagement (views/likes/comments) series begin at the first rollup,
|
|
21
|
+
* surfaced via `since`.
|
|
22
|
+
*/
|
|
23
|
+
export default defineEventHandler(async (event) => {
|
|
24
|
+
requireApiScope(event, 'read:analytics');
|
|
25
|
+
const parsed = querySchema.safeParse(getQuery(event));
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
throw createError({ statusCode: 400, statusMessage: 'Invalid query parameters', data: parsed.error.flatten() });
|
|
28
|
+
}
|
|
29
|
+
const { metric, interval } = parsed.data;
|
|
30
|
+
|
|
31
|
+
// Default to the last 90 days; clamp the span to 2 years to bound the scan.
|
|
32
|
+
const todayMs = Date.now();
|
|
33
|
+
const to = parsed.data.to ?? new Date(todayMs).toISOString().slice(0, 10);
|
|
34
|
+
let from = parsed.data.from ?? new Date(todayMs - 90 * DAY_MS).toISOString().slice(0, 10);
|
|
35
|
+
if (from > to) from = to;
|
|
36
|
+
const minFrom = new Date(new Date(`${to}T00:00:00Z`).getTime() - 730 * DAY_MS).toISOString().slice(0, 10);
|
|
37
|
+
if (from < minFrom) from = minFrom;
|
|
38
|
+
|
|
39
|
+
const db = useDB();
|
|
40
|
+
return await getMetricsTimeseries(db, { metric, interval, from, to });
|
|
41
|
+
});
|
|
@@ -379,6 +379,9 @@ export default defineEventHandler((event) => {
|
|
|
379
379
|
'/metrics/engagement': {
|
|
380
380
|
get: { summary: 'Aggregate engagement ratios and funnels', security: [{ bearer: ['read:analytics'] }], responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object' } } } } } },
|
|
381
381
|
},
|
|
382
|
+
'/metrics/timeseries': {
|
|
383
|
+
get: { summary: 'Daily time-series from rollups', security: [{ bearer: ['read:analytics'] }], parameters: [{ name: 'metric', in: 'query', required: true, schema: { type: 'string', enum: ['users.total', 'users.new', 'content.total', 'content.new', 'content.views', 'content.likes', 'content.comments'] } }, { name: 'interval', in: 'query', schema: { type: 'string', enum: ['day', 'week', 'month'] } }, { name: 'from', in: 'query', schema: { type: 'string', format: 'date' } }, { name: 'to', in: 'query', schema: { type: 'string', format: 'date' } }], responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object' } } } } } },
|
|
384
|
+
},
|
|
382
385
|
'/metrics/federation': {
|
|
383
386
|
get: { summary: 'Federation reach (opt-in; read:federation)', security: [{ bearer: ['read:federation'] }], parameters: [{ name: 'limit', in: 'query', schema: { type: 'integer' } }], responses: { '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object' } } } }, '404': { description: 'Federation reach metrics not enabled', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } } } },
|
|
384
387
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily analytics rollup worker (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* When `features.publicApi` is on, snapshots aggregate metrics into
|
|
5
|
+
* `metrics_daily` so `GET /api/public/v1/metrics/timeseries` has history. On the
|
|
6
|
+
* first ever run (empty table) it backfills the deterministic count-based series
|
|
7
|
+
* from timestamps. Runs are idempotent (upsert per day), so the periodic
|
|
8
|
+
* interval just refreshes today's row. Aggregates only — no per-user data.
|
|
9
|
+
*/
|
|
10
|
+
import { runDailyRollup, backfillMetricsDaily } from '@commonpub/server';
|
|
11
|
+
import { metricsDaily } from '@commonpub/schema';
|
|
12
|
+
|
|
13
|
+
const ROLLUP_INTERVAL_MS = 6 * 60 * 60 * 1000; // every 6h; refreshes today's snapshot
|
|
14
|
+
|
|
15
|
+
export default defineNitroPlugin((nitro) => {
|
|
16
|
+
if (process.env.NODE_ENV === 'test') return;
|
|
17
|
+
|
|
18
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
19
|
+
|
|
20
|
+
const startupTimer = setTimeout(() => {
|
|
21
|
+
try {
|
|
22
|
+
const config = useConfig();
|
|
23
|
+
if (!config.features.publicApi) return; // no public API ⇒ no rollups needed
|
|
24
|
+
console.log(`[metrics-rollup] worker started (interval: ${ROLLUP_INTERVAL_MS}ms)`);
|
|
25
|
+
runRollup();
|
|
26
|
+
interval = setInterval(runRollup, ROLLUP_INTERVAL_MS);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('[metrics-rollup] failed to start:', err instanceof Error ? err.message : err);
|
|
29
|
+
}
|
|
30
|
+
}, 15_000); // stagger after federation/registry workers
|
|
31
|
+
|
|
32
|
+
async function runRollup(): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const db = useDB();
|
|
35
|
+
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD (UTC)
|
|
36
|
+
const [existing] = await db.select({ id: metricsDaily.id }).from(metricsDaily).limit(1);
|
|
37
|
+
if (!existing) {
|
|
38
|
+
const n = await backfillMetricsDaily(db);
|
|
39
|
+
console.log(`[metrics-rollup] backfilled ${n} historical rows`);
|
|
40
|
+
}
|
|
41
|
+
await runDailyRollup(db, today);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('[metrics-rollup] run error:', err instanceof Error ? err.message : err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
nitro.hooks.hook('close', () => {
|
|
48
|
+
clearTimeout(startupTimer);
|
|
49
|
+
if (interval) {
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
interval = null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|