mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-10-22 13:54:10 -04:00
first pass analytics redo
This commit is contained in:
1 parent
b656fd04c0
commit
45ffa39f2a
69 files changed
+6400
-1138
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { isAdmin } from '$lib/server/admin';
|
||||
import { isValidDate } from '$lib/server/stats/dates';
|
||||
import { buildSnapshot } from '$lib/server/stats/snapshot';
|
||||
|
||||
/** Admin-only manual snapshot (re)build, e.g. after a settings change. */
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!isAdmin(locals.user?.email)) {
|
||||
return json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
let body: { date?: string; mature?: boolean } = {};
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
/* fall through to validation */
|
||||
}
|
||||
|
||||
const date = body.date;
|
||||
if (!date || !isValidDate(date)) {
|
||||
return json({ error: 'A valid date (YYYY-MM-DD) is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { snapshot } = await buildSnapshot(date, {
|
||||
kind: 'reconstructed',
|
||||
replace: true
|
||||
});
|
||||
|
||||
return json({ ok: true, date, buildKind: snapshot.buildKind, generatedAt: snapshot.generatedAt });
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { addDays } from '$lib/server/stats/dates';
|
||||
import { buildSnapshot } from '$lib/server/stats/snapshot';
|
||||
import { sendDigest } from '$lib/server/stats/digest';
|
||||
|
||||
/**
|
||||
* Daily snapshot build trigger. Protected by `Authorization: Bearer $CRON_SECRET`
|
||||
* (same pattern as `/api/send-daily-verse`). Builds yesterday in `America/New_York`
|
||||
* and then sends the digest.
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const cronSecret = Bun.env.CRON_SECRET;
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const todayEst = new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });
|
||||
const yesterday = addDays(todayEst, -1);
|
||||
|
||||
const { snapshot } = await buildSnapshot(yesterday, {
|
||||
kind: 'scheduled',
|
||||
replace: true
|
||||
});
|
||||
|
||||
// Digest failure must never fail the build.
|
||||
await sendDigest(snapshot.payload);
|
||||
|
||||
return json({ ok: true, date: yesterday, buildKind: 'scheduled' });
|
||||
};
|
||||
@@ -2,10 +2,11 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getVerseForDate } from '$lib/server/daily-verse';
|
||||
import { getBookById } from '$lib/server/bible';
|
||||
import { recordEvent } from '$lib/server/stats/events';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const body = await request.json();
|
||||
const { date } = body;
|
||||
const { date, anonymousId } = body;
|
||||
|
||||
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
return json({ error: 'A valid date (YYYY-MM-DD) is required' }, { status: 400 });
|
||||
@@ -16,6 +17,18 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
const dailyVerse = await getVerseForDate(dateStr);
|
||||
const correctBook = getBookById(dailyVerse.bookId) ?? null;
|
||||
|
||||
// Instrumentation B: a player "started" the puzzle for this date. The unique
|
||||
// partial index dedupes reloads, so this is safe to call on every load.
|
||||
// Never let analytics failures affect the response.
|
||||
// Prefer the signed-in user's ID so the started→finished funnel matches
|
||||
// completions (which are keyed to the user ID for logged-in players).
|
||||
const playerId =
|
||||
locals.user?.id ??
|
||||
(typeof anonymousId === 'string' && anonymousId.length > 0 ? anonymousId : null);
|
||||
if (playerId) {
|
||||
void recordEvent({ name: 'verse_served', anonymousId: playerId, date: dateStr });
|
||||
}
|
||||
|
||||
return json({
|
||||
dailyVerse,
|
||||
correctBookId: dailyVerse.bookId,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { isEventName, recordEvent, encodeMetadata } from '$lib/server/stats/events';
|
||||
|
||||
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
/**
|
||||
* Fire-and-forget client analytics sink.
|
||||
*
|
||||
* Body: `{ name, anonymousId?, date?, metadata? }`.
|
||||
* `userId` is resolved server-side from the session — never trusted from the client.
|
||||
* Always returns 204, even for invalid input, so gameplay is never blocked.
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
let body: any;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const name = typeof body?.name === 'string' ? body.name : '';
|
||||
if (!isEventName(name)) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const anonymousId =
|
||||
typeof body?.anonymousId === 'string' && body.anonymousId.length > 0
|
||||
? body.anonymousId.slice(0, 128)
|
||||
: null;
|
||||
|
||||
const date =
|
||||
typeof body?.date === 'string' && DATE_RE.test(body.date) ? body.date : null;
|
||||
|
||||
const metadata =
|
||||
body?.metadata && typeof body.metadata === 'object' && !Array.isArray(body.metadata)
|
||||
? (body.metadata as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
// Ensure metadata is always serializable/capped before it reaches the DB.
|
||||
void encodeMetadata(metadata);
|
||||
|
||||
await recordEvent({
|
||||
name,
|
||||
anonymousId,
|
||||
userId: locals.user?.id ?? null,
|
||||
date,
|
||||
metadata
|
||||
});
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
Reference in new issue
Block a user