Use serializer for formatting cohorts data
This commit is contained in:
parent
61eaf4fe17
commit
ac0146a08e
|
@ -1,9 +1,11 @@
|
|||
class Admin::CohortsController < Admin::ApplicationController
|
||||
def index
|
||||
if ApplicationSetting.current.usage_ping_enabled
|
||||
@cohorts = Rails.cache.fetch('cohorts', expires_in: 1.day) do
|
||||
if current_application_settings.usage_ping_enabled
|
||||
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
|
||||
CohortsService.new.execute
|
||||
end
|
||||
|
||||
@cohorts = CohortsSerializer.new.represent(cohorts_results)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class CohortActivityMonthEntity < Grape::Entity
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
expose :total do |cohort_activity_month|
|
||||
number_with_delimiter(cohort_activity_month[:total])
|
||||
end
|
||||
|
||||
expose :percentage do |cohort_activity_month|
|
||||
number_to_percentage(cohort_activity_month[:percentage], precision: 0)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
class CohortEntity < Grape::Entity
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
expose :registration_month do |cohort|
|
||||
cohort[:registration_month].strftime('%b %Y')
|
||||
end
|
||||
|
||||
expose :total do |cohort|
|
||||
number_with_delimiter(cohort[:total])
|
||||
end
|
||||
|
||||
expose :inactive do |cohort|
|
||||
number_with_delimiter(cohort[:inactive])
|
||||
end
|
||||
|
||||
expose :activity_months, using: CohortActivityMonthEntity
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
class CohortsEntity < Grape::Entity
|
||||
expose :months_included
|
||||
expose :cohorts, using: CohortEntity
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
class CohortsSerializer < AnalyticsGenericSerializer
|
||||
entity CohortsEntity
|
||||
end
|
|
@ -1,11 +1,19 @@
|
|||
class CohortsService
|
||||
MONTHS_INCLUDED = 12
|
||||
|
||||
# Get a hash that looks like:
|
||||
def execute
|
||||
{
|
||||
months_included: MONTHS_INCLUDED,
|
||||
cohorts: cohorts
|
||||
}
|
||||
end
|
||||
|
||||
# Get an array of hashes that looks like:
|
||||
#
|
||||
# {
|
||||
# month => {
|
||||
# months: [3, 2, 1],
|
||||
# [
|
||||
# {
|
||||
# registration_month: Date.new(2017, 3),
|
||||
# activity_months: [3, 2, 1],
|
||||
# total: 3
|
||||
# inactive: 0
|
||||
# },
|
||||
|
@ -13,29 +21,26 @@ class CohortsService
|
|||
#
|
||||
# The `months` array is always from oldest to newest, so it's always
|
||||
# non-strictly decreasing from left to right.
|
||||
#
|
||||
def execute
|
||||
cohorts = {}
|
||||
def cohorts
|
||||
months = Array.new(MONTHS_INCLUDED) { |i| i.months.ago.beginning_of_month.to_date }
|
||||
|
||||
MONTHS_INCLUDED.times do
|
||||
created_at_month = months.last
|
||||
activity_months = running_totals(months, created_at_month)
|
||||
Array.new(MONTHS_INCLUDED) do
|
||||
registration_month = months.last
|
||||
activity_months = running_totals(months, registration_month)
|
||||
|
||||
# Even if no users registered in this month, we always want to have a
|
||||
# value to fill in the table.
|
||||
inactive = counts_by_month[[created_at_month, nil]].to_i
|
||||
|
||||
cohorts[created_at_month] = {
|
||||
months: activity_months,
|
||||
total: activity_months.first,
|
||||
inactive: inactive
|
||||
}
|
||||
inactive = counts_by_month[[registration_month, nil]].to_i
|
||||
|
||||
months.pop
|
||||
end
|
||||
|
||||
cohorts
|
||||
{
|
||||
registration_month: registration_month,
|
||||
activity_months: activity_months,
|
||||
total: activity_months.first[:total],
|
||||
inactive: inactive
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -44,11 +49,20 @@ class CohortsService
|
|||
# count as active in this month, too. Start with the most recent month first,
|
||||
# for calculating the running totals, and then reverse for displaying in the
|
||||
# table.
|
||||
def running_totals(all_months, created_at_month)
|
||||
all_months
|
||||
.map { |activity_month| counts_by_month[[created_at_month, activity_month]] }
|
||||
.reduce([]) { |result, total| result << result.last.to_i + total.to_i }
|
||||
.reverse
|
||||
#
|
||||
# Each month has a total, and a percentage of the overall total, as keys.
|
||||
def running_totals(all_months, registration_month)
|
||||
month_totals =
|
||||
all_months
|
||||
.map { |activity_month| counts_by_month[[registration_month, activity_month]] }
|
||||
.reduce([]) { |result, total| result << result.last.to_i + total.to_i }
|
||||
.reverse
|
||||
|
||||
overall_total = month_totals.first
|
||||
|
||||
month_totals.map do |total|
|
||||
{ total: total, percentage: total.zero? ? 0 : 100 * total / overall_total }
|
||||
end
|
||||
end
|
||||
|
||||
# Get a hash that looks like:
|
||||
|
@ -60,9 +74,8 @@ class CohortsService
|
|||
# }
|
||||
#
|
||||
# created_at_month can never be nil, but current_sign_in_at_month can (when a
|
||||
# user has never logged in, just been created). This covers the last twelve
|
||||
# months.
|
||||
#
|
||||
# user has never logged in, just been created). This covers the last
|
||||
# MONTHS_INCLUDED months.
|
||||
def counts_by_month
|
||||
@counts_by_month ||=
|
||||
begin
|
||||
|
@ -80,7 +93,7 @@ class CohortsService
|
|||
def column_to_date(column)
|
||||
if Gitlab::Database.postgresql?
|
||||
"CAST(DATE_TRUNC('month', #{column}) AS date)"
|
||||
elsif Gitlab::Database.mysql?
|
||||
else
|
||||
"STR_TO_DATE(DATE_FORMAT(#{column}, '%Y-%m-01'), '%Y-%m-%d')"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.bs-callout.clearfix
|
||||
%p
|
||||
User cohorts are shown for the last twelve months. Only users with
|
||||
activity are counted in the cohort total; inactive users are counted
|
||||
separately.
|
||||
User cohorts are shown for the last #{@cohorts[:months_included]}
|
||||
months. Only users with activity are counted in the cohort total; inactive
|
||||
users are counted separately.
|
||||
= link_to icon('question-circle'), help_page_path('administration/usage_ping_and_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
|
||||
|
||||
.table-holder
|
||||
|
@ -12,27 +12,17 @@
|
|||
%th Registration month
|
||||
%th Inactive users
|
||||
%th Cohort total
|
||||
%th Month 0
|
||||
%th Month 1
|
||||
%th Month 2
|
||||
%th Month 3
|
||||
%th Month 4
|
||||
%th Month 5
|
||||
%th Month 6
|
||||
%th Month 7
|
||||
%th Month 8
|
||||
%th Month 9
|
||||
%th Month 10
|
||||
%th Month 11
|
||||
- @cohorts[:months_included].times do |i|
|
||||
%th Month #{i}
|
||||
%tbody
|
||||
- @cohorts.each do |registration_month, cohort|
|
||||
- @cohorts[:cohorts].each do |cohort|
|
||||
%tr
|
||||
%td= registration_month.strftime('%b %Y')
|
||||
%td= number_with_delimiter(cohort[:inactive])
|
||||
%td= number_with_delimiter(cohort[:total])
|
||||
- cohort[:months].each do |running_total|
|
||||
%td= cohort[:registration_month]
|
||||
%td= cohort[:inactive]
|
||||
%td= cohort[:total]
|
||||
- cohort[:activity_months].each do |activity_month|
|
||||
%td
|
||||
- next if cohort[:total].zero?
|
||||
= number_to_percentage(100 * running_total / cohort[:total], precision: 0)
|
||||
- next if cohort[:total] == '0'
|
||||
= activity_month[:percentage]
|
||||
%br
|
||||
(#{number_with_delimiter(running_total)})
|
||||
= activity_month[:total]
|
||||
|
|
|
@ -17,22 +17,83 @@ describe CohortsService do
|
|||
|
||||
create(:user) # this user is inactive and belongs to the current month
|
||||
|
||||
expected = {
|
||||
month_start(11) => { months: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], total: 0, inactive: 0 },
|
||||
month_start(10) => { months: [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], total: 2, inactive: 0 },
|
||||
month_start(9) => { months: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], total: 0, inactive: 0 },
|
||||
month_start(8) => { months: [2, 1, 1, 1, 1, 1, 1, 1, 1], total: 2, inactive: 0 },
|
||||
month_start(7) => { months: [0, 0, 0, 0, 0, 0, 0, 0], total: 0, inactive: 0 },
|
||||
month_start(6) => { months: [2, 1, 1, 1, 1, 1, 1], total: 2, inactive: 0 },
|
||||
month_start(5) => { months: [0, 0, 0, 0, 0, 0], total: 0, inactive: 0 },
|
||||
month_start(4) => { months: [2, 1, 1, 1, 1], total: 2, inactive: 0 },
|
||||
month_start(3) => { months: [0, 0, 0, 0], total: 0, inactive: 0 },
|
||||
month_start(2) => { months: [2, 1, 1], total: 2, inactive: 0 },
|
||||
month_start(1) => { months: [0, 0], total: 0, inactive: 0 },
|
||||
month_start(0) => { months: [2], total: 2, inactive: 1 }
|
||||
}
|
||||
expected_cohorts = [
|
||||
{
|
||||
registration_month: month_start(11),
|
||||
activity_months: Array.new(12) { { total: 0, percentage: 0 } },
|
||||
total: 0,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(10),
|
||||
activity_months: [{ total: 2, percentage: 100 }] + Array.new(10) { { total: 1, percentage: 50 } },
|
||||
total: 2,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(9),
|
||||
activity_months: Array.new(10) { { total: 0, percentage: 0 } },
|
||||
total: 0,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(8),
|
||||
activity_months: [{ total: 2, percentage: 100 }] + Array.new(8) { { total: 1, percentage: 50 } },
|
||||
total: 2,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(7),
|
||||
activity_months: Array.new(8) { { total: 0, percentage: 0 } },
|
||||
total: 0,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(6),
|
||||
activity_months: [{ total: 2, percentage: 100 }] + Array.new(6) { { total: 1, percentage: 50 } },
|
||||
total: 2,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(5),
|
||||
activity_months: Array.new(6) { { total: 0, percentage: 0 } },
|
||||
total: 0,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(4),
|
||||
activity_months: [{ total: 2, percentage: 100 }] + Array.new(4) { { total: 1, percentage: 50 } },
|
||||
total: 2,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(3),
|
||||
activity_months: Array.new(4) { { total: 0, percentage: 0 } },
|
||||
total: 0,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(2),
|
||||
activity_months: [{ total: 2, percentage: 100 }] + Array.new(2) { { total: 1, percentage: 50 } },
|
||||
total: 2,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(1),
|
||||
activity_months: Array.new(2) { { total: 0, percentage: 0 } },
|
||||
total: 0,
|
||||
inactive: 0
|
||||
},
|
||||
{
|
||||
registration_month: month_start(0),
|
||||
activity_months: [{ total: 2, percentage: 100 }],
|
||||
total: 2,
|
||||
inactive: 1
|
||||
},
|
||||
]
|
||||
|
||||
expect(described_class.new.execute).to eq(expected)
|
||||
expect(described_class.new.execute).to eq(months_included: 12,
|
||||
cohorts: expected_cohorts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue