diff --git a/app/controllers/concerns/record_user_last_activity.rb b/app/controllers/concerns/record_user_last_activity.rb
index 6ac87d8f27b0eaf85eea7f2f600c8027376616d0..501590d33d992f9d29260df3a9b399dd5ef4110b 100644
--- a/app/controllers/concerns/record_user_last_activity.rb
+++ b/app/controllers/concerns/record_user_last_activity.rb
@@ -20,6 +20,7 @@ def set_user_last_activity
     return if Gitlab::Database.read_only?
     return unless current_user && current_user.last_activity_on != Date.today
 
-    Users::ActivityService.new(current_user).execute
+    # TODO: add namespace & project - https://gitlab.com/gitlab-org/gitlab/-/issues/387952
+    Users::ActivityService.new(author: current_user).execute
   end
 end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 942cb9beed445f9e78c30a27fe7cb085f2e84e25..2f01bdecd2395de9019d03580618ded90b4b4348 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -144,7 +144,8 @@ def disable_query_limiting
   def set_user_last_activity
     return unless current_user
 
-    Users::ActivityService.new(current_user).execute
+    # TODO: add namespace & project - https://gitlab.com/gitlab-org/gitlab/-/issues/387951
+    Users::ActivityService.new(author: current_user).execute
   end
 
   def track_vs_code_usage
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 144ec4c0de9ba5dfde173b14fb1645852438e217..bd3461d83318396a7de0faa867f436fd77e9fcac 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -116,7 +116,7 @@ def access_klass
     end
 
     def log_user_activity
-      Users::ActivityService.new(user).execute
+      Users::ActivityService.new(author: user, project: project, namespace: project&.namespace).execute
     end
   end
 end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 699dcf1adac43fff7e017bada28060a34f80f5e6..b6aba04c877eaac052864fb04e5bbffbeec172ea 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -289,7 +289,7 @@ def log_audit_event(user, resource, options = {})
 
   def log_user_activity(user)
     login_counter.increment
-    Users::ActivityService.new(user).execute
+    Users::ActivityService.new(author: user).execute
   end
 
   def load_recaptcha
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index bf4a26400e1b950c5587c50c57d799bfc5aa5857..acc5176059d35534f222c20f4f89064e1641bf4d 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -245,7 +245,7 @@ def create_push_event(service_class, project, current_user, push_data)
     Users::LastPushEventService.new(current_user)
       .cache_last_push_event(event)
 
-    Users::ActivityService.new(current_user).execute
+    Users::ActivityService.new(author: current_user, namespace: namespace, project: project).execute
   end
 
   def create_event(resource_parent, current_user, status, attributes = {})
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index 4978f778870b6c7f1876b0226dfeb61d3dd0daba..c8f9c28061f06e04d96f7f9414c4d5aa4e8aa376 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -4,38 +4,56 @@ module Users
   class ActivityService
     LEASE_TIMEOUT = 1.minute.to_i
 
-    def initialize(author)
+    def initialize(author:, namespace: nil, project: nil)
       @user = if author.respond_to?(:username)
                 author
               elsif author.respond_to?(:user)
                 author.user
               end
 
-      @user = nil unless @user.is_a?(User)
+      @user = nil unless user.is_a?(User)
+      @namespace = namespace
+      @project = project
     end
 
     def execute
-      return unless @user
+      return unless user
 
       ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes { record_activity }
     end
 
     private
 
+    attr_reader :user, :namespace, :project
+
     def record_activity
       return if Gitlab::Database.read_only?
 
       today = Date.today
 
-      return if @user.last_activity_on == today
+      return if user.last_activity_on == today
 
-      lease = Gitlab::ExclusiveLease.new("activity_service:#{@user.id}",
+      lease = Gitlab::ExclusiveLease.new("activity_service:#{user.id}",
                                          timeout: LEASE_TIMEOUT)
       return unless lease.try_obtain
 
-      @user.update_attribute(:last_activity_on, today)
+      user.update_attribute(:last_activity_on, today)
+
+      Gitlab::UsageDataCounters::HLLRedisCounter.track_event('unique_active_user', values: user.id)
+
+      return unless Feature.enabled?(:route_hll_to_snowplow_phase3)
 
-      Gitlab::UsageDataCounters::HLLRedisCounter.track_event('unique_active_user', values: @user.id)
+      Gitlab::Tracking.event(
+        'Users::ActivityService',
+        'perform_action',
+        user: user,
+        namespace: namespace,
+        project: project,
+        label: 'redis_hll_counters.manage.unique_active_users_monthly',
+        context: [
+          Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'unique_active_user').to_context
+        ]
+      )
     end
   end
 end
diff --git a/config/events/1671713111_Users__ActivityService_perform_action.yml b/config/events/1671713111_Users__ActivityService_perform_action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b1f90d42b48486089398e689a27f84299e13087
--- /dev/null
+++ b/config/events/1671713111_Users__ActivityService_perform_action.yml
@@ -0,0 +1,23 @@
+---
+description: User's last_activity_on gets updated
+category: Users::ActivityService
+action: perform_action
+label_description: key_path of Service Ping metric redis_hll_counters.manage.unique_active_users_monthly
+property_description:
+value_description:
+extra_properties:
+identifiers:
+product_section: dev
+product_stage: manage
+product_group: manage
+product_category:
+milestone: "15.8"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108108
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
+
diff --git a/ee/spec/controllers/repositories/git_http_controller_spec.rb b/ee/spec/controllers/repositories/git_http_controller_spec.rb
index 1044e36dd239606bd33558de5b866ed3ca930e69..250cddee5e7195660651d68ed012f98621bf14d5 100644
--- a/ee/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/ee/spec/controllers/repositories/git_http_controller_spec.rb
@@ -8,6 +8,7 @@
 
     let_it_be(:group) { create(:group, :wiki_repo) }
     let_it_be(:user) { create(:user) }
+    let_it_be(:project) { nil }
 
     before_all do
       group.add_owner(user)
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 5e44902267693912d74be411e3787f461f340037..81b96207863e39dae754a3b7d6cda7003aa6911e 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -164,7 +164,7 @@ class API < ::API::Base
 
     namespace do
       after do
-        ::Users::ActivityService.new(@current_user).execute
+        ::Users::ActivityService.new(author: @current_user, project: @project, namespace: @group).execute
       end
 
       # Mount endpoints to include in the OpenAPI V2 documentation here
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 56db6ee4c5cb90f845c69df1b0a7e4bc4f0c6bf6..816b8deb4618a11b6da7ea02825125efe141e1f3 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -58,7 +58,9 @@ def parse_env
       def log_user_activity(actor)
         commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS
 
-        ::Users::ActivityService.new(actor).execute if commands.include?(params[:action])
+        return unless commands.include?(params[:action])
+
+        ::Users::ActivityService.new(author: actor, namespace: project&.namespace, project: project).execute
       end
 
       def redis_ping
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 78b3cc63b085e3c25b09dbed9712490e29dd2e52..1f7d169bae565dad00f77ecead201dafd1d9c853 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -221,7 +221,7 @@ def succesful_login(user_params, sesion_params: {})
           expect(Gitlab::Metrics).to receive(:counter)
                                       .with(:successful_login_captcha_total, anything)
                                       .and_return(counter)
-          expect(Gitlab::Metrics).to receive(:counter).and_call_original
+          expect(Gitlab::Metrics).to receive(:counter).at_least(1).time.and_call_original
 
           post(:create, params: { user: user_params }, session: sesion_params)
         end
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 1ad0f20b77e1ae8d403450c50cfd66ca129782a0..35851fff6c883a74994c50073832b1e36f0b1c76 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -12,8 +12,22 @@
     let(:user) { create(:user, last_activity_on: Date.yesterday) }
 
     it 'updates the users last_activity_on to the current date' do
+      expect(Users::ActivityService).to receive(:new).with(author: user, project: nil, namespace: nil).and_call_original
+
       expect { get api('/groups', user) }.to change { user.reload.last_activity_on }.to(Date.today)
     end
+
+    context "with a project-specific path" do
+      let_it_be(:project) { create(:project, :public) }
+      let_it_be(:user) { project.first_owner }
+
+      it "passes correct arguments to ActivityService" do
+        activity_args = { author: user, project: project, namespace: project.group }
+        expect(Users::ActivityService).to receive(:new).with(activity_args).and_call_original
+
+        get(api("/projects/#{project.id}/issues", user))
+      end
+    end
   end
 
   describe 'User with only read_api scope personal access token' do
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 767f3e8b5b5382cfc415780a12a22788ed660af1..ca32271f5738c34788ad7ff19168144261db094c 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -651,6 +651,12 @@ def request
                   headers: gitlab_shell_internal_api_request_header
                 )
               end
+
+              it "updates user's activity data" do
+                expect(::Users::ActivityService).to receive(:new).with(author: user, namespace: project.namespace, project: project)
+
+                request
+              end
             end
           end
         end
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 47a4b943d8313e081ce4b2acd8c634a8d49c172b..6c0d93f568a7dd18007bce1ccd1a83c67f0e9ee1 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -7,9 +7,21 @@
 
   let(:user) { create(:user, last_activity_on: last_activity_on) }
 
-  subject { described_class.new(user) }
+  subject { described_class.new(author: user) }
 
   describe '#execute', :clean_gitlab_redis_shared_state do
+    shared_examples 'does not update last_activity_on' do
+      it 'does not update user attribute' do
+        expect { subject.execute }.not_to change(user, :last_activity_on)
+      end
+
+      it 'does not track Snowplow event' do
+        subject.execute
+
+        expect_no_snowplow_event
+      end
+    end
+
     context 'when last activity is nil' do
       let(:last_activity_on) { nil }
 
@@ -41,13 +53,29 @@
 
         subject.execute
       end
+
+      it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+        subject(:record_activity) { described_class.new(author: user, namespace: namespace, project: project).execute }
+
+        let(:feature_flag_name) { :route_hll_to_snowplow_phase3 }
+        let(:category) { described_class.name }
+        let(:action) { 'perform_action' }
+        let(:label) { 'redis_hll_counters.manage.unique_active_users_monthly' }
+        let(:namespace) { build(:group) }
+        let(:project) { build(:project) }
+        let(:context) do
+          payload = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+                                                             event: 'unique_active_user').to_context
+          [Gitlab::Json.dump(payload)]
+        end
+      end
     end
 
     context 'when a bad object is passed' do
       let(:fake_object) { double(username: 'hello') }
 
       it 'does not record activity' do
-        service = described_class.new(fake_object)
+        service = described_class.new(author: fake_object)
 
         expect(service).not_to receive(:record_activity)
 
@@ -58,9 +86,7 @@
     context 'when last activity is today' do
       let(:last_activity_on) { Date.today }
 
-      it 'does not update last_activity_on' do
-        expect { subject.execute }.not_to change(user, :last_activity_on)
-      end
+      it_behaves_like 'does not update last_activity_on'
 
       it 'does not try to obtain ExclusiveLease' do
         expect(Gitlab::ExclusiveLease).not_to receive(:new).with("activity_service:#{user.id}", anything)
@@ -76,19 +102,17 @@
         allow(Gitlab::Database).to receive(:read_only?).and_return(true)
       end
 
-      it 'does not update last_activity_on' do
-        expect { subject.execute }.not_to change(user, :last_activity_on)
-      end
+      it_behaves_like 'does not update last_activity_on'
     end
 
     context 'when a lease could not be obtained' do
       let(:last_activity_on) { nil }
 
-      it 'does not update last_activity_on' do
+      before do
         stub_exclusive_lease_taken("activity_service:#{user.id}", timeout: 1.minute.to_i)
-
-        expect { subject.execute }.not_to change(user, :last_activity_on)
       end
+
+      it_behaves_like 'does not update last_activity_on'
     end
   end
 
@@ -104,7 +128,7 @@
         end
 
         let(:service) do
-          service = described_class.new(user)
+          service = described_class.new(author: user)
 
           ::Gitlab::Database::LoadBalancing::Session.clear_session
 
@@ -123,7 +147,7 @@
       end
 
       context 'database load balancing is not configured' do
-        let(:service) { described_class.new(user) }
+        let(:service) { described_class.new(author: user) }
 
         it 'updates user without error' do
           service.execute
diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
index 3a7588a5cc93b692006d3e659553a854b04f01ed..cc28a79b4ca48eb5820471bf8d1e4824a5d083d3 100644
--- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
@@ -61,9 +61,14 @@
       end
 
       it 'updates the user activity' do
-        expect_next_instance_of(Users::ActivityService) do |activity_service|
-          expect(activity_service).to receive(:execute)
-        end
+        activity_project = container.is_a?(PersonalSnippet) ? nil : project
+
+        activity_service = instance_double(Users::ActivityService)
+
+        args = { author: user, project: activity_project, namespace: activity_project&.namespace }
+        expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service)
+
+        expect(activity_service).to receive(:execute)
 
         get :info_refs, params: params
       end