diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md
index f234ee92af9a3af712771606d33dbd33777cd19f..b5483ceaf877c18797d151c492c7851327fbc13c 100644
--- a/doc/user/compliance/audit_event_types.md
+++ b/doc/user/compliance/audit_event_types.md
@@ -386,6 +386,12 @@ Audit event types belong to the following product categories.
 | [`epic_created_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an epic is created by a group access token| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | Group |
 | [`epic_reopened_by_project_bot`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121485) | Triggered when an epic is reopened by a group access token| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/323299) | Group |
 
+### Product analytics data management
+
+| Name | Description | Saved to database | Streamed | Introduced in | Scope |
+|:------------|:------------|:------------------|:---------|:--------------|:--------------|
+| [`product_analytics_settings_update`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154101) | Triggered when product analytics settings are changed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/463318) | Project |
+
 ### Project
 
 | Name | Description | Saved to database | Streamed | Introduced in | Scope |
diff --git a/ee/config/audit_events/types/product_analytics_settings_update.yml b/ee/config/audit_events/types/product_analytics_settings_update.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4c5a17f351a884a962e6fd569ac9fb73930fc7cc
--- /dev/null
+++ b/ee/config/audit_events/types/product_analytics_settings_update.yml
@@ -0,0 +1,10 @@
+---
+name: product_analytics_settings_update
+description: Triggered when product analytics settings are changed
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/463318
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154101
+feature_category: product_analytics_data_management
+milestone: '17.1'
+saved_to_database: true
+streamed: true
+scope: [Project]
diff --git a/ee/lib/audit/project_analytics_changes_auditor.rb b/ee/lib/audit/project_analytics_changes_auditor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..447bedbb53e643e45584d98e83dc0ac749f03bd5
--- /dev/null
+++ b/ee/lib/audit/project_analytics_changes_auditor.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Audit # rubocop:disable Gitlab/BoundedContexts -- govern::compliance will need to refactor all instances of Audit
+  class ProjectAnalyticsChangesAuditor < BaseChangesAuditor
+    ATTRIBUTE_NAMES = [
+      :encrypted_product_analytics_configurator_connection_string,
+      :product_analytics_data_collector_host,
+      :cube_api_base_url,
+      :encrypted_cube_api_key
+    ].freeze
+
+    def initialize(current_user, project_setting, project)
+      @project = project
+
+      super(current_user, project_setting)
+    end
+
+    def execute
+      ATTRIBUTE_NAMES.each do |attr|
+        next unless model.previous_changes.key?(attr.to_s)
+
+        audit_context = {
+          name: 'product_analytics_settings_update',
+          author: @current_user,
+          scope: @project,
+          target: @project,
+          message: "Changed #{attr}",
+          additional_details: details(attr)
+        }
+
+        ::Gitlab::Audit::Auditor.audit(audit_context)
+      end
+    end
+
+    def details(column)
+      return { change: column } if
+        [:encrypted_product_analytics_configurator_connection_string, :encrypted_cube_api_key].include?(column)
+
+      {
+        change: column,
+        from: @model.previous_changes[column].first,
+        to: @model.previous_changes[column].last
+      }
+    end
+  end
+end
diff --git a/ee/lib/audit/project_changes_auditor.rb b/ee/lib/audit/project_changes_auditor.rb
index 6fd6f3ff3d520e836b0116b20edb5267a5df2e61..aca8427fa0d590a1d0e46afe8a07ece71811df6c 100644
--- a/ee/lib/audit/project_changes_auditor.rb
+++ b/ee/lib/audit/project_changes_auditor.rb
@@ -111,6 +111,7 @@ def execute
       audit_compliance_framework_changes
       audit_project_setting_changes
       audit_project_ci_cd_setting_changes
+      audit_analytics_setting_changes
     end
 
     private
@@ -133,6 +134,10 @@ def audit_merge_method
       ::Gitlab::Audit::Auditor.audit(audit_context)
     end
 
+    def audit_analytics_setting_changes
+      Audit::ProjectAnalyticsChangesAuditor.new(@current_user, model.project_setting, model).execute
+    end
+
     def audit_project_feature_changes
       Audit::ProjectFeatureChangesAuditor.new(@current_user, model.project_feature, model).execute
     end
diff --git a/ee/spec/lib/audit/project_analytics_changes_auditor_spec.rb b/ee/spec/lib/audit/project_analytics_changes_auditor_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0907cc900780085104df345f3d3a7804d2406b33
--- /dev/null
+++ b/ee/spec/lib/audit/project_analytics_changes_auditor_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Audit::ProjectAnalyticsChangesAuditor, feature_category: :product_analytics_data_management do
+  describe 'auditing project analytics changes' do
+    let_it_be(:user) { create(:user) }
+    let_it_be(:group) { create(:group) }
+    let_it_be(:project) { create(:project, group: group) }
+
+    subject(:auditor) { described_class.new(user, project.project_setting, project) }
+
+    before do
+      project.reload
+      stub_licensed_features(extended_audit_events: true, external_audit_events: true)
+    end
+
+    context 'when the cube_api_key is set' do
+      before do
+        project.project_setting.update!(cube_api_key: "thisisasecretkey")
+      end
+
+      it 'adds an audit event', :aggregate_failures do
+        expect { auditor.execute }.to change { AuditEvent.count }.by(1)
+        expect(AuditEvent.last.details)
+          .to include({ change: :encrypted_cube_api_key })
+
+        # 'from' and 'to' should be nil, as their value is encrypted
+        # and we should not expose it in the audit logs
+        expect(AuditEvent.last.details[:from]).to be_nil
+        expect(AuditEvent.last.details[:to]).to be_nil
+      end
+    end
+
+    context 'when the snowplow configurator connection string is set' do
+      before do
+        project.project_setting.update!(product_analytics_configurator_connection_string: "http://example.com")
+      end
+
+      it 'adds an audit event', :aggregate_failures do
+        expect { auditor.execute }.to change { AuditEvent.count }.by(1)
+        expect(AuditEvent.last.details)
+          .to include({ change: :encrypted_product_analytics_configurator_connection_string })
+
+        # 'from' and 'to' should be nil, as their value is encrypted
+        # and we should not expose it in the audit logs
+        expect(AuditEvent.last.details[:from]).to be_nil
+        expect(AuditEvent.last.details[:to]).to be_nil
+      end
+    end
+
+    context 'when the product_analytics_data_collector_host is set' do
+      before do
+        project.project_setting.update!(product_analytics_data_collector_host: "http://example2.com")
+      end
+
+      it 'adds an audit event' do
+        expect { auditor.execute }.to change { AuditEvent.count }.by(1)
+        expect(AuditEvent.last.details)
+          .to include({ change: :product_analytics_data_collector_host, from: nil, to: "http://example2.com" })
+      end
+    end
+
+    context 'when the cube_api_base_url is set' do
+      before do
+        project.project_setting.update!(cube_api_base_url: "http://example3.com")
+      end
+
+      it 'adds an audit event' do
+        expect { auditor.execute }.to change { AuditEvent.count }.by(1)
+        expect(AuditEvent.last.details)
+          .to include({ change: :cube_api_base_url, from: nil, to: "http://example3.com" })
+      end
+    end
+  end
+end