Commit 7c5ba0f9 authored by Stan Hu's avatar Stan Hu

Merge branch 'user-cohorts' into 'master'

Add user cohorts table to admin area

Closes gitlab-ce#29551

See merge request !1545
parents 86d1e55e a47d5937
......@@ -386,6 +386,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Admin();
switch (path[1]) {
case 'application_settings':
case 'cohorts':
new gl.ApplicationSettings();
break;
case 'groups':
......
......@@ -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
......
class Admin::CohortsController < Admin::ApplicationController
def index
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
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
MONTHS_INCLUDED = 12
def execute
{
months_included: MONTHS_INCLUDED,
cohorts: cohorts
}
end
# Get an array of hashes that looks like:
#
# [
# {
# registration_month: Date.new(2017, 3),
# activity_months: [3, 2, 1],
# total: 3
# inactive: 0
# },
# etc.
#
# The `months` array is always from oldest to newest, so it's always
# non-strictly decreasing from left to right.
def cohorts
months = Array.new(MONTHS_INCLUDED) { |i| i.months.ago.beginning_of_month.to_date }
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[[registration_month, nil]].to_i
months.pop
{
registration_month: registration_month,
activity_months: activity_months,
total: activity_months.first[:total],
inactive: inactive
}
end
end
private
# 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.
#
# 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:
#
# {
# [created_at_month, last_activity_on_month] => count,
# [created_at_month, last_activity_on_month_2] => count_2,
# # etc.
# }
#
# created_at_month can never be nil, but last_activity_on_month can (when a
# user has never logged in, just been created). This covers the last
# MONTHS_INCLUDED months.
def counts_by_month
@counts_by_month ||=
begin
created_at_month = column_to_date('created_at')
last_activity_on_month = column_to_date('last_activity_on')
User
.where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month)
.group(created_at_month, last_activity_on_month)
.reorder("#{created_at_month} ASC", "#{last_activity_on_month} ASC")
.count
end
end
def column_to_date(column)
if Gitlab::Database.postgresql?
"CAST(DATE_TRUNC('month', #{column}) AS date)"
else
"STR_TO_DATE(DATE_FORMAT(#{column}, '%Y-%m-01'), '%Y-%m-%d')"
end
end
end
......@@ -556,7 +556,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
......
.bs-callout.clearfix
%p
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('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
.table-holder
%table.table
%thead
%tr
%th Registration month
%th Inactive users
%th Cohort total
- @cohorts[:months_included].times do |i|
%th Month #{i}
%tbody
- @cohorts[:cohorts].each do |cohort|
%tr
%td= cohort[:registration_month]
%td= cohort[:inactive]
%td= cohort[:total]
- cohort[:activity_months].each do |activity_month|
%td
- next if cohort[:total] == '0'
= activity_month[:percentage]
%br
= activity_month[:total]
%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) } }
- @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/usage_statistics'), target: '_blank'
is enabled. To enable it and see user cohorts,
visit
= succeed '.' do
= link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
......@@ -27,3 +27,7 @@
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
= nav_link path: 'cohorts#index' do
= link_to admin_cohorts_path, title: 'Cohorts' do
%span
Cohorts
---
title: Show user cohorts data when usage ping is enabled
merge_request:
author:
......@@ -133,6 +133,8 @@ namespace :admin do
end
end
resources :cohorts, only: :index
resources :builds, only: :index do
collection do
post :cancel_all
......
......@@ -88,6 +88,7 @@ All technical content published by GitLab lives in the documentation, including:
- [Repository restrictions](user/admin_area/settings/account_and_limit_settings.md#repository-size-limit) Define size restrictions for your repositories to limit the space they occupy in your storage device. Includes LFS objects.
- [Auditor users](administration/auditor_users.md) Create auditor users, with read-only access to the entire system.
- [Database load balancing](administration/database_load_balancing.md) Distribute database queries amongst multiple database servers.
- [User cohorts](user/admin_area/user_cohorts.md) View user activity over time.
## Contributor documentation
......
......@@ -22,26 +22,25 @@ importance of the update.
If enabled, the version status will also be shown in the help page (`/help`)
for all signed in users.
## Usage data
## Usage ping
> [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics
[were added][ee-735] in GitLab Enterprise Edition 8.12.
[were added][ee-735] in GitLab Enterprise Edition
8.12. [Moved to GitLab Community Edition][ce-23361] in 9.1.
GitLab Inc. can collect non-sensitive information about how Enterprise Edition
customers use their GitLab instance upon the activation of a ping feature
GitLab Inc. can collect non-sensitive information about how GitLab users
use their GitLab instance upon the activation of a ping feature
located in the admin panel (`/admin/application_settings`).
You can see the **exact** JSON payload that your instance sends to GitLab Inc.
You can see the **exact** JSON payload that your instance sends to GitLab
in the "Usage statistics" section of the admin panel.
Nothing qualitative is collected. Only quantitative. Meaning, no project name,
author name, nature of comments, name of labels, etc.
Nothing qualitative is collected. Only quantitative. That means no project
names, author names, comment bodies, names of labels, etc.
This is done mainly for the following reasons:
- to have a better understanding on how our users use our product
- to provide more tools for the customer success team to help customers onboard
better.
The usage ping is sent in order for GitLab Inc. to have a better understanding
of how our users use our product, and to be more data-driven when creating or
changing features.
The total number of the following is sent back to GitLab Inc.:
......@@ -81,6 +80,16 @@ The total number of the following is sent back to GitLab Inc.:
Also, we track if you've installed Mattermost with GitLab.
For example: `"mattermost_enabled":true"`.
More data will be added over time. The goal of this ping is to be as light as
possible, so it won't have any performance impact on your installation when
the calculation is made.
### Deactivate the usage ping
By default, usage ping is opt-out. If you want to deactivate this feature, go to
the Settings page of your administration panel and uncheck the Usage ping
checkbox.
## Privacy policy
GitLab Inc. does **not** collect any sensitive information, like project names
......@@ -91,3 +100,4 @@ Read more in about the [Privacy policy](https://about.gitlab.com/privacy).
[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
# Cohorts
> **Notes:**
- [Introduced][ce-23361] in GitLab 9.1.
As a benefit of having the [usage ping active](settings/usage_statistics.md),
GitLab lets you analyze the users' activities of your GitLab installation.
Under `/admin/cohorts`, when the usage ping is active, GitLab will show the
monthly cohorts of new users and their activities over time.
How do we read the user cohorts table? Let's take an example with the following
user cohorts.
![User cohort example](img/cohorts.png)
For the cohort of June 2016, 163 users have been created on this server. One
month later, in July 2016, 155 users (or 95% of the June cohort) are still
active. Two months later, 139 users (or 85%) are still active. 9 months later,
we can see that only 6% of this cohort are still active.
How do we measure the activity of users? GitLab considers a user active if:
* the user signs in
* the user has Git activity (whether push or pull).
### Setup
1. Activate the usage ping as defined [in the documentation](settings/usage_statistics.md)
2. Go to `/admin/cohorts` to see the user cohorts of the server
[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
require 'spec_helper'
describe CohortsService 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, last_activity_on: Time.now)
create(:user, created_at: months_ago_time, last_activity_on: months_ago_time)
end
create(:user) # this user is inactive and belongs to the current month
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(months_included: 12,
cohorts: expected_cohorts)
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