From cb630be4f3eff672a0e4936217fbedaa90d94d82 Mon Sep 17 00:00:00 2001 From: Paul Arterburn Date: Wed, 4 Sep 2024 07:02:36 -0600 Subject: [PATCH] Optimize queries --- app/controllers/admin/stats_controller.rb | 40 ++++ app/models/admin_stats.rb | 19 +- app/views/admin/stats.html.haml | 198 ++---------------- .../admin/stats/_bounces_table.html.haml | 8 + .../admin/stats/_free_users_table.html.haml | 12 ++ .../admin/stats/_upgrades_table.html.haml | 14 ++ app/views/admin/stats/index.html.haml | 145 +++++++++++++ config/routes.rb | 2 +- 8 files changed, 243 insertions(+), 195 deletions(-) create mode 100644 app/controllers/admin/stats_controller.rb create mode 100644 app/views/admin/stats/_bounces_table.html.haml create mode 100644 app/views/admin/stats/_free_users_table.html.haml create mode 100644 app/views/admin/stats/_upgrades_table.html.haml create mode 100644 app/views/admin/stats/index.html.haml diff --git a/app/controllers/admin/stats_controller.rb b/app/controllers/admin/stats_controller.rb new file mode 100644 index 00000000..8b603cf9 --- /dev/null +++ b/app/controllers/admin/stats_controller.rb @@ -0,0 +1,40 @@ +class Admin::StatsController < ApplicationController + def index + @dashboard = AdminStats.new + @users = User.all + @entries = Entry.all + + # Preload data for charts + @users_by_week = @dashboard.users_by_week_since(90.days.ago) + @pro_users_by_week = @dashboard.pro_users_by_week_since(90.days.ago) + @entries_by_week = @dashboard.entries_by_week_since(90.days.ago) + @emails_sent_by_month = @dashboard.emails_sent_by_month_since(90.days.ago) + @payments_by_month = @dashboard.payments_by_month(1.year.ago) + + # Use counter cache for frequently accessed counts + @all_count = Entry.count + @photos_count = Entry.only_images.count + @ai_entries_count = Entry.with_ai_responses.count + + # User statistics + @total_users = @users.count + @pro_users = @users.pro_only + @free_users = @users.free_only + @monthly_users = @users.monthly + @yearly_users = @users.yearly + @forever_users = @users.forever + @payhere_users = @users.payhere_only + @gumroad_users = @users.gumroad_only + @paypal_users = @users.paypal_only + @referral_users = @users.referrals + + # Email statistics + @emails_sent_total = @users.sum(:emails_sent) + @emails_received_total = @users.sum(:emails_received) + + # Paginate large datasets + @upgrades = @dashboard.upgraded_users_since(90.days.ago).page(params[:upgrades_page]).per(20) + @bounces = @dashboard.bounced_users_since(90.days.ago).page(params[:bounces_page]).per(20) + @free_users_recent = @dashboard.free_users_created_since(90.days.ago).order(:created_at).page(params[:free_users_page]).per(20) + end +end diff --git a/app/models/admin_stats.rb b/app/models/admin_stats.rb index e9030fe0..981588da 100644 --- a/app/models/admin_stats.rb +++ b/app/models/admin_stats.rb @@ -75,22 +75,21 @@ def users_created_since(date) end def free_users_created_since(date) - User.free_only.where("created_at >= ?", date) + User.free_only.where('created_at >= ?', date) end def upgraded_users_since(date) - pro_users = [] - User.pro_only.includes(:payments).order("payments.created_at ASC").each do | user| - first_payment = user.payments.order('payments.date').try(:first).try(:date) - if first_payment.present? && first_payment > date - pro_users << user - end - end - pro_users + User.pro_only + .joins(:payments) + .where('payments.date > ?', date) + .select('users.*, MIN(payments.date) as first_payment_date') + .group('users.id') + .order('first_payment_date ASC') end def bounced_users_since(date) - User.where("emails_bounced > 0").where("updated_at >= ?", date) + User.where('emails_bounced > 0') + .where('updated_at >= ?', date) end def entries_per_day_for(user) diff --git a/app/views/admin/stats.html.haml b/app/views/admin/stats.html.haml index fcdf592b..72fb8a4f 100644 --- a/app/views/admin/stats.html.haml +++ b/app/views/admin/stats.html.haml @@ -1,184 +1,14 @@ -- title ("Admin Stats") -= javascript_include_tag "//www.gstatic.com/charts/loader.js", "chartkick" -.row.s-admin-container.container{style: 'margin: 0 auto;'} - .col-md-12 - %h3 - Admin Stats - - .col-md-12 - %hr - - .col-md-12 - .row - .col-md-3 - %strong All Entries - - all_count = Entry.all.size - .col-md-2 - %strong #{format_number(all_count)} - .row - .col-md-3 - %strong All Entries with Photos - - photos_count = Entry.only_images.size - .col-md-2 - =link_to admin_photos_path do - %strong #{format_number(photos_count)} - %span{style: "margin-left: 10px;"} #{number_to_percentage(photos_count.to_f/all_count.to_f*100, precision: 0)} - .row - .col-md-3 - %strong All Entries using DabbleMeGPT - - ai_entries = Entry.with_ai_responses - .col-md-2 - %strong #{format_number(ai_entries.size)} - %span{style: "margin-left: 10px;"} #{format_number(ai_entries.pluck(:user_id).uniq.size)} users - - .col-md-12 - %hr - - .col-md-12 - .row - .col-md-3 - %strong All - .col-md-2 - %strong #{format_number(User.all.size)} - .row - .col-md-3 - %strong Pro / Free - .col-md-2 - %strong #{format_number(User.pro_only.size)} - %span{style: "margin-left: 10px;"} #{number_to_percentage(User.pro_only.size.to_f/all_count.to_f*100, precision: 1)} - .col-md-2 - %strong #{format_number(User.free_only.size)} - .row - .col-md-3 - %strong Pro Monthly / Yearly / Forevers - .col-md-2 - %strong #{format_number(User.monthly.size)} - .col-md-2 - %strong #{format_number(User.yearly.size)} - %span{style: "margin-left: 10px;"} #{number_to_percentage(User.yearly.size.to_f/User.pro_only.size.to_f*100, precision: 1)} - .col-md-2 - %strong #{format_number(User.forever.size)} - .row - .col-md-3 - %strong Pro Stripe / Gumroad / Paypal - .col-md-2 - %strong #{format_number(User.payhere_only.size)} - .col-md-2 - %strong #{format_number(User.gumroad_only.size)} - .col-md-2 - %strong #{format_number(User.paypal_only.size)} - - -# .col-md-12 - -# %hr - - -# .col-md-12 - -# .row - -# .col-md-3 - -# %strong Referrals - -# .col-md-2 - -# %strong #{format_number(User.referrals.size)} - -# - User.referrals.pluck(:referrer).uniq.each do |ref| - -# .row - -# .col-md-3 - -# = ref - -# .col-md-2 - -# #{format_number(User.referrals.where(referrer: ref).size)} - - -# .col-md-12 - -# %hr - - -# .col-md-3 - -# %strong Total Emails Sent - -# .col-md-3 - -# - emails_sent_total = User.sum(:emails_sent) - -# %strong= format_number(emails_sent_total) - -# .clearfix - -# .col-md-3 - -# %strong Total Emails Received - -# .col-md-2 - -# - emails_received_total = User.sum(:emails_received) - -# %strong= format_number(emails_received_total) - -# %span{style: "margin-left: 10px;"} #{number_to_percentage(emails_received_total.to_f/emails_sent_total.to_f*100, precision: 0)} - - - -# .col-md-12 - -# %hr - - -# .col-md-12 - -# %h3 Sign ups over the last 90 days - -# = line_chart @dashboard.users_by_week_since(90.days.ago), discrete: true - -# %br - - -# .col-md-12 - -# %h3 Pro Upgrades over the last 90 days - -# = line_chart @dashboard.pro_users_by_week_since(90.days.ago), discrete: true - -# %br - - -# .col-md-12 - -# %h3 Entries over the last 90 days - -# = line_chart @dashboard.entries_by_week_since(90.days.ago), discrete: true - -# %br - - -# .col-md-12 - -# %h3 Emails over the last 90 days - -# = line_chart @dashboard.emails_sent_by_month_since(90.days.ago), discrete: true - -# %br - - -# .col-md-12 - -# %h3 Payments by month over the last year - -# = column_chart @dashboard.payments_by_month(1.year.ago), discrete: true - -# %br - - -# .col-md-12 - -# - upgrades = @dashboard.upgraded_users_since(90.days.ago) - -# %h3 #{pluralize(format_number(upgrades.size), "Upgrade")} from the last 90 days - -# %p - -# %table.table.table-striped.table-hover - -# %tr - -# %th Email - -# %th Upgraded - -# %th Paid - -# %th Entries - -# %th Per day - -# - upgrades.each do |user| - -# %tr{:class => @dashboard.paid_status_for(user)} - -# %td= user.email - -# %td= l(user.payments.first.date.to_date, format: :month_day) - -# %td= user.payments.sum(:amount) - -# %td= user.entries.size - -# %td= @dashboard.entries_per_day_for(user) - - -# .col-md-12 - -# %hr - -# .col-md-12 - -# - bounces = @dashboard.bounced_users_since(90.days.ago) - -# %h3 #{pluralize(format_number(bounces.size), "user")} from the last 90 days has had emails bouncing - -# %p - -# %table.table.table-striped.table-hover - -# %tr - -# %th Email - -# %th Bounces - -# - bounces.each do |user| - -# %tr{:class => @dashboard.paid_status_for(user)} - -# %td= user.email - -# %td= user.emails_bounced - - -# .col-md-12 - -# %hr - - -# .col-md-12 - -# - free_users = @dashboard.free_users_created_since(90.days.ago).order(:created_at) - -# %h3 #{pluralize(format_number(free_users.size), "Free User")} from the last 90 days - -# %p - -# %table.table.table-striped.table-hover - -# %tr - -# %th Email - -# %th Signed up - -# %th Entries - -# %th Per day - -# - free_users.each do |user| - -# %tr{:class => @dashboard.paid_status_for(user)} - -# %td= user.email - -# %td= l(user.created_at.to_date, format: :month_day) - -# %td= user.entries.size - -# %td= @dashboard.entries_per_day_for(user) +.col-md-12 + %hr +.col-md-12 + %h3 #{pluralize(format_number(@bounces.total_count), "user")} from the last 90 days has had emails bouncing + = render partial: 'bounces_table', locals: { bounces: @bounces, dashboard: @dashboard } + = paginate @bounces, param_name: 'bounces_page' + +.col-md-12 + %hr + +.col-md-12 + %h3 #{pluralize(format_number(@free_users_recent.total_count), "Free User")} from the last 90 days + = render partial: 'free_users_table', locals: { free_users: @free_users_recent, dashboard: @dashboard } + = paginate @free_users_recent, param_name: 'free_users_page' diff --git a/app/views/admin/stats/_bounces_table.html.haml b/app/views/admin/stats/_bounces_table.html.haml new file mode 100644 index 00000000..b23a4ace --- /dev/null +++ b/app/views/admin/stats/_bounces_table.html.haml @@ -0,0 +1,8 @@ +%table.table.table-striped.table-hover + %tr + %th Email + %th Bounces + - bounces.each do |user| + %tr{:class => dashboard.paid_status_for(user)} + %td= user.email + %td= user.emails_bounced diff --git a/app/views/admin/stats/_free_users_table.html.haml b/app/views/admin/stats/_free_users_table.html.haml new file mode 100644 index 00000000..2833bbf5 --- /dev/null +++ b/app/views/admin/stats/_free_users_table.html.haml @@ -0,0 +1,12 @@ +%table.table.table-striped.table-hover + %tr + %th Email + %th Signed up + %th Entries + %th Per day + - free_users.each do |user| + %tr{:class => dashboard.paid_status_for(user)} + %td= user.email + %td= l(user.created_at.to_date, format: :month_day) + %td= user.entries.size + %td= dashboard.entries_per_day_for(user) diff --git a/app/views/admin/stats/_upgrades_table.html.haml b/app/views/admin/stats/_upgrades_table.html.haml new file mode 100644 index 00000000..31a3d52e --- /dev/null +++ b/app/views/admin/stats/_upgrades_table.html.haml @@ -0,0 +1,14 @@ +%table.table.table-striped.table-hover + %tr + %th Email + %th Upgraded + %th Paid + %th Entries + %th Per day + - upgrades.each do |user| + %tr{:class => @dashboard.paid_status_for(user)} + %td= user.email + %td= l(user.first_payment_date.to_date, format: :month_day) + %td= user.payments.sum(:amount) + %td= user.entries_count + %td= @dashboard.entries_per_day_for(user) diff --git a/app/views/admin/stats/index.html.haml b/app/views/admin/stats/index.html.haml new file mode 100644 index 00000000..4b581528 --- /dev/null +++ b/app/views/admin/stats/index.html.haml @@ -0,0 +1,145 @@ +- title ("Admin Stats") += javascript_include_tag "//www.gstatic.com/charts/loader.js", "chartkick" +.row.s-admin-container.container{style: 'margin: 0 auto;'} + .col-md-12 + %h3 Admin Stats + + .col-md-12 + %hr + + .col-md-12 + .row + .col-md-3 + %strong All Entries + .col-md-2 + %strong #{format_number(@all_count)} + .row + .col-md-3 + %strong All Entries with Photos + .col-md-2 + =link_to admin_photos_path do + %strong #{format_number(@photos_count)} + %span{style: "margin-left: 10px;"} #{number_to_percentage(@photos_count.to_f/@all_count.to_f*100, precision: 0)} + .row + .col-md-3 + %strong All Entries using DabbleMeGPT + .col-md-2 + %strong #{format_number(@ai_entries_count)} + %span{style: "margin-left: 10px;"} #{format_number(@ai_users_count)} users + + .col-md-12 + %hr + + .col-md-12 + .row + .col-md-3 + %strong All Users + .col-md-2 + %strong #{format_number(@users.size)} + .row + .col-md-3 + %strong Pro / Free + .col-md-2 + %strong #{format_number(@users.pro_only.size)} + %span{style: "margin-left: 10px;"} #{number_to_percentage(@users.pro_only.size.to_f/@users.size.to_f*100, precision: 1)} + .col-md-2 + %strong #{format_number(@users.free_only.size)} + .row + .col-md-3 + %strong Pro Monthly / Yearly / Forevers + .col-md-2 + %strong #{format_number(@users.monthly.size)} + .col-md-2 + %strong #{format_number(@users.yearly.size)} + %span{style: "margin-left: 10px;"} #{number_to_percentage(@users.yearly.size.to_f/@users.pro_only.size.to_f*100, precision: 1)} + .col-md-2 + %strong #{format_number(@users.forever.size)} + .row + .col-md-3 + %strong Pro Stripe / Gumroad / Paypal + .col-md-2 + %strong #{format_number(@users.payhere_only.size)} + .col-md-2 + %strong #{format_number(@users.gumroad_only.size)} + .col-md-2 + %strong #{format_number(@users.paypal_only.size)} + + .col-md-12 + %hr + + .col-md-12 + .row + .col-md-3 + %strong Referrals + .col-md-2 + %strong #{format_number(@users.referrals.size)} + - @users.referrals.pluck(:referrer).uniq.each do |ref| + .row + .col-md-3 + = ref + .col-md-2 + #{format_number(@users.referrals.where(referrer: ref).size)} + + .col-md-12 + %hr + + .col-md-3 + %strong Total Emails Sent + .col-md-3 + - emails_sent_total = @users.sum(:emails_sent) + %strong= format_number(emails_sent_total) + .clearfix + .col-md-3 + %strong Total Emails Received + .col-md-2 + - emails_received_total = @users.sum(:emails_received) + %strong= format_number(emails_received_total) + %span{style: "margin-left: 10px;"} #{number_to_percentage(emails_received_total.to_f/emails_sent_total.to_f*100, precision: 0)} + + .col-md-12 + %hr + + .col-md-12 + %h3 Sign ups over the last 90 days + = line_chart @users_by_week, discrete: true + %br + + .col-md-12 + %h3 Pro Upgrades over the last 90 days + = line_chart @pro_users_by_week, discrete: true + %br + + .col-md-12 + %h3 Entries over the last 90 days + = line_chart @entries_by_week, discrete: true + %br + + .col-md-12 + %h3 Emails over the last 90 days + = line_chart @emails_sent_by_month, discrete: true + %br + + .col-md-12 + %h3 Payments by month over the last year + = column_chart @payments_by_month, discrete: true + %br + + .col-md-12 + %h3 #{pluralize(format_number(@upgrades.total_count), "Upgrade")} from the last 90 days + = render partial: 'upgrades_table', locals: { upgrades: @upgrades, dashboard: @dashboard } + = paginate @upgrades, param_name: 'upgrades_page' + + .col-md-12 + %hr + .col-md-12 + %h3 #{pluralize(format_number(@bounces.total_count), "user")} from the last 90 days has had emails bouncing + = render partial: 'bounces_table', locals: { bounces: @bounces } + = paginate @bounces, param_name: 'bounces_page' + + .col-md-12 + %hr + + .col-md-12 + %h3 #{pluralize(format_number(@free_users_recent.total_count), "Free User")} from the last 90 days + = render partial: 'free_users_table', locals: { free_users: @free_users_recent, dashboard: @dashboard } + = paginate @free_users_recent, param_name: 'free_users_page' diff --git a/config/routes.rb b/config/routes.rb index fc75c53f..88058808 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,7 @@ resources :inspirations, path: '/admin/inspirations' resources :payments, path: '/admin/payments' get 'admin/users' => 'admin#users', as: 'admin_users' - get 'admin/stats' => 'admin#stats', as: 'admin_stats' + get 'admin/stats' => 'admin/stats#index', as: 'admin_stats' get 'admin/photos' => 'admin#photos', as: 'admin_photos' end