Commit 2913c5cd authored by Marc Shaw's avatar Marc Shaw

Add action monthly active users to usage data

Currently we are only going to be tracking the actions of
pushing to a repo, creating a wiki page and creating a new
event on a design management page.

Issue: gitlab.com/gitlab-org/gitlab/-/issues/219956
Merge Request: gitlab.com/gitlab-org/gitlab/-/merge_requests/35580
parent 55010526
......@@ -135,6 +135,8 @@ class EventCreateService
event.update_columns(updated_at: time_stamp, created_at: time_stamp)
end
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
event
end
......@@ -174,6 +176,8 @@ class EventCreateService
action = Event.actions[status]
raise IllegalActionError, "#{status} is not a valid status" if action.nil?
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: status, event_target: record.class, author_id: current_user.id)
parent_attrs(record.resource_parent)
.merge(base_attrs)
.merge(action: action, target_id: record.id, target_type: record.class.name)
......@@ -194,6 +198,8 @@ class EventCreateService
new_event
end
Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: :pushed, event_target: Project, author_id: current_user.id)
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
......
......@@ -525,6 +525,9 @@ appear to be associated to any of the services running, since they all appear to
| `labels` | `counts` | | | | |
| `merge_requests` | `counts` | | | | |
| `merge_requests_users` | `counts` | | | | |
| `action_monthly_active_users_project_repo` | `counts` | | | | |
| `action_monthly_active_users_design_management` | `counts` | | | | |
| `action_monthly_active_users_wiki_repo` | `counts` | | | | |
| `notes` | `counts` | | | | |
| `wiki_pages_create` | `counts` | | | | |
| `wiki_pages_update` | `counts` | | | | |
......
......@@ -158,7 +158,8 @@ module Gitlab
usage_counters,
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage
container_expiration_policies_usage,
action_monthly_active_users(default_time_period)
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
......@@ -574,6 +575,35 @@ module Gitlab
{ analytics_unique_visits: results }
end
# rubocop: disable CodeReuse/ActiveRecord
def action_monthly_active_users(time_period)
counter = Gitlab::UsageDataCounters::TrackUniqueActions.new
project_count = counter.count_unique_events(
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION,
date_from: time_period[:created_at].first,
date_to: time_period[:created_at].last
)
design_count = counter.count_unique_events(
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::DESIGN_ACTION,
date_from: time_period[:created_at].first,
date_to: time_period[:created_at].last
)
wiki_count = counter.count_unique_events(
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::WIKI_ACTION,
date_from: time_period[:created_at].first,
date_to: time_period[:created_at].last)
{
action_monthly_active_users_project_repo: project_count,
action_monthly_active_users_design_management: design_count,
action_monthly_active_users_wiki_repo: wiki_count
}
end
# rubocop: enable CodeReuse/ActiveRecord
private
def unique_visit_service
......
# frozen_string_literal: true
module Gitlab
module UsageDataCounters
class TrackUniqueActions
KEY_EXPIRY_LENGTH = 29.days
WIKI_ACTION = :wiki_action
DESIGN_ACTION = :design_action
PUSH_ACTION = :project_action
ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({
wiki: {
created: WIKI_ACTION,
updated: WIKI_ACTION,
destroyed: WIKI_ACTION
},
design: {
created: DESIGN_ACTION,
updated: DESIGN_ACTION,
destroyed: DESIGN_ACTION
},
project: {
pushed: PUSH_ACTION
}
})
def self.track_action(params)
self.new.track_action(params)
end
def self.count_unique_events(params)
self.new.count_unique_events(params)
end
def track_action(event_action:, event_target:, author_id:, time: Time.zone.now)
return unless Gitlab::CurrentSettings.usage_ping_enabled
return unless Feature.enabled?(:track_push_events_with_redis)
return unless valid_target?(event_target)
return unless valid_action?(event_action)
transformed_target = transform_target(event_target)
transformed_action = transform_action(event_action, transformed_target)
add_event(transformed_action, author_id, time)
end
def count_unique_events(event_action:, date_from:, date_to:)
keys = (date_from.to_date..date_to.to_date).map { |date| key(event_action, date) }
Gitlab::Redis::SharedState.with do |redis|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.pfcount(*keys)
end
end
end
private
def transform_action(event_action, event_target)
ACTION_TRANSFORMATIONS.dig(event_target, event_action) || event_action
end
def transform_target(event_target)
Event::TARGET_TYPES.key(event_target)
end
def valid_target?(target)
Event::TARGET_TYPES.value?(target)
end
def valid_action?(action)
Event.actions.key?(action)
end
def key(event_action, date)
year_day = date.strftime('%G-%j')
"#{event_action}-#{year_day}"
end
def add_event(event_action, author_id, date)
target_key = key(event_action, date)
Gitlab::Redis::SharedState.with do |redis|
redis.multi do |multi|
multi.pfadd(target_key, author_id)
multi.expire(target_key, KEY_EXPIRY_LENGTH)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redis_shared_state do
subject(:track_unique_events) { described_class.new }
def track_action(params)
track_unique_events.track_action(params)
end
def count_unique_events(params)
track_unique_events.count_unique_events(params)
end
describe '#tracking_event' do
context 'when usage pings are enabled' do
before do
Gitlab::CurrentSettings.update!(usage_ping_enabled: true)
end
context 'when the feature flag is enabled ' do
let(:time) { Time.zone.now }
before do
stub_feature_flags(cache_diff_stats_merge_request: true)
end
context 'when target is valid' do
context 'when action is valid' do
it 'tracks and counts the events as expected' do
project = Event::TARGET_TYPES[:project]
design = Event::TARGET_TYPES[:design]
wiki = Event::TARGET_TYPES[:wiki]
track_action(event_action: :pushed, event_target: project, author_id: 1)
track_action(event_action: :pushed, event_target: project, author_id: 1)
track_action(event_action: :pushed, event_target: project, author_id: 2)
track_action(event_action: :pushed, event_target: project, author_id: 3)
track_action(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days)
track_action(event_action: :created, event_target: project, author_id: 5, time: time - 3.days)
track_action(event_action: :destroyed, event_target: design, author_id: 3)
track_action(event_action: :created, event_target: design, author_id: 4)
track_action(event_action: :updated, event_target: design, author_id: 5)
track_action(event_action: :pushed, event_target: design, author_id: 6)
track_action(event_action: :destroyed, event_target: wiki, author_id: 5)
track_action(event_action: :created, event_target: wiki, author_id: 3)
track_action(event_action: :updated, event_target: wiki, author_id: 4)
track_action(event_action: :pushed, event_target: wiki, author_id: 6)
expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time, date_to: Date.tomorrow)).to eq(3)
expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time - 5.days, date_to: Date.tomorrow)).to eq(4)
expect(count_unique_events(event_action: described_class::DESIGN_ACTION, date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3)
expect(count_unique_events(event_action: described_class::WIKI_ACTION, date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3)
expect(count_unique_events(event_action: described_class::PUSH_ACTION, date_from: time - 5.days, date_to: time - 2.days)).to eq(1)
end
end
context 'when action is invalid' do
it 'does not add any event' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
track_action(event_action: :test, event_target: :wiki, author_id: 2)
end
end
end
context 'when target is invalid' do
it 'does not add any event' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
track_action(event_action: :pushed, event_target: :test, author_id: 2)
end
end
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(cache_diff_stats_merge_request: true)
end
it 'does not add any event' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
track_action(event_action: :pushed, event_target: :project, author_id: 2)
end
end
end
context 'when usage pings are disabled' do
before do
Gitlab::CurrentSettings.update!(usage_ping_enabled: false)
end
it 'does not add any event' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
track_action(event_action: :pushed, event_target: :project, author_id: 2)
end
end
end
end
......@@ -893,6 +893,37 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
describe '#action_monthly_active_users', :clean_gitlab_redis_shared_state do
let(:time_period) { { created_at: 2.days.ago..time } }
let(:time) { Time.zone.now }
before do
counter = Gitlab::UsageDataCounters::TrackUniqueActions.new
project = Event::TARGET_TYPES[:project]
wiki = Event::TARGET_TYPES[:wiki]
design = Event::TARGET_TYPES[:design]
counter.track_action(event_action: :pushed, event_target: project, author_id: 1)
counter.track_action(event_action: :pushed, event_target: project, author_id: 1)
counter.track_action(event_action: :pushed, event_target: project, author_id: 2)
counter.track_action(event_action: :pushed, event_target: project, author_id: 3)
counter.track_action(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days)
counter.track_action(event_action: :created, event_target: project, author_id: 5, time: time - 3.days)
counter.track_action(event_action: :created, event_target: wiki, author_id: 3)
counter.track_action(event_action: :created, event_target: design, author_id: 3)
end
it 'returns the distinct count of user actions within the specified time period' do
expect(described_class.action_monthly_active_users(time_period)).to eq(
{
action_monthly_active_users_design_management: 1,
action_monthly_active_users_project_repo: 3,
action_monthly_active_users_wiki_repo: 1
}
)
end
end
describe '.analytics_unique_visits_data' do
subject { described_class.analytics_unique_visits_data }
......
......@@ -173,7 +173,7 @@ RSpec.describe EventCreateService do
end
end
describe '#wiki_event' do
describe '#wiki_event', :clean_gitlab_redis_shared_state do
let_it_be(:user) { create(:user) }
let_it_be(:wiki_page) { create(:wiki_page) }
let_it_be(:meta) { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
......@@ -193,6 +193,15 @@ RSpec.describe EventCreateService do
)
end
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { event }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
end
it 'is idempotent', :aggregate_failures do
expect { event }.to change(Event, :count).by(1)
duplicate = nil
......@@ -241,6 +250,15 @@ RSpec.describe EventCreateService do
subject { service.push(project, user, push_data) }
it_behaves_like 'service for creating a push event', PushEventPayloadService
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { subject }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
end
end
describe '#bulk_push', :clean_gitlab_redis_shared_state do
......@@ -255,6 +273,15 @@ RSpec.describe EventCreateService do
subject { service.bulk_push(project, user, push_data) }
it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { subject }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
end
end
describe 'Project' do
......@@ -273,7 +300,7 @@ RSpec.describe EventCreateService do
end
end
describe 'design events' do
describe 'design events', :clean_gitlab_redis_shared_state do
let_it_be(:design) { create(:design, project: project) }
let_it_be(:author) { user }
......@@ -314,6 +341,15 @@ RSpec.describe EventCreateService do
end
it_behaves_like 'feature flag gated multiple event creation'
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { result }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
end
end
describe '#destroy_designs' do
......@@ -334,6 +370,15 @@ RSpec.describe EventCreateService do
end
it_behaves_like 'feature flag gated multiple event creation'
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { result }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
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