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 0000000000000000000000000000000000000000..980ce9aaad99e0e32bdc8301ad2cb3b685ea8a77 --- /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 2c8b5e4933da2baf3e6434300568b08494d5826b..46d75004c3b2cfbde672ea502b5e87856015043c 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 8154124c763c0a2195a317c8fcb1c46f1020f7d8..341a50cf532add95b802a326dedf0b732542e94f 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 f3a632fca2c4392fe5a513a2125b4d78e73aa6a5..bcb605c7688662c86e6674a5494e9d7575c4c5cb 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