Commit ac0146a0 authored by Sean McGivern's avatar Sean McGivern Committed by Rémy Coutable

Use serializer for formatting cohorts data

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