From c9691c5300ca9b5cdb865b6e49239749a8e8d67d Mon Sep 17 00:00:00 2001 From: Drew McMillan Date: Sat, 8 Nov 2025 10:46:42 +0000 Subject: [PATCH] stats --- package-lock.json | 53 ++++++++++++++------- package.json | 1 + scripts/stats/blog-posts.js | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 scripts/stats/blog-posts.js diff --git a/package-lock.json b/package-lock.json index 66a2959a..4366af53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "concurrently": "^7.3.0", "cross-env": "^7.0.3", "csvtojson": "^2.0.10", + "date-fns": "^4.1.0", "firebase": "^9.0.0", "firebase-functions": "^6.3.2", "firebase-tools": "^13.33.0", @@ -1569,7 +1570,6 @@ "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1580,8 +1580,7 @@ "node_modules/@babel/runtime/node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/@babel/template": { "version": "7.26.9", @@ -6125,6 +6124,22 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/concurrently/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/concurrently/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6573,15 +6588,13 @@ } }, "node_modules/date-fns": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz", - "integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==", - "engines": { - "node": ">=0.11" - }, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/dateformat": { @@ -18201,7 +18214,6 @@ "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", - "dev": true, "requires": { "regenerator-runtime": "^0.14.0" }, @@ -18209,8 +18221,7 @@ "regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" } } }, @@ -21720,6 +21731,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -22045,9 +22064,9 @@ } }, "date-fns": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz", - "integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" }, "dateformat": { "version": "3.0.3", diff --git a/package.json b/package.json index 5b53c730..c58ce0cb 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "concurrently": "^7.3.0", "cross-env": "^7.0.3", "csvtojson": "^2.0.10", + "date-fns": "^4.1.0", "firebase": "^9.0.0", "firebase-functions": "^6.3.2", "firebase-tools": "^13.33.0", diff --git a/scripts/stats/blog-posts.js b/scripts/stats/blog-posts.js new file mode 100644 index 00000000..ad7ba727 --- /dev/null +++ b/scripts/stats/blog-posts.js @@ -0,0 +1,92 @@ +const { getPaginatedTableItems } = require("../utils/dynamodb"); +const fs = require("fs"); +const path = require("path"); +const { format } = require("date-fns"); + +const tutorialsPath = path.join(__dirname, `../../private/blog-tutorials.json`); +const tutorialAttendancesPath = path.join(__dirname, `../../private/blog-tutorial-attendances.json`); +const circlesPath = path.join(__dirname, `../../private/blog-circles.json`); +const answerCountPath = path.join(__dirname, `../../private/blog-answer-count.json`); +const circleAttendancePath = path.join(__dirname, `../../private/blog-circle-attendance.json`); +const circleAttendanceCountPath = path.join(__dirname, `../../private/blog-circle-attendance-count.json`); + +const download = async () => { + const circles = await getPaginatedTableItems("prod-lives"); + const answerCount = await getPaginatedTableItems("prod-live_answer_count"); + const circleAttendance = await getPaginatedTableItems("prod-live_attendance"); + const circleAttendanceCount = await getPaginatedTableItems("prod-live_attendance_count"); + const tutorials = await getPaginatedTableItems("prod-euclid-tutorial"); + const tutorialAttendances = await getPaginatedTableItems("prod-euclid-attendance"); + + fs.writeFileSync(tutorialsPath, JSON.stringify(tutorials, null, 2)); + fs.writeFileSync(tutorialAttendancesPath, JSON.stringify(await tutorialAttendances, null, 2)); + fs.writeFileSync(circlesPath, JSON.stringify(await circles, null, 2)); + fs.writeFileSync(answerCountPath, JSON.stringify(await answerCount, null, 2)); + fs.writeFileSync(circleAttendancePath, JSON.stringify(await circleAttendance, null, 2)); + fs.writeFileSync(circleAttendanceCountPath, JSON.stringify(await circleAttendanceCount, null, 2)); +}; + +// const circles = JSON.parse(fs.readFileSync(circlesPath)); +// const answerCount = JSON.parse(fs.readFileSync(answerCountPath)); +// const circleAttendance = JSON.parse(fs.readFileSync(circleAttendancePath)); +// const circleAttendanceCount = JSON.parse(fs.readFileSync(circleAttendanceCountPath)); +// const tutorials = JSON.parse(fs.readFileSync(tutorialsPath)); +// const tutorialAttendances = JSON.parse(fs.readFileSync(tutorialAttendancesPath)); + +const circlesAttendance = async () => { + const circles = JSON.parse(fs.readFileSync(circlesPath)); + const circleAttendanceCount = JSON.parse(fs.readFileSync(circleAttendanceCountPath)); + + const circleAttendanceCountsByLiveId = {}; + + for (const attendanceCount of circleAttendanceCount) { + if (!circleAttendanceCountsByLiveId[attendanceCount.liveId]) { + circleAttendanceCountsByLiveId[attendanceCount.liveId] = []; + } + circleAttendanceCountsByLiveId[attendanceCount.liveId].push(attendanceCount); + } + + const pastCircles = circles.filter((c) => c.startTimestamp < Date.now()); + + let loggedInMinutes = 0; + let anonymousMinutes = 0; + let circleCount = 0; + + for (const circles of pastCircles) { + const startTimestamp = circles.startTimestamp; + let endTimestamp = circles.endTimestamp || startTimestamp + 60 * 60 * 1000; + const duration = (endTimestamp - startTimestamp) / (60 * 1000); // in minutes + + const attendanceCountsForLive = (circleAttendanceCountsByLiveId[circles.id] || []) + .map((ac) => ({ ...ac, total: ac.loggedInCount + ac.anonymousCount })) + .sort((a, b) => b.total - a.total) + .slice(0, duration); + let individualMinutes = 0; + + for (const attendanceCount of attendanceCountsForLive) { + loggedInMinutes += attendanceCount.loggedInCount || 0; + anonymousMinutes += attendanceCount.anonymousCount || 0; + individualMinutes += (attendanceCount.loggedInCount || 0) + (attendanceCount.anonymousCount || 0); + } + + if (individualMinutes > 0) { + circleCount++; + } + } + + console.log("\n\n\n"); + console.log("Circles\n"); + console.log(circleCount, "Circles"); + console.log(((loggedInMinutes / (anonymousMinutes + loggedInMinutes)) * 100).toFixed(2), "Percentage logged in"); + console.log(loggedInMinutes, "Logged in minutes of learning"); + console.log(anonymousMinutes, "Anonymous minutes of learning"); + const tot = loggedInMinutes + anonymousMinutes; + console.log(tot, "Minutes of learning"); + console.log((tot / 60).toFixed(2), "Hours of learning"); + console.log((tot / 60 / 24).toFixed(2), "Days of learning"); + console.log((tot / 60 / 24 / 365).toFixed(2), "Years of learning"); + console.log("\n\n\n"); +}; + +// download(); +circlesAttendance();