Fix analytics data-loss bugs and clean up the admin dashboard; v2.2.4
Publish Container / build (push) Successful in 16m3s
Publish Container / build (push) Successful in 16m3s
- Self-heal consent desync: if the server's consent cookie and the client's localStorage flag fall out of sync, the client now notices and stops silently sending events into a void. - Stop double-counting hard page loads in hit/visitor stats. - Flush pending disk writes on SIGTERM/SIGINT so redeploys don't drop the last few events. - Log rate-limiter drops once per window (diagnoses proxy misconfig). - Widen the bot-detection allowlist to avoid flagging real browsers (Chrome-iOS, Samsung Browser, etc.) as bots. - Stop merging distinct visitors who share an IP when a real session cookie is already present — only fall back to IP correlation when no cookie was sent at all. - Include analyticsEvents/episodePlays in backup snapshots and restore them, so a restore no longer wipes engagement analytics. - Regroup admin dashboard KPI cards by time scope (range vs all-time) and remove a duplicated Top Countries/States block. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8496e227b3
commit
d1a68754a1
9 files changed
+183
-45
No files matched your search
@@ -232,8 +232,67 @@ Promise.all([
|
||||
console.error('[series-scheduler] error:', err)
|
||||
})
|
||||
|
||||
app.listen(PORT, () => {
|
||||
const server = app.listen(PORT, () => {
|
||||
logResendEmailAlignmentWarnings()
|
||||
console.log(`Siteforge listening on http://localhost:${PORT}`)
|
||||
})
|
||||
|
||||
// On every deploy the running container gets SIGTERM'd and replaced.
|
||||
// All the queueXWrite() helpers in data.js debounce disk writes onto
|
||||
// per-resource promise chains — without draining those here, a redeploy
|
||||
// can kill the process mid-write and silently lose the last few
|
||||
// analytics events (or any other pending write) of that container's life.
|
||||
function collectPendingWrites() {
|
||||
return [
|
||||
state.hitStatsWritePromise,
|
||||
state.visitorStatsWritePromise,
|
||||
state.contactSubmissionsWritePromise,
|
||||
state.questionsWritePromise,
|
||||
state.draftQuestionsWritePromise,
|
||||
state.replyTemplatesWritePromise,
|
||||
state.replyHistoryWritePromise,
|
||||
state.podcastChecklistWritePromise,
|
||||
state.studyUsersWritePromise,
|
||||
state.studyCommunityWritePromise,
|
||||
state.studyRemindersWritePromise,
|
||||
state.studyCommentsWritePromise,
|
||||
state.studyCertificatesWritePromise,
|
||||
state.episodeScriptsWritePromise,
|
||||
state.downloadCountsWritePromise,
|
||||
state.episodePlaysWritePromise,
|
||||
state.analyticsEventsWritePromise,
|
||||
state.qrCodesWritePromise,
|
||||
state.emailSettingsWritePromise,
|
||||
state.calendarEventsWritePromise,
|
||||
state.auditLogWritePromise,
|
||||
state.webhooksWritePromise,
|
||||
state.studySessionsWritePromise,
|
||||
...state.studyNotesWriteQueues.values(),
|
||||
...state.studyProgressWriteQueues.values(),
|
||||
]
|
||||
}
|
||||
|
||||
let shuttingDown = false
|
||||
async function gracefulShutdown(signal) {
|
||||
if (shuttingDown) return
|
||||
shuttingDown = true
|
||||
console.log(`[shutdown] ${signal} received — closing server and flushing pending writes...`)
|
||||
server.close()
|
||||
const forceExit = setTimeout(() => {
|
||||
console.error('[shutdown] timed out waiting for pending writes; exiting anyway')
|
||||
process.exit(1)
|
||||
}, 10_000)
|
||||
try {
|
||||
await Promise.all(collectPendingWrites())
|
||||
console.log('[shutdown] all pending writes flushed')
|
||||
} catch (err) {
|
||||
console.error('[shutdown] error while flushing writes:', err)
|
||||
} finally {
|
||||
clearTimeout(forceExit)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
|
||||
})
|
||||
Reference in new issue
Block a user