Mastodon/app/lib/admin/metrics/retention.rb
Claire 5d7e3bbd17 Merge commit from fork
* Fix GHSA-7jvv-fhmg-wpfw

* Fix GHSA-hx34-2pfw-2qfj

* Fix GHSA-vwhj-3g83-v276

* Bump version to v4.6.4
2026-08-14 07:27:50 -04:00

107 lines
3.0 KiB
Ruby

# frozen_string_literal: true
class Admin::Metrics::Retention
CACHE_TTL = 5.minutes.freeze
class Cohort < ActiveModelSerializers::Model
attributes :period, :frequency, :data
end
class CohortData < ActiveModelSerializers::Model
attributes :date, :rate, :value
end
attr_reader :loaded
alias loaded? loaded
def initialize(start_at, end_at, frequency)
@start_at = start_at.to_date
@end_at = end_at.to_date
@frequency = %w(day month).include?(frequency) ? frequency : 'day'
@loaded = false
@start_at = [@start_at, @end_at - (@frequency == 'day' ? 31.days : 12.months)].max
end
def cache_key
['metrics/retention', @start_at, @end_at, @frequency].join(';')
end
def cohorts
load
end
protected
def load
unless loaded?
@values = Rails.cache.fetch(cache_key, expires_in: CACHE_TTL) { perform_query }
@loaded = true
end
@values
end
def perform_query
report_rows.each_with_object([]) do |row, arr|
current_cohort = arr.last
if current_cohort.nil? || current_cohort.period != row['cohort_period']
current_cohort = Cohort.new(period: row['cohort_period'], frequency: @frequency, data: [])
arr << current_cohort
end
value, rate = row['retention_value_and_rate'].delete('{}').split(',')
current_cohort.data << CohortData.new(
date: row['retention_period'],
rate: rate.to_f,
value: value.to_s
)
end
end
def report_rows
ActiveRecord::Base.connection.select_all(sanitized_sql_string)
end
def sanitized_sql_string
ActiveRecord::Base.sanitize_sql_array(
[sql_query_string, { start_at: @start_at, end_at: @end_at, frequency: @frequency }]
)
end
def sql_query_string
<<~SQL.squish
SELECT axis.*, (
WITH new_users AS (
SELECT users.id
FROM users
WHERE users.account_id >= (date_part('epoch', date_trunc(:frequency, axis.cohort_period)::date) * 1000)::bigint << 16 AND users.account_id < ((date_part('epoch', date_trunc(:frequency, axis.cohort_period)::date + ('1' || :frequency)::interval)) * 1000)::bigint << 16
),
retained_users AS (
SELECT users.id
FROM users
INNER JOIN new_users on new_users.id = users.id
WHERE date_trunc(:frequency, users.current_sign_in_at) >= axis.retention_period
)
SELECT ARRAY[count(*), (count(*))::float / (SELECT GREATEST(count(*), 1) FROM new_users)] AS retention_value_and_rate
FROM retained_users
)
FROM (
WITH cohort_periods AS (
SELECT generate_series(date_trunc(:frequency, :start_at::timestamp)::date, date_trunc(:frequency, :end_at::timestamp)::date, ('1 ' || :frequency)::interval) AS cohort_period
),
retention_periods AS (
SELECT cohort_period AS retention_period FROM cohort_periods
)
SELECT *
FROM cohort_periods, retention_periods
WHERE retention_period >= cohort_period
) as axis
SQL
end
end