summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/dispatcher.js1
-rw-r--r--app/controllers/admin/application_settings_controller.rb7
-rw-r--r--app/controllers/admin/user_cohorts_controller.rb7
-rw-r--r--app/services/user_cohorts_service.rb49
-rw-r--r--app/views/admin/application_settings/_form.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/user_cohorts/_cohorts_table.html.haml37
-rw-r--r--app/views/admin/user_cohorts/_usage_ping.html.haml10
-rw-r--r--app/views/admin/user_cohorts/index.html.haml16
-rw-r--r--changelogs/unreleased-ee/user-cohorts.yml4
-rw-r--r--config/routes/admin.rb2
-rw-r--r--spec/services/user_cohorts_service_spec.rb42
12 files changed, 179 insertions, 2 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 9d8f965dee0..6c94975d851 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -366,6 +366,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Admin();
switch (path[1]) {
case 'application_settings':
+ case 'user_cohorts':
new gl.ApplicationSettings();
break;
case 'groups':
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 73b03b41594..643993d035e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -19,7 +19,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def usage_data
respond_to do |format|
- format.html { render html: Gitlab::Highlight.highlight('payload.json', Gitlab::UsageData.to_json) }
+ format.html do
+ usage_data = Gitlab::UsageData.data
+ usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json
+
+ render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
+ end
format.json { render json: Gitlab::UsageData.to_json }
end
end
diff --git a/app/controllers/admin/user_cohorts_controller.rb b/app/controllers/admin/user_cohorts_controller.rb
new file mode 100644
index 00000000000..5dd6eedfb06
--- /dev/null
+++ b/app/controllers/admin/user_cohorts_controller.rb
@@ -0,0 +1,7 @@
+class Admin::UserCohortsController < Admin::ApplicationController
+ def index
+ if ApplicationSetting.current.usage_ping_enabled
+ @cohorts = UserCohortsService.new.execute(12)
+ end
+ end
+end
diff --git a/app/services/user_cohorts_service.rb b/app/services/user_cohorts_service.rb
new file mode 100644
index 00000000000..7f84b6a0634
--- /dev/null
+++ b/app/services/user_cohorts_service.rb
@@ -0,0 +1,49 @@
+class UserCohortsService
+ def initialize
+ end
+
+ def execute(months_included)
+ if Gitlab::Database.postgresql?
+ created_at_month = "CAST(DATE_TRUNC('month', created_at) AS date)"
+ current_sign_in_at_month = "CAST(DATE_TRUNC('month', current_sign_in_at) AS date)"
+ elsif Gitlab::Database.mysql?
+ created_at_month = "STR_TO_DATE(DATE_FORMAT(created_at, '%Y-%m-01'), '%Y-%m-%d')"
+ current_sign_in_at_month = "STR_TO_DATE(DATE_FORMAT(current_sign_in_at, '%Y-%m-01'), '%Y-%m-%d')"
+ end
+
+ counts_by_month =
+ User
+ .where('created_at > ?', months_included.months.ago.end_of_month)
+ .group(created_at_month, current_sign_in_at_month)
+ .reorder("#{created_at_month} ASC", "#{current_sign_in_at_month} DESC")
+ .count
+
+ cohorts = {}
+ months = Array.new(months_included) { |i| i.months.ago.beginning_of_month.to_date }
+
+ months_included.times do
+ month = months.last
+ inactive = counts_by_month[[month, nil]] || 0
+
+ # Calculate a running sum of active users, so users active in later months
+ # 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.
+ activity_months =
+ months
+ .map { |activity_month| counts_by_month[[month, activity_month]] }
+ .reduce([]) { |result, total| result << result.last.to_i + total.to_i }
+ .reverse
+
+ cohorts[month] = {
+ months: activity_months,
+ total: activity_months.first,
+ inactive: inactive
+ }
+
+ months.pop
+ end
+
+ cohorts
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index f4e4bac62d7..13e9faa9642 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -477,7 +477,7 @@
diagrams in Asciidoc documents using an external PlantUML service.
%fieldset
- %legend Usage statistics
+ %legend#usage-statistics Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 7893c1dee97..0c2e5efc052 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -27,3 +27,7 @@
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
+ = nav_link path: 'user_cohorts#index' do
+ = link_to admin_user_cohorts_path, title: 'User cohorts' do
+ %span
+ User cohorts
diff --git a/app/views/admin/user_cohorts/_cohorts_table.html.haml b/app/views/admin/user_cohorts/_cohorts_table.html.haml
new file mode 100644
index 00000000000..a322ea9e5db
--- /dev/null
+++ b/app/views/admin/user_cohorts/_cohorts_table.html.haml
@@ -0,0 +1,37 @@
+.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.
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %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
+ %tbody
+ - @cohorts.each do |registration_month, 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
+ - next if cohort[:total].zero?
+ = number_to_percentage(100 * running_total / cohort[:total], precision: 0)
+ %br
+ (#{number_with_delimiter(running_total)})
diff --git a/app/views/admin/user_cohorts/_usage_ping.html.haml b/app/views/admin/user_cohorts/_usage_ping.html.haml
new file mode 100644
index 00000000000..a95f81a7f49
--- /dev/null
+++ b/app/views/admin/user_cohorts/_usage_ping.html.haml
@@ -0,0 +1,10 @@
+%h2 Usage ping
+
+.bs-callout.clearfix
+ %p
+ User cohorts are shown because the usage ping is enabled. The data sent with
+ this is shown below. To disable this, visit
+ = succeed '.' do
+ = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
+
+%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html, pretty: true) } }
diff --git a/app/views/admin/user_cohorts/index.html.haml b/app/views/admin/user_cohorts/index.html.haml
new file mode 100644
index 00000000000..dddcbd834f7
--- /dev/null
+++ b/app/views/admin/user_cohorts/index.html.haml
@@ -0,0 +1,16 @@
+- @no_container = true
+= render "admin/dashboard/head"
+
+%div{ class: container_class }
+ - if @cohorts
+ = render 'cohorts_table'
+ = render 'usage_ping'
+ - else
+ .bs-callout.bs-callout-warning.clearfix
+ %p
+ User cohorts are only shown when the
+ = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-data')
+ usage ping is enabled. It is currently disabled. To enable it and see
+ user cohorts, visit
+ = succeed '.' do
+ = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
diff --git a/changelogs/unreleased-ee/user-cohorts.yml b/changelogs/unreleased-ee/user-cohorts.yml
new file mode 100644
index 00000000000..67d64600a4f
--- /dev/null
+++ b/changelogs/unreleased-ee/user-cohorts.yml
@@ -0,0 +1,4 @@
+---
+title: Show user cohorts data when usage ping is enabled
+merge_request:
+author:
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 3c1c2ce2582..5b44d449b2b 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -106,6 +106,8 @@ namespace :admin do
end
end
+ resources :user_cohorts, only: :index
+
resources :builds, only: :index do
collection do
post :cancel_all
diff --git a/spec/services/user_cohorts_service_spec.rb b/spec/services/user_cohorts_service_spec.rb
new file mode 100644
index 00000000000..8d8d0de31cd
--- /dev/null
+++ b/spec/services/user_cohorts_service_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe UserCohortsService do
+ describe '#execute' do
+ def month_start(months_ago)
+ months_ago.months.ago.beginning_of_month.to_date
+ end
+
+ # In the interests of speed and clarity, this example has minimal data.
+ it 'returns a list of user cohorts' do
+ 6.times do |months_ago|
+ months_ago_time = (months_ago * 2).months.ago
+
+ create(:user, created_at: months_ago_time, current_sign_in_at: Time.now)
+ create(:user, created_at: months_ago_time, current_sign_in_at: months_ago_time)
+ end
+
+ 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 }
+ }
+
+ result = described_class.new.execute(12)
+
+ expect(result.length).to eq(12)
+ expect(result.keys).to all(be_a(Date))
+ expect(result).to eq(expected)
+ end
+ end
+end