dubs-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Cohort Retention Data Analysis - Heroku Production
3
+ *
4
+ * This script analyzes the production Heroku database.
5
+ *
6
+ * Usage:
7
+ * heroku config:get DATABASE_URL --app dubs-server-prod > /tmp/db_url.txt
8
+ * DATABASE_URL=$(cat /tmp/db_url.txt) node scripts/analyze-cohort-data-heroku.js
9
+ *
10
+ * Or use the Heroku CLI directly:
11
+ * heroku run node scripts/analyze-cohort-data-heroku.js --app dubs-server-prod
12
+ */
13
+
14
+ const { Pool } = require('pg');
15
+
16
+ async function analyzeCohortData() {
17
+ const DATABASE_URL = process.env.DATABASE_URL;
18
+
19
+ if (!DATABASE_URL) {
20
+ console.error('āŒ DATABASE_URL environment variable not set!');
21
+ console.error(' Set it with: export DATABASE_URL=<your-heroku-postgres-url>');
22
+ process.exit(1);
23
+ }
24
+
25
+ const isProduction = DATABASE_URL.includes('amazonaws') || DATABASE_URL.includes('heroku');
26
+
27
+ const pool = new Pool({
28
+ connectionString: DATABASE_URL,
29
+ ssl: isProduction ? { rejectUnauthorized: false } : false,
30
+ max: 2, // Use minimal connections for analysis
31
+ });
32
+
33
+ console.log('\nšŸ” COHORT RETENTION DATA ANALYSIS (PRODUCTION)\n');
34
+ console.log('=' .repeat(80));
35
+ console.log(`Database: ${DATABASE_URL.split('@')[1]?.split('/')[0] || 'unknown'}`);
36
+ console.log('=' .repeat(80));
37
+
38
+ try {
39
+ // 1. Check for registration events
40
+ console.log('\nšŸ“Š 1. REGISTRATION EVENTS');
41
+ console.log('-'.repeat(80));
42
+
43
+ const registrationEvents = await pool.query(`
44
+ SELECT
45
+ log_type,
46
+ COUNT(*) as total_events,
47
+ COUNT(DISTINCT user_id) as unique_users,
48
+ MIN(created_at) as first_event,
49
+ MAX(created_at) as last_event
50
+ FROM audit_logs
51
+ WHERE log_type IN ('registration_started', 'registration_completed', 'registration_submitted')
52
+ GROUP BY log_type
53
+ ORDER BY total_events DESC
54
+ `);
55
+
56
+ if (registrationEvents.rows.length === 0) {
57
+ console.log('āŒ No registration events found in database!');
58
+ console.log(' Cannot build cohort analysis without registration data.');
59
+ await pool.end();
60
+ return;
61
+ }
62
+
63
+ console.log('\nRegistration Events Summary:');
64
+ registrationEvents.rows.forEach(row => {
65
+ console.log(` ${row.log_type}:`);
66
+ console.log(` - Total Events: ${row.total_events}`);
67
+ console.log(` - Unique Users: ${row.unique_users}`);
68
+ console.log(` - Date Range: ${row.first_event?.toISOString().split('T')[0]} to ${row.last_event?.toISOString().split('T')[0]}`);
69
+ });
70
+
71
+ // 2. Check date range and volume
72
+ console.log('\n\nšŸ“… 2. SIGNUP DATE DISTRIBUTION (Last 30 days)');
73
+ console.log('-'.repeat(80));
74
+
75
+ const dailySignups = await pool.query(`
76
+ SELECT
77
+ DATE(created_at) as signup_date,
78
+ COUNT(*) as signups,
79
+ COUNT(DISTINCT user_id) as unique_users
80
+ FROM audit_logs
81
+ WHERE log_type = 'registration_completed'
82
+ AND user_id IS NOT NULL
83
+ GROUP BY DATE(created_at)
84
+ ORDER BY signup_date DESC
85
+ LIMIT 30
86
+ `);
87
+
88
+ if (dailySignups.rows.length === 0) {
89
+ console.log(' No signup data found!');
90
+ } else {
91
+ dailySignups.rows.forEach(row => {
92
+ console.log(` ${row.signup_date.toISOString().split('T')[0]}: ${row.signups} signups (${row.unique_users} unique users)`);
93
+ });
94
+ }
95
+
96
+ // 3. Weekly cohort summary
97
+ console.log('\n\nšŸ“Š 3. WEEKLY COHORT SUMMARY');
98
+ console.log('-'.repeat(80));
99
+
100
+ const weeklyCohorts = await pool.query(`
101
+ SELECT
102
+ DATE_TRUNC('week', DATE(created_at)) as cohort_week,
103
+ COUNT(*) as signups,
104
+ COUNT(DISTINCT user_id) as unique_users
105
+ FROM audit_logs
106
+ WHERE log_type = 'registration_completed'
107
+ AND user_id IS NOT NULL
108
+ GROUP BY DATE_TRUNC('week', DATE(created_at))
109
+ ORDER BY cohort_week DESC
110
+ LIMIT 12
111
+ `);
112
+
113
+ console.log('\nLast 12 weeks of signups:');
114
+ if (weeklyCohorts.rows.length === 0) {
115
+ console.log(' No weekly cohort data found!');
116
+ } else {
117
+ console.log('\n Week Starting | Signups | Unique Users');
118
+ console.log(' ' + '-'.repeat(50));
119
+ weeklyCohorts.rows.forEach(row => {
120
+ const week = row.cohort_week.toISOString().split('T')[0];
121
+ console.log(` ${week} | ${row.signups.toString().padStart(4)} | ${row.unique_users.toString().padStart(4)}`);
122
+ });
123
+ }
124
+
125
+ // 4. Sample retention calculation
126
+ console.log('\n\nšŸ”¬ 4. SAMPLE COHORT RETENTION (First 4 weeks)');
127
+ console.log('-'.repeat(80));
128
+
129
+ const cohortRetention = await pool.query(`
130
+ WITH signups AS (
131
+ SELECT
132
+ user_id,
133
+ MIN(DATE(created_at)) as signup_date
134
+ FROM audit_logs
135
+ WHERE log_type = 'registration_completed'
136
+ AND user_id IS NOT NULL
137
+ GROUP BY user_id
138
+ ),
139
+ weekly_cohorts AS (
140
+ SELECT
141
+ DATE_TRUNC('week', signup_date) as cohort_week,
142
+ user_id,
143
+ signup_date
144
+ FROM signups
145
+ ),
146
+ user_activity AS (
147
+ SELECT
148
+ wc.cohort_week,
149
+ wc.user_id,
150
+ wc.signup_date,
151
+ DATE(al.created_at) as activity_date,
152
+ al.created_at - wc.signup_date as time_since_signup
153
+ FROM weekly_cohorts wc
154
+ LEFT JOIN audit_logs al ON al.user_id = wc.user_id
155
+ AND DATE(al.created_at) >= wc.signup_date
156
+ ),
157
+ retention_calc AS (
158
+ SELECT
159
+ cohort_week,
160
+ COUNT(DISTINCT user_id) as total_users,
161
+ COUNT(DISTINCT CASE
162
+ WHEN time_since_signup >= INTERVAL '1 day'
163
+ AND time_since_signup < INTERVAL '2 days'
164
+ THEN user_id
165
+ END) as d1_users,
166
+ COUNT(DISTINCT CASE
167
+ WHEN time_since_signup >= INTERVAL '7 days'
168
+ AND time_since_signup < INTERVAL '8 days'
169
+ THEN user_id
170
+ END) as d7_users,
171
+ COUNT(DISTINCT CASE
172
+ WHEN time_since_signup >= INTERVAL '14 days'
173
+ AND time_since_signup < INTERVAL '15 days'
174
+ THEN user_id
175
+ END) as d14_users,
176
+ COUNT(DISTINCT CASE
177
+ WHEN time_since_signup >= INTERVAL '30 days'
178
+ AND time_since_signup < INTERVAL '31 days'
179
+ THEN user_id
180
+ END) as d30_users
181
+ FROM user_activity
182
+ GROUP BY cohort_week
183
+ )
184
+ SELECT
185
+ cohort_week,
186
+ total_users,
187
+ d1_users,
188
+ CASE WHEN total_users > 0
189
+ THEN ROUND(100.0 * d1_users / total_users, 1)
190
+ ELSE 0
191
+ END as d1_pct,
192
+ d7_users,
193
+ CASE WHEN total_users > 0
194
+ THEN ROUND(100.0 * d7_users / total_users, 1)
195
+ ELSE 0
196
+ END as d7_pct,
197
+ d14_users,
198
+ CASE WHEN total_users > 0
199
+ THEN ROUND(100.0 * d14_users / total_users, 1)
200
+ ELSE 0
201
+ END as d14_pct,
202
+ d30_users,
203
+ CASE WHEN total_users > 0
204
+ THEN ROUND(100.0 * d30_users / total_users, 1)
205
+ ELSE 0
206
+ END as d30_pct
207
+ FROM retention_calc
208
+ WHERE total_users > 0
209
+ ORDER BY cohort_week DESC
210
+ LIMIT 4
211
+ `);
212
+
213
+ if (cohortRetention.rows.length === 0) {
214
+ console.log(' āŒ Cannot calculate cohort retention - insufficient data');
215
+ } else {
216
+ console.log('\n Cohort Week | Users | D1 | D1% | D7 | D7% | D14 | D14% | D30 | D30%');
217
+ console.log(' ' + '-'.repeat(90));
218
+ cohortRetention.rows.forEach(row => {
219
+ const week = row.cohort_week.toISOString().split('T')[0];
220
+ const users = row.total_users.toString().padStart(5);
221
+ const d1 = row.d1_users.toString().padStart(5);
222
+ const d1pct = `${row.d1_pct}%`.padStart(5);
223
+ const d7 = row.d7_users.toString().padStart(5);
224
+ const d7pct = `${row.d7_pct}%`.padStart(5);
225
+ const d14 = row.d14_users.toString().padStart(5);
226
+ const d14pct = `${row.d14_pct}%`.padStart(5);
227
+ const d30 = row.d30_users.toString().padStart(5);
228
+ const d30pct = `${row.d30_pct}%`.padStart(5);
229
+ console.log(` ${week} | ${users} | ${d1} | ${d1pct} | ${d7} | ${d7pct} | ${d14} | ${d14pct} | ${d30} | ${d30pct}`);
230
+ });
231
+
232
+ console.log('\n āœ… Cohort retention calculation is WORKING!');
233
+ }
234
+
235
+ // 5. Total stats
236
+ console.log('\n\nšŸ“‹ 5. OVERALL STATISTICS');
237
+ console.log('-'.repeat(80));
238
+
239
+ const totalStats = await pool.query(`
240
+ SELECT
241
+ COUNT(DISTINCT user_id) as total_registered_users,
242
+ MIN(created_at) as first_registration,
243
+ MAX(created_at) as last_registration,
244
+ EXTRACT(DAY FROM MAX(created_at) - MIN(created_at)) as days_of_data
245
+ FROM audit_logs
246
+ WHERE log_type = 'registration_completed'
247
+ AND user_id IS NOT NULL
248
+ `);
249
+
250
+ if (totalStats.rows.length > 0) {
251
+ const stats = totalStats.rows[0];
252
+ console.log(`\n Total Registered Users: ${stats.total_registered_users}`);
253
+ console.log(` First Registration: ${stats.first_registration?.toISOString().split('T')[0]}`);
254
+ console.log(` Last Registration: ${stats.last_registration?.toISOString().split('T')[0]}`);
255
+ console.log(` Days of Data: ${Math.floor(stats.days_of_data)}`);
256
+
257
+ console.log('\nāœ… COHORT RETENTION ANALYSIS IS READY TO BUILD!');
258
+ console.log('\nšŸ“Š Next steps:');
259
+ console.log(' 1. Create /api/analytics/cohort-retention endpoint');
260
+ console.log(' 2. Add UI component to AnalyticsDashboard');
261
+ console.log(' 3. Add CSV export for community manager');
262
+ }
263
+
264
+ } catch (error) {
265
+ console.error('\nāŒ Error analyzing data:', error.message);
266
+ console.error(error);
267
+ } finally {
268
+ console.log('\n' + '='.repeat(80));
269
+ console.log('\n');
270
+ await pool.end();
271
+ }
272
+ }
273
+
274
+ // Run analysis
275
+ analyzeCohortData().catch(console.error);
276
+
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Cohort Retention Data Analysis
3
+ *
4
+ * This script analyzes the audit_logs table to determine if we have
5
+ * sufficient data to build cohort retention reports.
6
+ *
7
+ * Usage:
8
+ * node scripts/analyze-cohort-data.js
9
+ */
10
+
11
+ require('dotenv').config();
12
+ const { pool } = require('../services/db');
13
+
14
+ async function analyzeCohortData() {
15
+ console.log('\nšŸ” COHORT RETENTION DATA ANALYSIS\n');
16
+ console.log('=' .repeat(80));
17
+
18
+ try {
19
+ // 1. Check for registration events
20
+ console.log('\nšŸ“Š 1. REGISTRATION EVENTS');
21
+ console.log('-'.repeat(80));
22
+
23
+ const registrationEvents = await pool.query(`
24
+ SELECT
25
+ log_type,
26
+ COUNT(*) as total_events,
27
+ COUNT(DISTINCT user_id) as unique_users,
28
+ MIN(created_at) as first_event,
29
+ MAX(created_at) as last_event
30
+ FROM audit_logs
31
+ WHERE log_type IN ('registration_started', 'registration_completed', 'registration_submitted')
32
+ GROUP BY log_type
33
+ ORDER BY total_events DESC
34
+ `);
35
+
36
+ if (registrationEvents.rows.length === 0) {
37
+ console.log('āŒ No registration events found in database!');
38
+ console.log(' Cannot build cohort analysis without registration data.');
39
+ return;
40
+ }
41
+
42
+ console.log('\nRegistration Events Summary:');
43
+ registrationEvents.rows.forEach(row => {
44
+ console.log(` ${row.log_type}:`);
45
+ console.log(` - Total Events: ${row.total_events}`);
46
+ console.log(` - Unique Users: ${row.unique_users}`);
47
+ console.log(` - Date Range: ${row.first_event?.toISOString().split('T')[0]} to ${row.last_event?.toISOString().split('T')[0]}`);
48
+ });
49
+
50
+ // 2. Check date range and volume
51
+ console.log('\n\nšŸ“… 2. SIGNUP DATE DISTRIBUTION');
52
+ console.log('-'.repeat(80));
53
+
54
+ const dailySignups = await pool.query(`
55
+ SELECT
56
+ DATE(created_at) as signup_date,
57
+ COUNT(*) as signups,
58
+ COUNT(DISTINCT user_id) as unique_users
59
+ FROM audit_logs
60
+ WHERE log_type = 'registration_completed'
61
+ AND user_id IS NOT NULL
62
+ GROUP BY DATE(created_at)
63
+ ORDER BY signup_date DESC
64
+ LIMIT 30
65
+ `);
66
+
67
+ console.log('\nLast 30 days of signups:');
68
+ if (dailySignups.rows.length === 0) {
69
+ console.log(' No signup data found!');
70
+ } else {
71
+ dailySignups.rows.forEach(row => {
72
+ console.log(` ${row.signup_date.toISOString().split('T')[0]}: ${row.signups} signups (${row.unique_users} unique users)`);
73
+ });
74
+ }
75
+
76
+ // 3. Check for user activity patterns
77
+ console.log('\n\nšŸ‘„ 3. USER ACTIVITY PATTERNS (Sample)');
78
+ console.log('-'.repeat(80));
79
+
80
+ const userActivity = await pool.query(`
81
+ WITH user_signups AS (
82
+ SELECT
83
+ user_id,
84
+ MIN(created_at) as signup_date
85
+ FROM audit_logs
86
+ WHERE log_type = 'registration_completed'
87
+ AND user_id IS NOT NULL
88
+ GROUP BY user_id
89
+ LIMIT 5
90
+ ),
91
+ user_events AS (
92
+ SELECT
93
+ us.user_id,
94
+ us.signup_date,
95
+ DATE(al.created_at) as activity_date,
96
+ COUNT(*) as events_count,
97
+ COUNT(DISTINCT al.log_type) as unique_event_types,
98
+ EXTRACT(DAY FROM MIN(al.created_at) - us.signup_date) as days_since_signup
99
+ FROM user_signups us
100
+ LEFT JOIN audit_logs al ON al.user_id = us.user_id
101
+ AND al.created_at >= us.signup_date
102
+ GROUP BY us.user_id, us.signup_date, DATE(al.created_at)
103
+ ORDER BY us.user_id, activity_date
104
+ )
105
+ SELECT * FROM user_events
106
+ ORDER BY user_id, activity_date
107
+ LIMIT 50
108
+ `);
109
+
110
+ console.log('\nSample user activity (first 5 users):');
111
+ if (userActivity.rows.length === 0) {
112
+ console.log(' No user activity data found!');
113
+ } else {
114
+ let currentUser = null;
115
+ userActivity.rows.forEach(row => {
116
+ if (row.user_id !== currentUser) {
117
+ currentUser = row.user_id;
118
+ console.log(`\n User: ${row.user_id.substring(0, 8)}...`);
119
+ console.log(` Signup: ${row.signup_date.toISOString().split('T')[0]}`);
120
+ }
121
+ console.log(` Day ${row.days_since_signup}: ${row.events_count} events (${row.unique_event_types} types)`);
122
+ });
123
+ }
124
+
125
+ // 4. Check for referral/source tracking
126
+ console.log('\n\nšŸŽÆ 4. SOURCE/REFERRAL TRACKING');
127
+ console.log('-'.repeat(80));
128
+
129
+ const sourceTracking = await pool.query(`
130
+ SELECT
131
+ metadata->>'hasReferralCode' as has_referral,
132
+ metadata->>'referralCode' as referral_code,
133
+ COUNT(*) as count
134
+ FROM audit_logs
135
+ WHERE log_type = 'registration_completed'
136
+ AND user_id IS NOT NULL
137
+ GROUP BY metadata->>'hasReferralCode', metadata->>'referralCode'
138
+ ORDER BY count DESC
139
+ LIMIT 10
140
+ `);
141
+
142
+ console.log('\nReferral tracking in registrations:');
143
+ if (sourceTracking.rows.length === 0) {
144
+ console.log(' No referral data found in metadata');
145
+ } else {
146
+ sourceTracking.rows.forEach(row => {
147
+ console.log(` Has Referral: ${row.has_referral || 'unknown'}, Code: ${row.referral_code || 'none'} - ${row.count} users`);
148
+ });
149
+ }
150
+
151
+ // 5. Test cohort query structure
152
+ console.log('\n\nšŸ”¬ 5. COHORT RETENTION FEASIBILITY TEST');
153
+ console.log('-'.repeat(80));
154
+
155
+ const cohortTest = await pool.query(`
156
+ WITH signups AS (
157
+ -- Get first registration event per user
158
+ SELECT
159
+ user_id,
160
+ MIN(DATE(created_at)) as signup_date
161
+ FROM audit_logs
162
+ WHERE log_type = 'registration_completed'
163
+ AND user_id IS NOT NULL
164
+ GROUP BY user_id
165
+ ),
166
+ weekly_cohorts AS (
167
+ -- Group signups into weekly cohorts
168
+ SELECT
169
+ DATE_TRUNC('week', signup_date) as cohort_week,
170
+ user_id,
171
+ signup_date
172
+ FROM signups
173
+ ),
174
+ user_activity AS (
175
+ -- Get all user activity after signup
176
+ SELECT
177
+ wc.cohort_week,
178
+ wc.user_id,
179
+ wc.signup_date,
180
+ DATE(al.created_at) as activity_date,
181
+ al.created_at - wc.signup_date as time_since_signup
182
+ FROM weekly_cohorts wc
183
+ LEFT JOIN audit_logs al ON al.user_id = wc.user_id
184
+ AND DATE(al.created_at) >= wc.signup_date
185
+ ),
186
+ retention_calc AS (
187
+ -- Calculate retention metrics
188
+ SELECT
189
+ cohort_week,
190
+ COUNT(DISTINCT user_id) as total_users,
191
+ COUNT(DISTINCT CASE
192
+ WHEN time_since_signup >= INTERVAL '1 day'
193
+ AND time_since_signup < INTERVAL '2 days'
194
+ THEN user_id
195
+ END) as d1_users,
196
+ COUNT(DISTINCT CASE
197
+ WHEN time_since_signup >= INTERVAL '7 days'
198
+ AND time_since_signup < INTERVAL '8 days'
199
+ THEN user_id
200
+ END) as d7_users
201
+ FROM user_activity
202
+ GROUP BY cohort_week
203
+ )
204
+ SELECT
205
+ cohort_week,
206
+ total_users,
207
+ d1_users,
208
+ CASE WHEN total_users > 0
209
+ THEN ROUND(100.0 * d1_users / total_users, 1)
210
+ ELSE 0
211
+ END as d1_pct,
212
+ d7_users,
213
+ CASE WHEN total_users > 0
214
+ THEN ROUND(100.0 * d7_users / total_users, 1)
215
+ ELSE 0
216
+ END as d7_pct
217
+ FROM retention_calc
218
+ WHERE total_users > 0
219
+ ORDER BY cohort_week DESC
220
+ LIMIT 10
221
+ `);
222
+
223
+ console.log('\nSample cohort retention (last 10 weeks):');
224
+ if (cohortTest.rows.length === 0) {
225
+ console.log(' āŒ Cannot calculate cohort retention - insufficient data');
226
+ } else {
227
+ console.log('\n Cohort Week | Signups | D1 Users | D1% | D7 Users | D7%');
228
+ console.log(' ' + '-'.repeat(70));
229
+ cohortTest.rows.forEach(row => {
230
+ const week = row.cohort_week.toISOString().split('T')[0];
231
+ const signups = row.total_users.toString().padStart(7);
232
+ const d1 = row.d1_users.toString().padStart(8);
233
+ const d1pct = `${row.d1_pct}%`.padStart(4);
234
+ const d7 = row.d7_users.toString().padStart(8);
235
+ const d7pct = `${row.d7_pct}%`.padStart(4);
236
+ console.log(` ${week} | ${signups} | ${d1} | ${d1pct} | ${d7} | ${d7pct}`);
237
+ });
238
+
239
+ console.log('\n āœ… Cohort retention calculation is FEASIBLE!');
240
+ }
241
+
242
+ // 6. Summary and recommendations
243
+ console.log('\n\nšŸ“‹ 6. SUMMARY & RECOMMENDATIONS');
244
+ console.log('-'.repeat(80));
245
+
246
+ const totalSignups = registrationEvents.rows.find(r => r.log_type === 'registration_completed');
247
+ const oldestSignup = dailySignups.rows[dailySignups.rows.length - 1];
248
+ const newestSignup = dailySignups.rows[0];
249
+
250
+ if (totalSignups && oldestSignup && newestSignup) {
251
+ console.log('\nāœ… DATA AVAILABILITY:');
252
+ console.log(` - Total Signups: ${totalSignups.total_events} events, ${totalSignups.unique_users} users`);
253
+ console.log(` - Date Range: ${oldestSignup.signup_date.toISOString().split('T')[0]} to ${newestSignup.signup_date.toISOString().split('T')[0]}`);
254
+ console.log(` - Cohort Analysis: POSSIBLE`);
255
+
256
+ console.log('\nšŸ“Š RECOMMENDED COHORT GROUPINGS:');
257
+ console.log(' - Weekly cohorts (best for initial analysis)');
258
+ console.log(' - Monthly cohorts (for long-term trends)');
259
+ console.log(' - Daily cohorts (if high volume)');
260
+
261
+ console.log('\nšŸ“ˆ METRICS WE CAN TRACK:');
262
+ console.log(' āœ… signup_cohort (week/month starting)');
263
+ console.log(' āœ… signup_date_range (date range string)');
264
+ console.log(' āœ… total_signups (count)');
265
+ console.log(' āœ… d1_users, d1_pct (day 1 retention)');
266
+ console.log(' āœ… d7_users, d7_pct (day 7 retention)');
267
+ console.log(' āœ… d14_users, d14_pct (day 14 retention)');
268
+ console.log(' āœ… d30_users, d30_pct (day 30 retention)');
269
+ console.log(sourceTracking.rows.length > 0 ? ' āœ… source (from referral tracking)' : ' āš ļø source (limited/no tracking)');
270
+
271
+ console.log('\nšŸš€ NEXT STEPS:');
272
+ console.log(' 1. Create /api/analytics/cohort-retention endpoint');
273
+ console.log(' 2. Support weekly and monthly cohort grouping');
274
+ console.log(' 3. Add source/referral filter if available');
275
+ console.log(' 4. Build visualization in AnalyticsDashboard');
276
+ console.log(' 5. Export to CSV for community manager');
277
+ } else {
278
+ console.log('\nāŒ INSUFFICIENT DATA:');
279
+ console.log(' Cannot build cohort retention without registration events');
280
+ console.log(' Ensure registration_completed events are being tracked');
281
+ }
282
+
283
+ } catch (error) {
284
+ console.error('\nāŒ Error analyzing data:', error.message);
285
+ console.error(error);
286
+ } finally {
287
+ console.log('\n' + '='.repeat(80));
288
+ console.log('\n');
289
+ await pool.end();
290
+ }
291
+ }
292
+
293
+ // Run analysis
294
+ analyzeCohortData().catch(console.error);
295
+
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # Analyze cohort retention data on production Heroku database
3
+
4
+ echo "šŸ” Analyzing cohort retention data on dubs-server-prod..."
5
+ echo ""
6
+
7
+ # Get the DATABASE_URL from Heroku and run the analysis
8
+ heroku config:get DATABASE_URL --app dubs-server-prod | \
9
+ xargs -I {} env DATABASE_URL={} node scripts/analyze-cohort-data-heroku.js
10
+