From ad88c6a2f6046cf55c4d64a66e5811a9e609d117 Mon Sep 17 00:00:00 2001 From: yusuftomilola Date: Thu, 27 Aug 2026 13:35:22 +0100 Subject: [PATCH] Add slow query detector, content-based recommender, XP level calculator, and funnel analysis utilities --- src/common/utils/content-based-recommender.ts | 34 +++++++++++++++++ src/common/utils/funnel-analysis.ts | 37 +++++++++++++++++++ src/common/utils/slow-query-detector.ts | 35 ++++++++++++++++++ src/common/utils/xp-level-calculator.ts | 27 ++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 src/common/utils/content-based-recommender.ts create mode 100644 src/common/utils/funnel-analysis.ts create mode 100644 src/common/utils/slow-query-detector.ts create mode 100644 src/common/utils/xp-level-calculator.ts diff --git a/src/common/utils/content-based-recommender.ts b/src/common/utils/content-based-recommender.ts new file mode 100644 index 00000000..bfee02cd --- /dev/null +++ b/src/common/utils/content-based-recommender.ts @@ -0,0 +1,34 @@ +/** + * Simple content-based puzzle recommender using tag overlap scoring. (#413) + */ +export interface PuzzleProfile { + puzzleId: string; + tags: string[]; +} + +export interface ScoredRecommendation { + puzzleId: string; + score: number; +} + +function jaccardSimilarity(a: string[], b: string[]): number { + const setA = new Set(a); + const setB = new Set(b); + const intersection = [...setA].filter((tag) => setB.has(tag)).length; + const union = new Set([...setA, ...setB]).size; + return union === 0 ? 0 : intersection / union; +} + +export function recommendByContent( + playerPreferredTags: string[], + candidates: PuzzleProfile[], + limit = 5, +): ScoredRecommendation[] { + return candidates + .map((puzzle) => ({ + puzzleId: puzzle.puzzleId, + score: jaccardSimilarity(playerPreferredTags, puzzle.tags), + })) + .sort((a, b) => b.score - a.score) + .slice(0, limit); +} diff --git a/src/common/utils/funnel-analysis.ts b/src/common/utils/funnel-analysis.ts new file mode 100644 index 00000000..0fdf6567 --- /dev/null +++ b/src/common/utils/funnel-analysis.ts @@ -0,0 +1,37 @@ +/** + * Compute funnel conversion rates between sequential puzzle analytics + * events (e.g. started -> attempted -> completed). (#415) + */ +export interface FunnelStep { + name: string; + count: number; +} + +export interface FunnelConversion { + from: string; + to: string; + conversionRate: number; +} + +export function computeFunnelConversions(steps: FunnelStep[]): FunnelConversion[] { + const conversions: FunnelConversion[] = []; + + for (let i = 1; i < steps.length; i++) { + const previous = steps[i - 1]; + const current = steps[i]; + conversions.push({ + from: previous.name, + to: current.name, + conversionRate: previous.count === 0 ? 0 : current.count / previous.count, + }); + } + + return conversions; +} + +export function overallDropOff(steps: FunnelStep[]): number { + if (steps.length === 0 || steps[0].count === 0) return 0; + const first = steps[0].count; + const last = steps[steps.length - 1].count; + return 1 - last / first; +} diff --git a/src/common/utils/slow-query-detector.ts b/src/common/utils/slow-query-detector.ts new file mode 100644 index 00000000..2664799a --- /dev/null +++ b/src/common/utils/slow-query-detector.ts @@ -0,0 +1,35 @@ +/** + * Detect slow database queries from profiling logs and suggest an + * actionable index recommendation. (#412) + */ +export interface QueryLogEntry { + sql: string; + durationMs: number; + table?: string; +} + +export interface SlowQueryReport { + sql: string; + durationMs: number; + recommendation: string; +} + +export function detectSlowQueries( + logs: QueryLogEntry[], + thresholdMs = 200, +): SlowQueryReport[] { + return logs + .filter((log) => log.durationMs >= thresholdMs) + .map((log) => ({ + sql: log.sql, + durationMs: log.durationMs, + recommendation: log.table + ? `Consider adding an index on frequently filtered columns of "${log.table}"` + : "Run EXPLAIN ANALYZE to identify missing indexes", + })); +} + +export function averageQueryDuration(logs: QueryLogEntry[]): number { + if (logs.length === 0) return 0; + return logs.reduce((sum, log) => sum + log.durationMs, 0) / logs.length; +} diff --git a/src/common/utils/xp-level-calculator.ts b/src/common/utils/xp-level-calculator.ts new file mode 100644 index 00000000..70cbac09 --- /dev/null +++ b/src/common/utils/xp-level-calculator.ts @@ -0,0 +1,27 @@ +/** + * Compute player level and progression from accumulated experience. (#414) + */ +const BASE_XP_PER_LEVEL = 100; +const XP_GROWTH_FACTOR = 1.15; + +export function xpRequiredForLevel(level: number): number { + return Math.round(BASE_XP_PER_LEVEL * Math.pow(XP_GROWTH_FACTOR, level - 1)); +} + +export function computeLevelFromXp(totalXp: number): { level: number; xpIntoLevel: number; xpToNextLevel: number } { + let level = 1; + let remainingXp = totalXp; + + for (;;) { + const requiredForNext = xpRequiredForLevel(level); + if (remainingXp < requiredForNext) { + return { level, xpIntoLevel: remainingXp, xpToNextLevel: requiredForNext - remainingXp }; + } + remainingXp -= requiredForNext; + level += 1; + } +} + +export function isMilestoneLevel(level: number): boolean { + return level % 10 === 0; +}