From 347b555e724d4a3033ce4e5ee2925d100b0ccc03 Mon Sep 17 00:00:00 2001 From: Pavel Shutsin Date: Wed, 22 Oct 2025 11:46:17 +0200 Subject: [PATCH] Add feature groups for AI usage events These feature groups will be used to calculate different metrics per Duo feature. E.g. "user retention per feature". No user facing changes so far. Changelog: added EE: true --- .../ai_usage/ai_usage_event_feature_enum.rb | 16 ++++++++ ee/lib/gitlab/tracking/ai_tracking.rb | 12 +++++- .../tracking/ai_usage_events_registry_dsl.rb | 40 +++++++++++++++++-- .../ai_usage_events_registry_dsl_spec.rb | 40 +++++++++++++++++-- 4 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 ee/app/graphql/types/analytics/ai_usage/ai_usage_event_feature_enum.rb diff --git a/ee/app/graphql/types/analytics/ai_usage/ai_usage_event_feature_enum.rb b/ee/app/graphql/types/analytics/ai_usage/ai_usage_event_feature_enum.rb new file mode 100644 index 00000000000000..980ce9aaad99e0 --- /dev/null +++ b/ee/app/graphql/types/analytics/ai_usage/ai_usage_event_feature_enum.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Analytics + module AiUsage + class AiUsageEventFeatureEnum < BaseEnum + graphql_name 'AiUsageEventFeature' + description 'Associated Duo feature of AI usage event' + + Gitlab::Tracking::AiTracking.registered_features.each do |feature| + value feature.upcase, value: feature, description: "Duo #{feature}" + end + end + end + end +end diff --git a/ee/lib/gitlab/tracking/ai_tracking.rb b/ee/lib/gitlab/tracking/ai_tracking.rb index 2c8b5e4933da2b..46d75004c3b2cf 100644 --- a/ee/lib/gitlab/tracking/ai_tracking.rb +++ b/ee/lib/gitlab/tracking/ai_tracking.rb @@ -7,7 +7,7 @@ module Tracking module AiTracking extend AiUsageEventsRegistryDsl - register do + register_feature(:code_suggestions) do deprecated_events(code_suggestions_requested: 1) # old data events( @@ -22,9 +22,13 @@ module AiTracking end deprecated_events(code_suggestion_direct_access_token_refresh: 5) # old data + end + register_feature(:chat) do events(request_duo_chat_response: 6) + end + register_feature(:troubleshoot_job) do events(troubleshoot_job: 7) do |context| { job_id: context['job'].id, @@ -33,7 +37,9 @@ module AiTracking merge_request_id: context['job'].pipeline&.merge_request_id } end + end + register_feature(:agentic_chat) do events( agent_platform_session_created: 8, agent_platform_session_started: 9, @@ -47,7 +53,9 @@ module AiTracking environment: context['property'] } end + end + register_feature(:code_review) do events( encounter_duo_code_review_error_during_review: 10, find_no_issues_duo_code_review_after_review: 11, @@ -59,8 +67,8 @@ module AiTracking request_review_duo_code_review_on_mr_by_non_author: 17, excluded_files_from_duo_code_review: 18 ) - # Current highest event ID: 21, next available: 22 end + # Current highest event ID: 21, next available: 22 class << self def track_event(event_name, **context_hash) diff --git a/ee/lib/gitlab/tracking/ai_usage_events_registry_dsl.rb b/ee/lib/gitlab/tracking/ai_usage_events_registry_dsl.rb index 8154124c763c0a..341a50cf532add 100644 --- a/ee/lib/gitlab/tracking/ai_usage_events_registry_dsl.rb +++ b/ee/lib/gitlab/tracking/ai_usage_events_registry_dsl.rb @@ -4,24 +4,40 @@ module Gitlab module Tracking # rubocop:disable Gitlab/ModuleWithInstanceVariables -- it's a class level DSL. It's intended to be a module. module AiUsageEventsRegistryDsl - def register(&block) + def register_feature(name, &block) + guard_absent_feature! @registered_events ||= {}.with_indifferent_access + @current_feature = name instance_eval(&block) + @current_feature = nil end def events(names_with_ids, &event_transformation) + guard_present_feature! + names_with_ids.each do |name, id| guard_internal_event_existence!(name) guard_duplicated_event!(name, id) - @registered_events[name] = { id: id, transformations: [] } + @registered_events[name] = { + id: id, + transformations: [], + feature: @current_feature + } transformation(name, &event_transformation) end end def deprecated_events(names_with_ids) + guard_present_feature! + names_with_ids.each do |name, id| guard_duplicated_event!(name, id) - @registered_events[name] = { id: id, transformations: [], deprecated: true } + @registered_events[name] = { + id: id, + transformations: [], + deprecated: true, + feature: @current_feature + } end end @@ -51,6 +67,12 @@ def deprecated_event?(event_name) @registered_events[event_name][:deprecated] end + def registered_features + return [] unless @registered_events + + @registered_events.values.pluck(:feature).uniq.compact # rubocop:disable CodeReuse/ActiveRecord -- it's a hash. + end + private def guard_internal_event_existence!(event_name) @@ -63,6 +85,18 @@ def guard_duplicated_event!(name, id) raise "Event with name `#{name}` was already registered" if @registered_events[name] raise "Event with id `#{id}` was already registered" if @registered_events.detect { |_n, e| e[:id] == id } end + + def guard_present_feature! + return if @current_feature + + raise "Cannot register events outside of a feature context. Use register_feature method." + end + + def guard_absent_feature! + return unless @current_feature + + raise "Nested features are not supported. Use register_feature method on top level." + end end # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/ee/spec/lib/gitlab/tracking/ai_usage_events_registry_dsl_spec.rb b/ee/spec/lib/gitlab/tracking/ai_usage_events_registry_dsl_spec.rb index f3a632fca2c439..bcb605c7688662 100644 --- a/ee/spec/lib/gitlab/tracking/ai_usage_events_registry_dsl_spec.rb +++ b/ee/spec/lib/gitlab/tracking/ai_usage_events_registry_dsl_spec.rb @@ -17,23 +17,41 @@ it 'returns empty transformations array' do expect(registry_module.registered_transformations(:some_event)).to eq([]) end + + it 'returns empty features array' do + expect(registry_module.registered_features).to eq([]) + end end context 'with events registered' do it 'fails when event does not have internal events definition' do expect do - registry_module.register do + registry_module.register_feature(:test_feature) do events(unknown_event: 1) end end.to raise_error("Event `unknown_event` is not defined in InternalEvents") end + it 'fails when events are registered outside of a feature context' do + expect do + registry_module.events(ungrouped_event: 5) + end.to raise_error("Cannot register events outside of a feature context. Use register_feature method.") + end + + it 'fails when feature is registered inside of another feature' do + expect do + registry_module.register_feature(:outer) do + register_feature(:inner) + end + end.to raise_error("Nested features are not supported. Use register_feature method on top level.") + end + context 'with InternalEvents definition in place' do before do allow(Gitlab::Tracking::EventDefinition).to receive(:internal_event_exists?) .and_return(true) - registry_module.register do + registry_module.register_feature(:test_feature) do events(simple_event: 1, multi_event: 2) do |context| context end @@ -51,7 +69,7 @@ describe '.events' do it 'fails when same event ID already exists' do expect do - registry_module.register do + registry_module.register_feature(:another_feature) do events(same_id_event: 1) end end.to raise_error("Event with id `1` was already registered") @@ -59,7 +77,7 @@ it 'fails when same event name already exists' do expect do - registry_module.register do + registry_module.register_feature(:another_feature) do events(simple_event: 123) end end.to raise_error("Event with name `simple_event` was already registered") @@ -93,6 +111,20 @@ expect(registry_module.deprecated_event?(:old_event)).to be_truthy end end + + describe '.registered_features' do + it 'returns list of all registered feature names' do + expect(registry_module.registered_features).to contain_exactly(:test_feature) + end + + it 'returns empty array when no features are registered' do + new_registry = Class.new.tap do |module_class| + module_class.extend(Gitlab::Tracking::AiUsageEventsRegistryDsl) + end + + expect(new_registry.registered_features).to eq([]) + end + end end end end -- GitLab