mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-10-22 05: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
@@ -1,484 +1,25 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { dailyCompletions, user } from '$lib/server/db/schema';
|
||||
import { eq, gte, count, countDistinct, avg, asc, min } from 'drizzle-orm';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { ensureSnapshot } from '$lib/server/stats/snapshot';
|
||||
import { addDays } from '$lib/server/stats/dates';
|
||||
|
||||
function estDateStr(daysAgo = 0): string {
|
||||
const estNow = new Date(Date.now() - 5 * 60 * 60 * 1000); // UTC-5
|
||||
estNow.setUTCDate(estNow.getUTCDate() - daysAgo);
|
||||
return estNow.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function prevDay(d: string): string {
|
||||
const dt = new Date(d + 'T00:00:00Z');
|
||||
dt.setUTCDate(dt.getUTCDate() - 1);
|
||||
return dt.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function addDays(d: string, n: number): string {
|
||||
const dt = new Date(d + 'T00:00:00Z');
|
||||
dt.setUTCDate(dt.getUTCDate() + n);
|
||||
return dt.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slim public community widget. Reads the latest daily snapshot and exposes
|
||||
* only safe aggregates (no retention, churn, signup or revenue data).
|
||||
*/
|
||||
export const load: PageServerLoad = async () => {
|
||||
const todayEst = estDateStr(0);
|
||||
const yesterdayEst = estDateStr(1);
|
||||
const sevenDaysAgo = estDateStr(7);
|
||||
|
||||
// Three weekly windows for first + second derivative calculations
|
||||
// Week A: last 7 days (indices 0–6)
|
||||
// Week B: 7–13 days ago (indices 7–13)
|
||||
// Week C: 14–20 days ago (indices 14–20)
|
||||
const weekAStart = estDateStr(6);
|
||||
const weekBEnd = estDateStr(7);
|
||||
const weekBStart = estDateStr(13);
|
||||
const weekCEnd = estDateStr(14);
|
||||
const weekCStart = estDateStr(20);
|
||||
|
||||
// ── Scalar stats ──────────────────────────────────────────────────────────
|
||||
|
||||
const [{ todayCount }] = await db
|
||||
.select({ todayCount: count() })
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.date, todayEst));
|
||||
|
||||
const [{ totalCount }] = await db
|
||||
.select({ totalCount: count() })
|
||||
.from(dailyCompletions);
|
||||
|
||||
const [{ uniquePlayers }] = await db
|
||||
.select({ uniquePlayers: countDistinct(dailyCompletions.anonymousId) })
|
||||
.from(dailyCompletions);
|
||||
|
||||
const [{ weeklyPlayers }] = await db
|
||||
.select({ weeklyPlayers: countDistinct(dailyCompletions.anonymousId) })
|
||||
.from(dailyCompletions)
|
||||
.where(gte(dailyCompletions.date, sevenDaysAgo));
|
||||
|
||||
const thirtyDaysAgo = estDateStr(30);
|
||||
const [{ monthlyPlayers }] = await db
|
||||
.select({ monthlyPlayers: countDistinct(dailyCompletions.anonymousId) })
|
||||
.from(dailyCompletions)
|
||||
.where(gte(dailyCompletions.date, thirtyDaysAgo));
|
||||
|
||||
const todayPlayers = await db
|
||||
.selectDistinct({ id: dailyCompletions.anonymousId })
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.date, todayEst));
|
||||
|
||||
const yesterdayPlayers = await db
|
||||
.selectDistinct({ id: dailyCompletions.anonymousId })
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.date, yesterdayEst));
|
||||
|
||||
const todaySet = new Set(todayPlayers.map((r) => r.id));
|
||||
const activeStreaks = yesterdayPlayers.filter((r) => todaySet.has(r.id)).length;
|
||||
|
||||
const [{ avgGuessesRaw }] = await db
|
||||
.select({ avgGuessesRaw: avg(dailyCompletions.guessCount) })
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.date, todayEst));
|
||||
|
||||
const avgGuessesToday = avgGuessesRaw != null ? parseFloat(avgGuessesRaw) : null;
|
||||
|
||||
const [{ registeredUsers }] = await db
|
||||
.select({ registeredUsers: count() })
|
||||
.from(user);
|
||||
|
||||
const avgCompletionsPerPlayer =
|
||||
uniquePlayers > 0 ? Math.round((totalCount / uniquePlayers) * 100) / 100 : null;
|
||||
|
||||
// ── 21-day completions per day (covers all three weekly windows) ──────────
|
||||
|
||||
const rawPerDay21 = await db
|
||||
.select({ date: dailyCompletions.date, dayCount: count() })
|
||||
.from(dailyCompletions)
|
||||
.where(gte(dailyCompletions.date, weekCStart))
|
||||
.groupBy(dailyCompletions.date)
|
||||
.orderBy(asc(dailyCompletions.date));
|
||||
|
||||
const counts21 = new Map(rawPerDay21.map((r) => [r.date, r.dayCount]));
|
||||
|
||||
// Build indexed array: index 0 = today, index 20 = 20 days ago
|
||||
const completionsPerDay: number[] = [];
|
||||
for (let i = 0; i <= 20; i++) {
|
||||
completionsPerDay.push(counts21.get(estDateStr(i)) ?? 0);
|
||||
}
|
||||
|
||||
// last14Days for the trend chart (most recent first)
|
||||
const last14Days: { date: string; count: number }[] = [];
|
||||
for (let i = 0; i <= 13; i++) {
|
||||
last14Days.push({ date: estDateStr(i), count: completionsPerDay[i] });
|
||||
}
|
||||
|
||||
// Weekly totals from the indexed array
|
||||
const weekATotal = completionsPerDay.slice(0, 7).reduce((a, b) => a + b, 0);
|
||||
const weekBTotal = completionsPerDay.slice(7, 14).reduce((a, b) => a + b, 0);
|
||||
const weekCTotal = completionsPerDay.slice(14, 21).reduce((a, b) => a + b, 0);
|
||||
|
||||
// First derivative: avg daily completions change (week A vs week B)
|
||||
const completionsVelocity = Math.round(((weekATotal - weekBTotal) / 7) * 10) / 10;
|
||||
// Second derivative: is velocity itself increasing or decreasing?
|
||||
const completionsAcceleration =
|
||||
Math.round((((weekATotal - weekBTotal) - (weekBTotal - weekCTotal)) / 7) * 10) / 10;
|
||||
|
||||
// ── 90-day per-user data (reused for streaks + weekly user sets) ──────────
|
||||
|
||||
const ninetyDaysAgo = estDateStr(90);
|
||||
const recentCompletions = await db
|
||||
.select({ anonymousId: dailyCompletions.anonymousId, date: dailyCompletions.date })
|
||||
.from(dailyCompletions)
|
||||
.where(gte(dailyCompletions.date, ninetyDaysAgo))
|
||||
.orderBy(asc(dailyCompletions.date));
|
||||
|
||||
// Group dates by user (ascending) and users by date
|
||||
const userDatesMap = new Map<string, string[]>();
|
||||
const dateUsersMap = new Map<string, Set<string>>();
|
||||
for (const row of recentCompletions) {
|
||||
const arr = userDatesMap.get(row.anonymousId);
|
||||
if (arr) arr.push(row.date);
|
||||
else userDatesMap.set(row.anonymousId, [row.date]);
|
||||
|
||||
let s = dateUsersMap.get(row.date);
|
||||
if (!s) { s = new Set(); dateUsersMap.set(row.date, s); }
|
||||
s.add(row.anonymousId);
|
||||
}
|
||||
|
||||
// ── Streak distribution ───────────────────────────────────────────────────
|
||||
|
||||
const streakDistribution = new Map<number, number>();
|
||||
for (const dates of userDatesMap.values()) {
|
||||
const desc = dates.slice().reverse();
|
||||
if (desc[0] !== todayEst && desc[0] !== yesterdayEst) continue;
|
||||
let streak = 1;
|
||||
let cur = desc[0];
|
||||
for (let i = 1; i < desc.length; i++) {
|
||||
if (desc[i] === prevDay(cur)) {
|
||||
streak++;
|
||||
cur = desc[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (streak >= 2) {
|
||||
streakDistribution.set(streak, (streakDistribution.get(streak) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const streakChart = Array.from(streakDistribution.entries())
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([days, userCount]) => ({ days, count: userCount }));
|
||||
|
||||
// ── Weekly user sets (for user-based velocity + churn) ───────────────────
|
||||
|
||||
const weekAUsers = new Set<string>();
|
||||
const weekBUsers = new Set<string>();
|
||||
const weekCUsers = new Set<string>();
|
||||
|
||||
for (const [userId, dates] of userDatesMap) {
|
||||
if (dates.some((d) => d >= weekAStart)) weekAUsers.add(userId);
|
||||
if (dates.some((d) => d >= weekBStart && d <= weekBEnd)) weekBUsers.add(userId);
|
||||
if (dates.some((d) => d >= weekCStart && d <= weekCEnd)) weekCUsers.add(userId);
|
||||
}
|
||||
|
||||
// First derivative: weekly unique users change
|
||||
const userVelocity = weekAUsers.size - weekBUsers.size;
|
||||
// Second derivative: is user growth speeding up or slowing down?
|
||||
const userAcceleration =
|
||||
weekAUsers.size - weekBUsers.size - (weekBUsers.size - weekCUsers.size);
|
||||
|
||||
// ── New players + churn ───────────────────────────────────────────────────
|
||||
// New players: anonymousIds whose first-ever completion falls in the last 7 days.
|
||||
// Checking against all-time data (not just the 90-day window) ensures accuracy.
|
||||
const firstDates = await db
|
||||
.select({
|
||||
anonymousId: dailyCompletions.anonymousId,
|
||||
firstDate: min(dailyCompletions.date),
|
||||
totalCompletions: count()
|
||||
})
|
||||
.from(dailyCompletions)
|
||||
.groupBy(dailyCompletions.anonymousId);
|
||||
|
||||
const newUsers7d = firstDates.filter((r) => r.firstDate != null && r.firstDate >= weekAStart).length;
|
||||
|
||||
// Churned: played in week B but not at all in week A
|
||||
const churned7d = [...weekBUsers].filter((id) => !weekAUsers.has(id)).length;
|
||||
|
||||
// Net growth = truly new arrivals minus departures
|
||||
const netGrowth7d = newUsers7d - churned7d;
|
||||
|
||||
// ── Session depth funnel ──────────────────────────────────────────────────
|
||||
// For each depth d, count players with >= d completions.
|
||||
// returnRate at depth d = (players with >= d+1) / (players with >= d).
|
||||
const depthCounts = new Map<number, number>();
|
||||
for (const r of firstDates) {
|
||||
const n = r.totalCompletions;
|
||||
for (let d = 1; d <= n; d++) {
|
||||
depthCounts.set(d, (depthCounts.get(d) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
const sessionDepthCards = [2, 3, 4, 5, 7].map((d) => {
|
||||
const atD = depthCounts.get(d) ?? 0;
|
||||
const atDplus1 = depthCounts.get(d + 1) ?? 0;
|
||||
return {
|
||||
depth: d,
|
||||
players: atD,
|
||||
returnRate: atD >= 3 ? Math.round((atDplus1 / atD) * 1000) / 10 : null
|
||||
};
|
||||
});
|
||||
|
||||
// ── Return rate ───────────────────────────────────────────────────────────
|
||||
// "Return rate": % of all-time unique players who have ever played more than once.
|
||||
const playersWithReturn = firstDates.filter((r) => r.totalCompletions >= 2).length;
|
||||
const overallReturnRate =
|
||||
firstDates.length > 0
|
||||
? Math.round((playersWithReturn / firstDates.length) * 1000) / 10
|
||||
: null;
|
||||
|
||||
// Daily new-player return rate: for each day D, what % of first-time players
|
||||
// on D ever came back (i.e. totalCompletions >= 2)?
|
||||
const dailyNewPlayerReturn = new Map<string, { cohort: number; returned: number }>();
|
||||
for (const r of firstDates) {
|
||||
if (!r.firstDate) continue;
|
||||
const existing = dailyNewPlayerReturn.get(r.firstDate) ?? { cohort: 0, returned: 0 };
|
||||
existing.cohort++;
|
||||
if (r.totalCompletions >= 2) existing.returned++;
|
||||
dailyNewPlayerReturn.set(r.firstDate, existing);
|
||||
}
|
||||
|
||||
// Build chronological array of daily rates (oldest first, days 60→1 ago)
|
||||
// Days with fewer than 3 new players get rate=null to exclude from rolling avg
|
||||
const dailyReturnRates: { date: string; cohort: number; rate: number | null }[] = [];
|
||||
for (let i = 60; i >= 1; i--) {
|
||||
const dateD = estDateStr(i);
|
||||
const d = dailyNewPlayerReturn.get(dateD);
|
||||
dailyReturnRates.push({
|
||||
date: dateD,
|
||||
cohort: d?.cohort ?? 0,
|
||||
rate: d && d.cohort >= 3 ? Math.round((d.returned / d.cohort) * 1000) / 10 : null
|
||||
});
|
||||
}
|
||||
|
||||
// 7-day trailing rolling average of the daily rates
|
||||
// Index 0 = 60 days ago, index 59 = yesterday
|
||||
const newPlayerReturnSeries = dailyReturnRates.map((r, idx) => {
|
||||
const window = dailyReturnRates
|
||||
.slice(Math.max(0, idx - 6), idx + 1)
|
||||
.filter((d) => d.rate !== null);
|
||||
const avg =
|
||||
window.length > 0
|
||||
? Math.round((window.reduce((sum, d) => sum + (d.rate ?? 0), 0) / window.length) * 10) /
|
||||
10
|
||||
: null;
|
||||
return { date: r.date, cohort: r.cohort, rate: r.rate, rollingAvg: avg };
|
||||
});
|
||||
|
||||
// Velocity: avg of last 7 complete days (idx 53–59) vs prior 7 (idx 46–52)
|
||||
const recentWindow = newPlayerReturnSeries.slice(53).filter((d) => d.rate !== null);
|
||||
const priorWindow = newPlayerReturnSeries.slice(46, 53).filter((d) => d.rate !== null);
|
||||
const current7dReturnAvg =
|
||||
recentWindow.length > 0
|
||||
? Math.round(
|
||||
(recentWindow.reduce((a, d) => a + (d.rate ?? 0), 0) / recentWindow.length) * 10
|
||||
) / 10
|
||||
: null;
|
||||
const prior7dReturnAvg =
|
||||
priorWindow.length > 0
|
||||
? Math.round(
|
||||
(priorWindow.reduce((a, d) => a + (d.rate ?? 0), 0) / priorWindow.length) * 10
|
||||
) / 10
|
||||
: null;
|
||||
const returnRateChange =
|
||||
current7dReturnAvg !== null && prior7dReturnAvg !== null
|
||||
? Math.round((current7dReturnAvg - prior7dReturnAvg) * 10) / 10
|
||||
: null;
|
||||
|
||||
// ── Retention over time ───────────────────────────────────────────────────
|
||||
// For each cohort day D, retention = % of that day's players who played
|
||||
// again within the next N days. Only compute for days where D+N is in the past.
|
||||
|
||||
function retentionSeries(
|
||||
windowDays: number,
|
||||
seriesLength: number
|
||||
): { date: string; rate: number; cohortSize: number }[] {
|
||||
// Earliest computable cohort day: today - (windowDays + 1)
|
||||
// We use index windowDays+1 through windowDays+seriesLength
|
||||
const series: { date: string; rate: number; cohortSize: number }[] = [];
|
||||
for (let i = windowDays + 1; i <= windowDays + seriesLength; i++) {
|
||||
const dateD = estDateStr(i);
|
||||
const cohort = dateUsersMap.get(dateD);
|
||||
if (!cohort || cohort.size < 3) continue; // skip tiny cohorts
|
||||
let retained = 0;
|
||||
for (const userId of cohort) {
|
||||
if (dateUsersMap.get(addDays(dateD, windowDays))?.has(userId)) {
|
||||
retained++;
|
||||
}
|
||||
}
|
||||
series.push({
|
||||
date: dateD,
|
||||
rate: Math.round((retained / cohort.size) * 1000) / 10,
|
||||
cohortSize: cohort.size
|
||||
});
|
||||
}
|
||||
return series; // newest first (loop iterates i from smallest = most recent)
|
||||
}
|
||||
|
||||
const retention7dSeries = retentionSeries(7, 30);
|
||||
const retention30dSeries = retentionSeries(30, 30);
|
||||
|
||||
// ── Weekly Active Users history (12 weeks) ────────────────────────────────
|
||||
|
||||
const wauWeeks: { weekStart: string; weekEnd: string; wau: number; changePct: number | null }[] = [];
|
||||
|
||||
for (let w = 0; w < 12; w++) {
|
||||
const weekEnd = estDateStr(w * 7);
|
||||
const weekStart = estDateStr(w * 7 + 6);
|
||||
const users = new Set<string>();
|
||||
for (const row of recentCompletions) {
|
||||
if (row.date >= weekStart && row.date <= weekEnd) {
|
||||
users.add(row.anonymousId);
|
||||
}
|
||||
}
|
||||
wauWeeks.push({ weekEnd, weekStart, wau: users.size, changePct: null });
|
||||
}
|
||||
|
||||
// Change % vs prior week (index i+1 is the older week)
|
||||
for (let i = 0; i < wauWeeks.length - 1; i++) {
|
||||
const prev = wauWeeks[i + 1].wau;
|
||||
if (prev > 0) {
|
||||
wauWeeks[i].changePct = Math.round(((wauWeeks[i].wau - prev) / prev) * 1000) / 10;
|
||||
}
|
||||
}
|
||||
|
||||
const avgWau = Math.round(wauWeeks.reduce((sum, w) => sum + w.wau, 0) / wauWeeks.length);
|
||||
|
||||
// ── Monthly Active Users history (6 months) ───────────────────────────────
|
||||
|
||||
const sixMonthsAgo = estDateStr(185);
|
||||
const mauCompletions = await db
|
||||
.select({ anonymousId: dailyCompletions.anonymousId, date: dailyCompletions.date })
|
||||
.from(dailyCompletions)
|
||||
.where(gte(dailyCompletions.date, sixMonthsAgo));
|
||||
|
||||
// Rolling 30-day windows
|
||||
const mauMonths: { monthStart: string; monthEnd: string; mau: number; changePct: number | null }[] = [];
|
||||
for (let m = 0; m < 6; m++) {
|
||||
const monthEnd = estDateStr(m * 30);
|
||||
const monthStart = estDateStr(m * 30 + 29);
|
||||
const users = new Set<string>();
|
||||
for (const row of mauCompletions) {
|
||||
if (row.date >= monthStart && row.date <= monthEnd) {
|
||||
users.add(row.anonymousId);
|
||||
}
|
||||
}
|
||||
mauMonths.push({ monthStart, monthEnd, mau: users.size, changePct: null });
|
||||
}
|
||||
for (let i = 0; i < mauMonths.length - 1; i++) {
|
||||
const prev = mauMonths[i + 1].mau;
|
||||
if (prev > 0) {
|
||||
mauMonths[i].changePct = Math.round(((mauMonths[i].mau - prev) / prev) * 1000) / 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar month windows
|
||||
const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const [todayYear, todayMonth, todayDay] = todayEst.split('-').map(Number);
|
||||
|
||||
const calendarMauMonths: {
|
||||
label: string;
|
||||
monthStart: string;
|
||||
monthEnd: string;
|
||||
mau: number;
|
||||
daysElapsed: number;
|
||||
daysInMonth: number;
|
||||
projectedMau: number | null;
|
||||
changePct: number | null;
|
||||
isCurrentMonth: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
let mo = todayMonth - i;
|
||||
let yr = todayYear;
|
||||
if (mo <= 0) { mo += 12; yr--; }
|
||||
|
||||
// new Date(yr, mo, 0) gives last day of month mo (1-indexed) in local time
|
||||
const daysInMonth = new Date(yr, mo, 0).getDate();
|
||||
const monthStart = `${yr}-${String(mo).padStart(2, '0')}-01`;
|
||||
const monthEnd = `${yr}-${String(mo).padStart(2, '0')}-${String(daysInMonth).padStart(2, '0')}`;
|
||||
const isCurrentMonth = i === 0;
|
||||
const daysElapsed = isCurrentMonth ? todayDay : daysInMonth;
|
||||
const queryEnd = isCurrentMonth ? todayEst : monthEnd;
|
||||
|
||||
const users = new Set<string>();
|
||||
for (const row of mauCompletions) {
|
||||
if (row.date >= monthStart && row.date <= queryEnd) {
|
||||
users.add(row.anonymousId);
|
||||
}
|
||||
}
|
||||
|
||||
const projectedMau = isCurrentMonth && daysElapsed > 0
|
||||
? Math.round(users.size * (daysInMonth / daysElapsed))
|
||||
: null;
|
||||
|
||||
calendarMauMonths.push({
|
||||
label: `${MONTH_NAMES[mo - 1]} ${yr}`,
|
||||
monthStart,
|
||||
monthEnd,
|
||||
mau: users.size,
|
||||
daysElapsed,
|
||||
daysInMonth,
|
||||
projectedMau,
|
||||
changePct: null,
|
||||
isCurrentMonth
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < calendarMauMonths.length - 1; i++) {
|
||||
const curr = calendarMauMonths[i];
|
||||
const prev = calendarMauMonths[i + 1];
|
||||
if (prev.mau > 0) {
|
||||
const compareVal = curr.projectedMau ?? curr.mau;
|
||||
curr.changePct = Math.round(((compareVal - prev.mau) / prev.mau) * 1000) / 10;
|
||||
}
|
||||
}
|
||||
const todayEst = new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });
|
||||
const yesterday = addDays(todayEst, -1);
|
||||
const snapshot = await ensureSnapshot(yesterday);
|
||||
const payload = snapshot.payload;
|
||||
|
||||
return {
|
||||
todayEst,
|
||||
sessionDepthCards,
|
||||
stats: {
|
||||
todayCount,
|
||||
totalCount,
|
||||
uniquePlayers,
|
||||
weeklyPlayers,
|
||||
activeStreaks,
|
||||
avgGuessesToday,
|
||||
registeredUsers,
|
||||
monthlyPlayers
|
||||
},
|
||||
growth: {
|
||||
completionsVelocity,
|
||||
completionsAcceleration,
|
||||
userVelocity,
|
||||
userAcceleration,
|
||||
newUsers7d,
|
||||
churned7d,
|
||||
netGrowth7d
|
||||
},
|
||||
last14Days,
|
||||
streakChart,
|
||||
retention7dSeries,
|
||||
retention30dSeries,
|
||||
overallReturnRate,
|
||||
newPlayerReturnSeries: newPlayerReturnSeries.slice(-30).reverse(),
|
||||
newPlayerReturnVelocity: {
|
||||
current7dAvg: current7dReturnAvg,
|
||||
prior7dAvg: prior7dReturnAvg,
|
||||
change: returnRateChange
|
||||
},
|
||||
wauWeeks,
|
||||
avgWau,
|
||||
mauMonths,
|
||||
calendarMauMonths
|
||||
asOfDate: payload.asOfDate,
|
||||
completions: payload.totals.completions,
|
||||
players: payload.totals.players,
|
||||
weeklyPlayers: payload.activity.wau,
|
||||
monthlyPlayers: payload.activity.mau,
|
||||
yesterdayCompletions: payload.totals.yesterdayCompletions,
|
||||
yesterdayAvgGuesses: payload.totals.yesterdayAvgGuesses,
|
||||
calendar: payload.habit.calendar.slice(-30)
|
||||
};
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user