diff --git a/doc/development/internal_analytics/metrics/metrics_lifecycle.md b/doc/development/internal_analytics/metrics/metrics_lifecycle.md
index 6899f31a2b7a49511e6b7b7ffe375fef60927a18..0c1bb5a1b9e5ad7639a03694f8b7ed630831e06a 100644
--- a/doc/development/internal_analytics/metrics/metrics_lifecycle.md
+++ b/doc/development/internal_analytics/metrics/metrics_lifecycle.md
@@ -57,3 +57,25 @@ Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automa
 
    Do not remove the metric's YAML definition altogether. Some self-managed instances might not immediately update to the latest version of GitLab, and
    therefore continue to report the removed metric. The Analytics Instrumentation team requires a record of all removed metrics to identify and filter them.
+
+## Group name changes
+
+When the name of a group that owns events or metrics is changed, the `product_group` property should be updated in all metric and event definitions belonging to that group.
+
+The `product_group_renamer` script can update all the definitions so you do not have to do it manually.
+
+For example, if the group 5-min-app was renamed to 2-min-app, you can update the relevant files like this:
+
+```shell
+$ ruby scripts/internal_events/product_group_renamer.rb 5-min-app 2-min-app
+Updated '5-min-app' to '2-min-app' in 3 files
+
+Updated files:
+  config/metrics/schema/product_groups.json
+  config/metrics/counts_28d/20210216184517_p_ci_templates_5_min_production_app_monthly.yml
+  config/metrics/counts_7d/20210216184515_p_ci_templates_5_min_production_app_weekly.yml
+```
+
+After running the script, you must commit all the modified files to Git and create a merge request.
+
+If a group is split into multiple groups, you need to manually update the product_group.
diff --git a/scripts/internal_events/product_group_renamer.rb b/scripts/internal_events/product_group_renamer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2f2680c4172a7b7566559d2506270c2bd2bb7421
--- /dev/null
+++ b/scripts/internal_events/product_group_renamer.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+# !/usr/bin/env ruby
+#
+# Update group name in all relevant metric and event definition after a group name change.
+
+require 'json'
+PRODUCT_GROUPS_SCHEMA_PATH = 'config/metrics/schema/product_groups.json'
+ALL_METRIC_AND_EVENT_DEFINITIONS_GLOB = "{ee/,}config/{metrics/*,events}/*.yml"
+
+class ProductGroupRenamer
+  def initialize(schema_path, definitions_glob)
+    @schema_path = schema_path
+    @definitions_glob = definitions_glob
+  end
+
+  def rename_product_group(old_name, new_name)
+    changed_files = []
+    # Rename the product group in the schema
+
+    current_schema = File.read(@schema_path)
+    product_group_schema = JSON.parse(current_schema)
+
+    product_group_schema["enum"].delete(old_name)
+    product_group_schema["enum"].push(new_name) unless product_group_schema["enum"].include?(new_name)
+    product_group_schema["enum"].sort!
+
+    new_schema = "#{JSON.pretty_generate(product_group_schema)}\n"
+    if new_schema != current_schema
+      File.write(@schema_path, new_schema)
+      changed_files << @schema_path
+    end
+
+    # Rename product group in all metric and event definitions
+    Dir.glob(@definitions_glob).each do |file_path|
+      file_content = File.read(File.expand_path(file_path))
+
+      new_content = file_content.gsub(/product_group:\s*['"]?#{old_name}['"]?$/, "product_group: #{new_name}")
+
+      if new_content != file_content
+        File.write(file_path, new_content)
+        changed_files << file_path
+      end
+    end
+
+    changed_files
+  end
+end
+
+if $PROGRAM_NAME == __FILE__
+  if ARGV.length != 2
+    puts <<~TEXT
+    Usage:
+      When a group is renamed, this script replaces the value for "product_group" in all matching event & metric definitions.
+
+    Format:
+      ruby #{$PROGRAM_NAME} OLD_NAME NEW_NAME
+
+    Example:
+      ruby #{$PROGRAM_NAME} pipeline_authoring renamed_pipeline_authoring
+    TEXT
+    exit
+  end
+
+  old_name = ARGV[0]
+  new_name = ARGV[1]
+
+  changed_files = ProductGroupRenamer
+    .new(PRODUCT_GROUPS_SCHEMA_PATH, ALL_METRIC_AND_EVENT_DEFINITIONS_GLOB)
+    .rename_product_group(old_name, new_name)
+
+  puts "Updated '#{old_name}' to '#{new_name}' in #{changed_files.length} files"
+  puts
+
+  if changed_files.any?
+    puts "Updated files:"
+    changed_files.each do |file_path|
+      puts "  #{file_path}"
+    end
+  end
+end
diff --git a/spec/fixtures/scripts/product_group_renamer/event_definition.yml b/spec/fixtures/scripts/product_group_renamer/event_definition.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd4b48e762a6a8e8e91e5da57f230fe9748b9a5c
--- /dev/null
+++ b/spec/fixtures/scripts/product_group_renamer/event_definition.yml
@@ -0,0 +1,18 @@
+---
+description: Engineer uses Internal Event CLI to define a new event
+internal_events: true
+action: internal_events_cli_used
+identifiers:
+- project
+- namespace
+- user
+product_group: a_group_name
+milestone: '16.6'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149010
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml b/spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml
new file mode 100644
index 0000000000000000000000000000000000000000..57bf3a1b779c6d31ab2c67fb82e790680c7139f6
--- /dev/null
+++ b/spec/fixtures/scripts/product_group_renamer/event_definition_from_another_group.yml
@@ -0,0 +1,18 @@
+---
+description: Engineer uses Internal Event CLI to define a new event
+internal_events: true
+action: internal_events_cli_used
+identifiers:
+- project
+- namespace
+- user
+product_group: another_group
+milestone: '17.0'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149010
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/product_group_renamer/metric_definition.yml b/spec/fixtures/scripts/product_group_renamer/metric_definition.yml
new file mode 100644
index 0000000000000000000000000000000000000000..12a581501da789096f259c6c3cdcce633bdd075a
--- /dev/null
+++ b/spec/fixtures/scripts/product_group_renamer/metric_definition.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.count_total_internal_events_cli_used
+description: Total count of when an event was defined using the CLI
+product_group: a_group_name
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.0'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149010
+time_frame: all
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: internal_events_cli_used
diff --git a/spec/fixtures/scripts/product_group_renamer/product_groups.json b/spec/fixtures/scripts/product_group_renamer/product_groups.json
new file mode 100644
index 0000000000000000000000000000000000000000..99fd56a0465f908d7f996006336da5890c4404e7
--- /dev/null
+++ b/spec/fixtures/scripts/product_group_renamer/product_groups.json
@@ -0,0 +1,7 @@
+{
+  "type": "string",
+  "enum": [
+    "a_better_group_name",
+    "another_group_name"
+  ]
+}
diff --git a/spec/scripts/internal_events/product_group_renamer_spec.rb b/spec/scripts/internal_events/product_group_renamer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..98d847d608837f24785bcf17105e3ada7311f851
--- /dev/null
+++ b/spec/scripts/internal_events/product_group_renamer_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../scripts/internal_events/product_group_renamer'
+
+RSpec.describe ProductGroupRenamer, feature_category: :service_ping do
+  let(:renamer) { described_class.new(schema_path, definitions_glob) }
+
+  context 'with real definitions', :aggregate_failures do
+    let(:schema_path) { PRODUCT_GROUPS_SCHEMA_PATH }
+    let(:definitions_glob) { ALL_METRIC_AND_EVENT_DEFINITIONS_GLOB }
+
+    it 'reads all definitions files' do
+      allow(File).to receive(:read).and_call_original
+
+      Gitlab::Tracking::EventDefinition.definitions.each do |event_definition|
+        expect(File).to receive(:read).with(event_definition.path)
+        expect(File).not_to receive(:write).with(event_definition.path)
+      end
+
+      Gitlab::Usage::MetricDefinition.definitions.each_value do |metric_definition|
+        expect(File).to receive(:read).with(metric_definition.path)
+        expect(File).not_to receive(:write).with(metric_definition.path)
+      end
+
+      renamer.rename_product_group('old_name', 'new_name')
+    end
+  end
+
+  describe '#rename_product_group', :aggregate_failures do
+    let(:temp_dir) { Dir.mktmpdir }
+    let(:schema_path) { File.join(temp_dir, 'product_groups.json') }
+    let(:event_definition_path) { File.join(temp_dir, 'event_definition.yml') }
+    let(:metric_definition_path) { File.join(temp_dir, 'metric_definition.yml') }
+    let(:event_definition_from_another_group_path) do
+      File.join(temp_dir, 'event_definition_from_another_group.yml')
+    end
+
+    let(:definitions_glob) { [event_definition_path, metric_definition_path, event_definition_from_another_group_path] }
+
+    before do
+      FileUtils.cp_r(File.join('spec/fixtures/scripts/product_group_renamer', '.'), temp_dir)
+    end
+
+    after do
+      FileUtils.rm_rf(temp_dir)
+    end
+
+    it 'renames product group in the schema and the definitions' do
+      renamer.rename_product_group('a_group_name', 'a_better_group_name')
+
+      schema_content = File.read(schema_path)
+
+      expect(schema_content).to include('a_better_group_name')
+      expect(schema_content).not_to include('a_group_name')
+      expect(File.read(event_definition_path)).to include('product_group: a_better_group_name')
+      expect(File.read(metric_definition_path)).to include('product_group: a_better_group_name')
+      expect(File.read(event_definition_from_another_group_path)).not_to include('product_group: a_better_group_name')
+    end
+  end
+end