diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index fd4deadf105dd52dbc1d92590dd8fe7970efc9a9..4dbf68f214aa544f3a32f85ceae374968c2b79e0 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -22,6 +22,7 @@ module User GROUP_WITH_AI_CHAT_ENABLED_CACHE_PERIOD = 1.hour GROUP_WITH_AI_CHAT_ENABLED_CACHE_KEY = 'group_with_ai_chat_enabled' + GROUP_IDS_WITH_AI_CHAT_ENABLED_CACHE_KEY = 'group_ids_with_ai_chat_enabled' DUO_PRO_ADD_ON_CACHE_KEY = 'user-%{user_id}-code-suggestions-add-on-cache' @@ -256,8 +257,9 @@ def use_separate_indices? def clear_group_with_ai_available_cache(ids) cache_keys_ai_features = Array.wrap(ids).map { |id| ["users", id, GROUP_WITH_AI_ENABLED_CACHE_KEY] } cache_keys_ai_chat = Array.wrap(ids).map { |id| ["users", id, GROUP_WITH_AI_CHAT_ENABLED_CACHE_KEY] } + cache_keys_ai_chat_group_ids = Array.wrap(ids).map { |id| ["users", id, GROUP_IDS_WITH_AI_CHAT_ENABLED_CACHE_KEY] } - cache_keys = cache_keys_ai_features + cache_keys_ai_chat + cache_keys = cache_keys_ai_features + cache_keys_ai_chat + cache_keys_ai_chat_group_ids ::Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do Rails.cache.delete_multi(cache_keys) end @@ -654,6 +656,16 @@ def any_group_with_ai_available? end end + def ai_chat_enabled_namespace_ids + return [] unless ::Feature.enabled?(:ai_chat_enabled_namespace_ids) + + Rails.cache.fetch(['users', id, GROUP_IDS_WITH_AI_CHAT_ENABLED_CACHE_KEY], expires_in: GROUP_WITH_AI_CHAT_ENABLED_CACHE_PERIOD) do + groups = member_namespaces.with_ai_supported_plan(:ai_chat) + groups = ::Feature.enabled?(:duo_chat_ga) ? groups : groups.namespace_settings_with_ai_features_enabled + groups.pluck(Arel.sql('DISTINCT traversal_ids[1]')) + end + end + def any_group_with_ai_chat_available? Rails.cache.fetch(['users', id, GROUP_WITH_AI_CHAT_ENABLED_CACHE_KEY], expires_in: GROUP_WITH_AI_CHAT_ENABLED_CACHE_PERIOD) do groups = member_namespaces.with_ai_supported_plan(:ai_chat) diff --git a/ee/app/services/llm/chat_service.rb b/ee/app/services/llm/chat_service.rb index 36fdff9f34bc55e930cf018b5b4a809e27b24515..bab386404e5e623129b35a65646959fc2f5bfe30 100644 --- a/ee/app/services/llm/chat_service.rb +++ b/ee/app/services/llm/chat_service.rb @@ -21,7 +21,13 @@ def perform @options = options.merge(agent_version_id: agent_version.id) end - track_internal_event('request_duo_chat_response', user: user, project: project, namespace: namespace) + track_internal_event( + 'request_duo_chat_response', + user: user, + project: project, + namespace: namespace, + feature_enabled_by_namespace_ids: user.ai_chat_enabled_namespace_ids + ) prompt_message.save! GraphqlTriggers.ai_completion_response(prompt_message) diff --git a/ee/config/feature_flags/gitlab_com_derisk/ai_chat_enabled_namespace_ids.yml b/ee/config/feature_flags/gitlab_com_derisk/ai_chat_enabled_namespace_ids.yml new file mode 100644 index 0000000000000000000000000000000000000000..73738fd4b428428b2f9d1d59c6560cfbb47b1f19 --- /dev/null +++ b/ee/config/feature_flags/gitlab_com_derisk/ai_chat_enabled_namespace_ids.yml @@ -0,0 +1,9 @@ +--- +name: ai_chat_enabled_namespace_ids +feature_issue_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149221 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/456920 +milestone: '17.0' +group: group::ai framework +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb index 2eb1a0e5ea90de9cf85aff47bb1a09a168c6d012..00a75ef8eb314b7c6efab1668fea13d75bf3ef89 100644 --- a/ee/spec/models/ee/user_spec.rb +++ b/ee/spec/models/ee/user_spec.rb @@ -3907,6 +3907,109 @@ end end + describe '#ai_chat_enabled_namespace_ids', :saas, :use_clean_rails_redis_caching do + using RSpec::Parameterized::TableSyntax + + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:ultimate_group) { create(:group_with_plan, plan: :ultimate_plan, name: 'ultimate_group') } + let_it_be_with_reload(:ultimate_group_id) { ultimate_group.id } + let_it_be_with_reload(:bronze_group) { create(:group_with_plan, plan: :bronze_plan, name: 'bronze_group') } + let_it_be_with_reload(:free_group) { create(:group_with_plan, plan: :free_plan, name: 'free_group') } + let_it_be_with_reload(:group_without_plan) { create(:group, name: 'group_without_plan') } + let_it_be_with_reload(:trial_group) { create(:group_with_plan, plan: :ultimate_plan, trial_ends_on: 1.day.from_now, name: 'trial_group') } + let_it_be_with_reload(:trial_group_id) { trial_group.id } + let_it_be_with_reload(:ultimate_sub_group) { create(:group, parent: ultimate_group, name: 'ultimate_sub_group') } + let_it_be_with_reload(:bronze_sub_group) { create(:group, parent: bronze_group, name: 'bronze_sub_group') } + + subject(:group_with_ai_chat_enabled) { user.ai_chat_enabled_namespace_ids } + + where(:group, :result) do + ref(:bronze_group) | [] + ref(:free_group) | [] + ref(:group_without_plan) | [] + ref(:ultimate_group) | [ref(:ultimate_group_id)] + ref(:trial_group) | [ref(:trial_group_id)] + end + + with_them do + context 'when member of the root group' do + before do + group.add_guest(user) + end + + context 'when ai features are enabled' do + include_context 'with ai features enabled for group' + + it { is_expected.to eq(result) } + + it 'caches the result' do + group_with_ai_chat_enabled + + expect(Rails.cache.fetch(['users', user.id, 'group_ids_with_ai_chat_enabled'])).to eq(result) + end + end + + context 'when duo_chat_ga feature flag is disaabled and ai features are not enabled' do + before do + stub_feature_flags(duo_chat_ga: false) + end + + it { is_expected.to eq([]) } + end + end + end + + context 'when member of a sub-group only' do + include_context 'with ai features enabled for group' + + context 'with eligible group' do + let(:group) { ultimate_group } + + before do + ultimate_sub_group.add_guest(user) + end + + it { is_expected.to eq([group.id]) } + end + + context 'with not eligible group' do + let(:group) { bronze_group } + + before do + bronze_sub_group.add_guest(user) + end + + it { is_expected.to eq([]) } + end + end + + context 'when member of a project only' do + include_context 'with ai features enabled for group' + + context 'with eligible group' do + let(:group) { ultimate_group } + let_it_be(:project) { create(:project, group: ultimate_group) } + + before do + project.add_guest(user) + end + + it { is_expected.to eq([group.id]) } + end + + context 'with not eligible group' do + let(:group) { bronze_group } + let_it_be(:project) { create(:project, group: bronze_group) } + + before do + project.add_guest(user) + end + + it { is_expected.to eq([]) } + end + end + end + describe '.clear_group_with_ai_available_cache', :use_clean_rails_redis_caching do let_it_be(:user) { create(:user) } let_it_be(:other_user) { create(:user) } @@ -3916,18 +4019,21 @@ user.any_group_with_ai_available? other_user.any_group_with_ai_available? yet_another_user.any_group_with_ai_chat_available? + yet_another_user.ai_chat_enabled_namespace_ids end it 'clears cache from users with the given ids', :aggregate_failures do expect(Rails.cache.fetch(['users', user.id, 'group_with_ai_enabled'])).to eq(false) expect(Rails.cache.fetch(['users', other_user.id, 'group_with_ai_enabled'])).to eq(false) expect(Rails.cache.fetch(['users', yet_another_user.id, 'group_with_ai_chat_enabled'])).to eq(false) + expect(Rails.cache.fetch(['users', yet_another_user.id, 'group_ids_with_ai_chat_enabled'])).to eq([]) described_class.clear_group_with_ai_available_cache([user.id, yet_another_user.id]) expect(Rails.cache.fetch(['users', user.id, 'group_with_ai_enabled'])).to be_nil expect(Rails.cache.fetch(['users', other_user.id, 'group_with_ai_enabled'])).to eq(false) expect(Rails.cache.fetch(['users', yet_another_user.id, 'group_with_ai_chat_enabled'])).to be_nil + expect(Rails.cache.fetch(['users', yet_another_user.id, 'group_ids_with_ai_chat_enabled'])).to be_nil end it 'clears cache when given a single id', :aggregate_failures do diff --git a/ee/spec/services/llm/chat_service_spec.rb b/ee/spec/services/llm/chat_service_spec.rb index 01fe15d86836e88e2e9e147d388672d2cfc662f7..a6836eede2e68853477ecc2d5a4d984d52d0e548 100644 --- a/ee/spec/services/llm/chat_service_spec.rb +++ b/ee/spec/services/llm/chat_service_spec.rb @@ -49,7 +49,9 @@ it_behaves_like 'schedules completion worker' it_behaves_like 'llm service caches user request' it_behaves_like 'service emitting message for user prompt' - it_behaves_like 'track internal event for Duo Chat' + it_behaves_like 'track internal event for Duo Chat' do + let(:feature_enabled_by_namespace_ids) { [] } + end end context 'when resource is a user' do @@ -62,6 +64,7 @@ it_behaves_like 'service emitting message for user prompt' it_behaves_like 'track internal event for Duo Chat' do let(:project) { nil } + let(:feature_enabled_by_namespace_ids) { [] } end end end @@ -159,7 +162,9 @@ it_behaves_like 'schedules completion worker' it_behaves_like 'llm service caches user request' it_behaves_like 'service emitting message for user prompt' - it_behaves_like 'track internal event for Duo Chat' + it_behaves_like 'track internal event for Duo Chat' do + let(:feature_enabled_by_namespace_ids) { [group.id] } + end end context 'when resource is a user' do @@ -172,6 +177,7 @@ it_behaves_like 'service emitting message for user prompt' it_behaves_like 'track internal event for Duo Chat' do let(:project) { nil } + let(:feature_enabled_by_namespace_ids) { [group.id] } end end end