feat: privacy-respecting analytics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2170e9275c
commit
e10c1dc8aa
@@ -0,0 +1,67 @@
|
||||
import { sqlite } from '../db/index.js'
|
||||
|
||||
function today(): string {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
const incrementStmt = sqlite.prepare(
|
||||
`INSERT INTO analytics (date, metric, value) VALUES (?, ?, 1)
|
||||
ON CONFLICT(date, metric) DO UPDATE SET value = value + 1`
|
||||
)
|
||||
|
||||
const incrementByStmt = sqlite.prepare(
|
||||
`INSERT INTO analytics (date, metric, value) VALUES (?, ?, ?)
|
||||
ON CONFLICT(date, metric) DO UPDATE SET value = value + ?`
|
||||
)
|
||||
|
||||
/** Increment a metric counter for today */
|
||||
export function trackMetric(metric: string): void {
|
||||
try { incrementStmt.run(today(), metric) } catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
/** Increment a metric by a specific amount */
|
||||
export function trackMetricBy(metric: string, amount: number): void {
|
||||
try { incrementByStmt.run(today(), metric, amount, amount) } catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
/** Record a fight completion with aggregate counters */
|
||||
export function trackFightCompleted(opts: {
|
||||
satsWagered: number
|
||||
durationRounds: number
|
||||
mode: 'free' | 'ranked'
|
||||
}): void {
|
||||
trackMetric('fights')
|
||||
trackMetric(`fights_${opts.mode}`)
|
||||
if (opts.satsWagered > 0) {
|
||||
trackMetricBy('sats_wagered', opts.satsWagered)
|
||||
}
|
||||
trackMetricBy('total_rounds', opts.durationRounds)
|
||||
}
|
||||
|
||||
/** Track a unique bot that fought today */
|
||||
export function trackBotActive(botId: string): void {
|
||||
const key = `${today()}:${botId}`
|
||||
if (seenBots.has(key)) return
|
||||
seenBots.add(key)
|
||||
trackMetric('unique_bots')
|
||||
|
||||
// Prune old entries (keep only today's)
|
||||
const todayPrefix = today() + ':'
|
||||
for (const k of seenBots) {
|
||||
if (!k.startsWith(todayPrefix)) seenBots.delete(k)
|
||||
}
|
||||
}
|
||||
|
||||
const seenBots = new Set<string>()
|
||||
|
||||
/** Get aggregate stats for the last N days */
|
||||
export function getPublicStats(days: number = 30): Array<{ date: string; metric: string; value: number }> {
|
||||
const cutoff = new Date()
|
||||
cutoff.setDate(cutoff.getDate() - days)
|
||||
const cutoffStr = cutoff.toISOString().slice(0, 10)
|
||||
|
||||
const stmt = sqlite.prepare(
|
||||
'SELECT date, metric, value FROM analytics WHERE date >= ? ORDER BY date DESC, metric'
|
||||
)
|
||||
return stmt.all(cutoffStr) as Array<{ date: string; metric: string; value: number }>
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { settleBets, lockBets } from './betting.js'
|
||||
import { publishFightResult } from './nostr-publish.js'
|
||||
import { getCurrentSeason } from './seasons.js'
|
||||
import { onFightFinished as onTournamentFightFinished } from './tournaments.js'
|
||||
import { trackFightCompleted, trackBotActive, trackMetric } from './analytics.js'
|
||||
|
||||
interface BotRecord {
|
||||
id: string
|
||||
@@ -514,6 +515,18 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
|
||||
finalize()
|
||||
|
||||
// Track aggregate analytics (no PII)
|
||||
trackBotActive(botA.id)
|
||||
trackBotActive(botB.id)
|
||||
trackFightCompleted({
|
||||
satsWagered: mode === 'ranked' ? ENTRY_FEE_SATS * 2 : 0,
|
||||
durationRounds: lastRound,
|
||||
mode,
|
||||
})
|
||||
for (const ct of usedTypes) {
|
||||
trackMetric(`challenge_${ct}`)
|
||||
}
|
||||
|
||||
// Advance tournament bracket if this was a tournament match
|
||||
try { onTournamentFightFinished(fightId, winnerId ?? null) } catch { /* not a tournament fight */ }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user