diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
index d04c54c44cd40d8656e460fd20ce30ab1c2a1bfd..f59795126adf4785b53f27a5bc39c8e5392bb3d7 100644
--- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
+++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
@@ -490,8 +490,6 @@ Layout/LineEndStringConcatenationIndentation:
     - 'scripts/failed_tests.rb'
     - 'scripts/flaky_examples/prune-old-flaky-examples'
     - 'scripts/generate_rspec_pipeline.rb'
-    - 'scripts/internal_events/cli.rb'
-    - 'scripts/internal_events/cli/event_definer.rb'
     - 'scripts/lint-docs-redirects.rb'
     - 'scripts/qa/testcases-check'
     - 'scripts/trigger-build.rb'
diff --git a/scripts/internal_events/cli.rb b/scripts/internal_events/cli.rb
index e101216b8c0589d9c658019f85803e16f44e49f2..ff2392e115c38797275161b099d3e1f227271dd6 100755
--- a/scripts/internal_events/cli.rb
+++ b/scripts/internal_events/cli.rb
@@ -10,10 +10,10 @@
 require 'delegate'
 
 require_relative './cli/helpers'
-require_relative './cli/usage_viewer'
-require_relative './cli/metric_definer'
-require_relative './cli/event_definer'
-require_relative './cli/flow_advisor'
+require_relative './cli/flows/usage_viewer'
+require_relative './cli/flows/metric_definer'
+require_relative './cli/flows/event_definer'
+require_relative './cli/flows/flow_advisor'
 require_relative './cli/global_state'
 require_relative './cli/metric'
 require_relative './cli/event'
@@ -36,22 +36,22 @@ def run
       menu.enum "."
 
       menu.choice "New Event -- track when a specific scenario occurs on gitlab instances\n     " \
-                  "ex) a user applies a label to an issue", :new_event
+        "ex) a user applies a label to an issue", :new_event
       menu.choice "New Metric -- track the count of existing events over time\n     " \
-                  "ex) count unique users who assign labels to issues per month", :new_metric
+        "ex) count unique users who assign labels to issues per month", :new_metric
       menu.choice 'View Usage -- look at code and testing examples for existing events & metrics', :view_usage
       menu.choice '...am I in the right place?', :help_decide
     end
 
     case task
     when :new_event
-      InternalEventsCli::EventDefiner.new(cli).run
+      InternalEventsCli::Flows::EventDefiner.new(cli).run
     when :new_metric
-      InternalEventsCli::MetricDefiner.new(cli).run
+      InternalEventsCli::Flows::MetricDefiner.new(cli).run
     when :view_usage
-      InternalEventsCli::UsageViewer.new(cli).run
+      InternalEventsCli::Flows::UsageViewer.new(cli).run
     when :help_decide
-      InternalEventsCli::FlowAdvisor.new(cli).run
+      InternalEventsCli::Flows::FlowAdvisor.new(cli).run
     end
   end
 end
diff --git a/scripts/internal_events/cli/event_definer.rb b/scripts/internal_events/cli/event_definer.rb
deleted file mode 100755
index 0e5da5b35eb2249d9a60147e00ab3f9539defd81..0000000000000000000000000000000000000000
--- a/scripts/internal_events/cli/event_definer.rb
+++ /dev/null
@@ -1,280 +0,0 @@
-# frozen_string_literal: true
-
-require_relative './helpers'
-
-module InternalEventsCli
-  class EventDefiner
-    include Helpers
-
-    SCHEMA = ::JSONSchemer.schema(Pathname('config/events/schema.json'))
-    STEPS = [
-      'New Event',
-      'Description',
-      'Name',
-      'Context',
-      'URL',
-      'Group',
-      'Tiers',
-      'Save files'
-    ].freeze
-
-    IDENTIFIER_OPTIONS = {
-      %w[project namespace user] => 'Use case: For project-level user actions ' \
-                                    '(ex - issue_assignee_changed) [MOST COMMON]',
-      %w[namespace user] => 'Use case: For namespace-level user actions (ex - epic_assigned_to_milestone)',
-      %w[user] => 'Use case: For user-only actions (ex - admin_impersonated_user)',
-      %w[project namespace] => 'Use case: For project-level events without user interaction ' \
-                               '(ex - service_desk_request_received)',
-      %w[namespace] => 'Use case: For namespace-level events without user interaction ' \
-                       '(ex - stale_runners_cleaned_up)',
-      %w[feature_enabled_by_namespace_ids user] => 'Use case: For user actions attributable to multiple namespaces ' \
-                                                   '(ex - Code-Suggestions / Duo Pro)',
-      %w[] => "Use case: For instance-level events without user interaction [LEAST COMMON]"
-    }.freeze
-
-    IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.map { |k| k.join(', ') }.max_by(&:length)}]".length
-
-    attr_reader :cli, :event
-
-    def initialize(cli)
-      @cli = cli
-      @event = Event.new(milestone: MILESTONE)
-    end
-
-    def run
-      prompt_for_description
-      prompt_for_action
-      prompt_for_context
-      prompt_for_url
-      prompt_for_product_group
-      prompt_for_tier
-
-      outcome = create_event_file
-      display_result(outcome)
-
-      prompt_for_next_steps
-    end
-
-    private
-
-    def prompt_for_description
-      new_page!(1, 7, STEPS)
-      cli.say Text::EVENT_DESCRIPTION_INTRO
-
-      event.description = cli.ask("Describe what the event tracks: #{input_required_text}", **input_opts) do |q|
-        q.required true
-        q.modify :trim
-        q.messages[:required?] = Text::EVENT_DESCRIPTION_HELP
-      end
-    end
-
-    def prompt_for_action
-      new_page!(2, 7, STEPS)
-      cli.say Text::EVENT_ACTION_INTRO
-
-      event.action = cli.ask("Define the event name: #{input_required_text}", **input_opts) do |q|
-        q.required true
-        q.validate ->(input) { input =~ NAME_REGEX && cli.global.events.map(&:action).none?(input) }
-        q.modify :trim
-        q.messages[:valid?] = format_warning("Invalid event name. Only lowercase/numbers/underscores allowed. " \
-                                             "Ensure %{value} is not an existing event.")
-        q.messages[:required?] = Text::EVENT_ACTION_HELP
-      end
-    end
-
-    def prompt_for_context
-      new_page!(3, 7, STEPS)
-      cli.say format_prompt("EVENT CONTEXT #{counter(0, 2)}")
-      prompt_for_identifiers
-
-      new_page!(3, 7, STEPS) # Same "step" but increment counter
-      cli.say format_prompt("EVENT CONTEXT #{counter(1, 2)}")
-      prompt_for_additional_properties
-    end
-
-    def prompt_for_identifiers
-      cli.say Text::EVENT_IDENTIFIERS_INTRO % event.action
-
-      identifiers = prompt_for_array_selection(
-        'Which identifiers are available when the event occurs?',
-        IDENTIFIER_OPTIONS.keys,
-        per_page: IDENTIFIER_OPTIONS.length
-      ) { |choice| format_identifier_choice(choice) }
-
-      event.identifiers = identifiers if identifiers.any?
-    end
-
-    def format_identifier_choice(choice)
-      formatted_choice = choice.empty? ? 'None' : "[#{choice.sort.join(', ')}]"
-      buffer = IDENTIFIER_FORMATTING_BUFFER - formatted_choice.length
-
-      "#{formatted_choice}#{' ' * buffer} -- #{IDENTIFIER_OPTIONS[choice]}"
-    end
-
-    def prompt_for_additional_properties
-      cli.say Text::ADDITIONAL_PROPERTIES_INTRO
-
-      available_props = [:label, :property, :value, :add_extra_prop]
-
-      while available_props.any?
-        disabled = format_help('(already defined)')
-
-        # rubocop:disable Rails/NegateInclude -- this isn't Rails
-        options = [
-          { value: :none, name: 'None! Continue to next section!' },
-          disableable_option(
-            value: :label,
-            name: 'String 1 (attribute will be named `label`)',
-            disabled: disabled
-          ) { !available_props.include?(:label) },
-          disableable_option(
-            value: :property,
-            name: 'String 2 (attribute will be named `property`)',
-            disabled: disabled
-          ) { !available_props.include?(:property) },
-          disableable_option(
-            value: :value,
-            name: 'Number (attribute will be named `value`)',
-            disabled: disabled
-          ) { !available_props.include?(:value) },
-          disableable_option(
-            value: :add_extra_prop,
-            name: 'Add extra property (attribute will be named the input custom name)',
-            disabled: format_warning('(option disabled - use label/property/value first)')
-          ) do
-            !((!available_props.include?(:label) &&
-                !available_props.include?(:property)) ||
-                !available_props.include?(:value))
-          end
-        ]
-        # rubocop:enable Rails/NegateInclude
-
-        selected_property = cli.select(
-          "Which additional property do you want to add to the event?",
-          options,
-          help: format_help("(will reprompt for multiple)"),
-          **select_opts,
-          &disabled_format_callback
-        )
-
-        if selected_property == :none
-          available_props.clear
-        elsif selected_property == :add_extra_prop
-          property_name = prompt_for_add_extra_properties
-          property_description = prompt_for_text('Describe what the field will include:')
-          assign_extra_properties(property_name, property_description)
-        else
-          available_props.delete(selected_property)
-          property_description = prompt_for_text('Describe what the field will include:')
-          assign_extra_properties(selected_property, property_description)
-        end
-      end
-    end
-
-    def assign_extra_properties(property, description = nil)
-      event.additional_properties ||= {}
-      event.additional_properties[property.to_s] = {
-        'description' => description || 'TODO'
-      }
-    end
-
-    def prompt_for_add_extra_properties
-      primary_props = %w[label property value]
-
-      prompt_for_text('Define a name for the attribute:', **input_opts) do |q|
-        q.required true
-        q.validate ->(input) { input =~ NAME_REGEX && primary_props.none?(input) }
-        q.modify :trim
-        q.messages[:required?] = Text::ADDITIONAL_PROPERTIES_ADD_MORE_HELP
-        q.messages[:valid?] = format_warning("Invalid property name. Only lowercase/numbers/underscores allowed. " \
-                                             "Ensure %{value} is not one of `property, label, value`.")
-      end
-    end
-
-    def prompt_for_url
-      new_page!(4, 7, STEPS)
-
-      event.introduced_by_url = prompt_for_text('Which MR URL will merge the event definition?')
-    end
-
-    def prompt_for_product_group
-      new_page!(5, 7, STEPS)
-
-      product_group = prompt_for_group_ownership('Which group will own the event?')
-
-      event.product_group = product_group
-    end
-
-    def prompt_for_tier
-      new_page!(6, 7, STEPS)
-
-      event.tiers = prompt_for_array_selection(
-        'Which tiers will the event be recorded on?',
-        [%w[free premium ultimate], %w[premium ultimate], %w[ultimate]]
-      )
-
-      event.distributions = event.tiers.include?('free') ? %w[ce ee] : %w[ee]
-    end
-
-    def create_event_file
-      new_page!(7, 7, STEPS)
-
-      prompt_to_save_file(event.file_path, event.formatted_output)
-    end
-
-    def display_result(outcome)
-      new_page!
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_info('Done with event definition!')}
-
-        #{outcome || '  No files saved.'}
-
-        #{divider}
-
-          Do you need to create a metric? Probably!
-
-          Metrics are required to pull any usage data from self-managed instances or GitLab-Dedicated through Service Ping. Collected metric data can viewed in Tableau. Individual event details from GitLab.com can also be accessed through Snowflake.
-
-          Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake
-
-      TEXT
-    end
-
-    def prompt_for_next_steps
-      next_step = cli.select("How would you like to proceed?", **select_opts) do |menu|
-        menu.enum "."
-
-        menu.choice "New Event -- define another event", :new_event
-
-        choice = if File.exist?(event.file_path)
-                   ["Create Metric -- define a new metric using #{event.action}.yml", :add_metric]
-                 else
-                   ["Save & Create Metric -- save #{event.action}.yml and define a matching metric", :save_and_add]
-                 end
-
-        menu.default choice[0]
-        menu.choice(*choice)
-
-        menu.choice "View Usage -- look at code examples for #{event.action}.yml", :view_usage
-        menu.choice 'Exit', :exit
-      end
-
-      case next_step
-      when :new_event
-        InternalEventsCli::EventDefiner.new(cli).run
-      when :add_metric
-        MetricDefiner.new(cli, event.file_path).run
-      when :save_and_add
-        write_to_file(event.file_path, event.formatted_output, 'create')
-
-        MetricDefiner.new(cli, event.file_path).run
-      when :view_usage
-        UsageViewer.new(cli, event.file_path, event).run
-      when :exit
-        cli.say Text::FEEDBACK_NOTICE
-      end
-    end
-  end
-end
diff --git a/scripts/internal_events/cli/flow_advisor.rb b/scripts/internal_events/cli/flow_advisor.rb
deleted file mode 100644
index 5c8037aa684229049d5a1e2de7623e5b054e9c1a..0000000000000000000000000000000000000000
--- a/scripts/internal_events/cli/flow_advisor.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require_relative './helpers'
-
-# Entrypoint for help flow, which directs the user to the
-# correct flow or documentation based on their goal
-module InternalEventsCli
-  class FlowAdvisor
-    include Helpers
-
-    attr_reader :cli
-
-    def initialize(cli)
-      @cli = cli
-    end
-
-    def run
-      return use_case_error unless goal_is_tracking_usage?
-      return use_case_error unless usage_trackable_with_internal_events?
-
-      event_already_tracked? ? proceed_to_metric_definition : proceed_to_event_definition
-    end
-
-    private
-
-    def goal_is_tracking_usage?
-      new_page!
-
-      cli.say format_info("First, let's check your objective.\n")
-
-      cli.yes?('Are you trying to track customer usage of a GitLab feature?', **yes_no_opts)
-    end
-
-    def usage_trackable_with_internal_events?
-      new_page!
-
-      cli.say format_info("Excellent! Let's check that this tool will fit your needs.\n")
-      cli.say Text::EVENT_TRACKING_EXAMPLES
-
-      cli.yes?(
-        'Can usage for the feature be measured with a count of specific user actions or events? ' \
-          'Or counting a set of events?',
-        **yes_no_opts
-      )
-    end
-
-    def event_already_tracked?
-      new_page!
-
-      cli.say format_info("Super! Let's figure out if the event is already tracked & usable.\n")
-      cli.say Text::EVENT_EXISTENCE_CHECK_INSTRUCTIONS
-
-      cli.yes?('Is the event already tracked?', **yes_no_opts)
-    end
-
-    def use_case_error
-      new_page!
-
-      cli.error("Oh no! This probably isn't the tool you need!\n")
-      cli.say Text::ALTERNATE_RESOURCES_NOTICE
-      cli.say Text::FEEDBACK_NOTICE
-    end
-
-    def proceed_to_metric_definition
-      new_page!
-
-      cli.say format_info("Amazing! The next step is adding a new metric! (~8-15 min)\n")
-
-      return not_ready_error('New Metric') unless cli.yes?(format_prompt('Ready to start?'))
-
-      MetricDefiner.new(cli).run
-    end
-
-    def proceed_to_event_definition
-      new_page!
-
-      cli.say format_info("Okay! The next step is adding a new event! (~5-10 min)\n")
-
-      return not_ready_error('New Event') unless cli.yes?(format_prompt('Ready to start?'))
-
-      EventDefiner.new(cli).run
-    end
-
-    def not_ready_error(description)
-      cli.say "\nNo problem! When you're ready, run the CLI & select '#{description}'\n"
-      cli.say Text::FEEDBACK_NOTICE
-    end
-  end
-end
diff --git a/scripts/internal_events/cli/flows/event_definer.rb b/scripts/internal_events/cli/flows/event_definer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a5bae24f246ecef94b298468b4cdc562ace612d
--- /dev/null
+++ b/scripts/internal_events/cli/flows/event_definer.rb
@@ -0,0 +1,288 @@
+# frozen_string_literal: true
+
+require_relative '../helpers'
+
+# Entrypoint for flow to create an event definition file
+module InternalEventsCli
+  module Flows
+    class EventDefiner
+      include Helpers
+
+      SCHEMA = ::JSONSchemer.schema(Pathname('config/events/schema.json'))
+      STEPS = [
+        'New Event',
+        'Description',
+        'Name',
+        'Context',
+        'URL',
+        'Group',
+        'Tiers',
+        'Save files'
+      ].freeze
+
+      IDENTIFIER_OPTIONS = {
+        %w[project namespace user] =>
+          'Use case: For project-level user actions (ex - issue_assignee_changed) [MOST COMMON]',
+        %w[namespace user] =>
+          'Use case: For namespace-level user actions (ex - epic_assigned_to_milestone)',
+        %w[user] =>
+          'Use case: For user-only actions (ex - admin_impersonated_user)',
+        %w[project namespace] =>
+          'Use case: For project-level events without user interaction (ex - service_desk_request_received)',
+        %w[namespace] =>
+          'Use case: For namespace-level events without user interaction (ex - stale_runners_cleaned_up)',
+        %w[feature_enabled_by_namespace_ids user] =>
+          'Use case: For user actions attributable to multiple namespaces (ex - Code-Suggestions / Duo Pro)',
+        %w[] =>
+          'Use case: For instance-level events without user interaction [LEAST COMMON]'
+      }.freeze
+
+      IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.map { |k| k.join(', ') }.max_by(&:length)}]".length
+
+      attr_reader :cli, :event
+
+      def initialize(cli)
+        @cli = cli
+        @event = Event.new(milestone: MILESTONE)
+      end
+
+      def run
+        prompt_for_description
+        prompt_for_action
+        prompt_for_context
+        prompt_for_url
+        prompt_for_product_group
+        prompt_for_tier
+
+        outcome = create_event_file
+        display_result(outcome)
+
+        prompt_for_next_steps
+      end
+
+      private
+
+      def prompt_for_description
+        new_page!(1, 7, STEPS)
+        cli.say Text::EVENT_DESCRIPTION_INTRO
+
+        event.description = cli.ask("Describe what the event tracks: #{input_required_text}", **input_opts) do |q|
+          q.required true
+          q.modify :trim
+          q.messages[:required?] = Text::EVENT_DESCRIPTION_HELP
+        end
+      end
+
+      def prompt_for_action
+        new_page!(2, 7, STEPS)
+        cli.say Text::EVENT_ACTION_INTRO
+
+        event.action = cli.ask("Define the event name: #{input_required_text}", **input_opts) do |q|
+          q.required true
+          q.validate ->(input) { input =~ NAME_REGEX && cli.global.events.map(&:action).none?(input) }
+          q.modify :trim
+          q.messages[:valid?] = format_warning(
+            "Invalid event name. Only lowercase/numbers/underscores allowed. " \
+              "Ensure %{value} is not an existing event.")
+          q.messages[:required?] = Text::EVENT_ACTION_HELP
+        end
+      end
+
+      def prompt_for_context
+        new_page!(3, 7, STEPS)
+        cli.say format_prompt("EVENT CONTEXT #{counter(0, 2)}")
+        prompt_for_identifiers
+
+        new_page!(3, 7, STEPS) # Same "step" but increment counter
+        cli.say format_prompt("EVENT CONTEXT #{counter(1, 2)}")
+        prompt_for_additional_properties
+      end
+
+      def prompt_for_identifiers
+        cli.say Text::EVENT_IDENTIFIERS_INTRO % event.action
+
+        identifiers = prompt_for_array_selection(
+          'Which identifiers are available when the event occurs?',
+          IDENTIFIER_OPTIONS.keys,
+          per_page: IDENTIFIER_OPTIONS.length
+        ) { |choice| format_identifier_choice(choice) }
+
+        event.identifiers = identifiers if identifiers.any?
+      end
+
+      def format_identifier_choice(choice)
+        formatted_choice = choice.empty? ? 'None' : "[#{choice.sort.join(', ')}]"
+        buffer = IDENTIFIER_FORMATTING_BUFFER - formatted_choice.length
+
+        "#{formatted_choice}#{' ' * buffer} -- #{IDENTIFIER_OPTIONS[choice]}"
+      end
+
+      def prompt_for_additional_properties
+        cli.say Text::ADDITIONAL_PROPERTIES_INTRO
+
+        available_props = [:label, :property, :value, :add_extra_prop]
+
+        while available_props.any?
+          disabled = format_help('(already defined)')
+
+          # rubocop:disable Rails/NegateInclude -- this isn't Rails
+          options = [
+            { value: :none, name: 'None! Continue to next section!' },
+            disableable_option(
+              value: :label,
+              name: 'String 1 (attribute will be named `label`)',
+              disabled: disabled
+            ) { !available_props.include?(:label) },
+            disableable_option(
+              value: :property,
+              name: 'String 2 (attribute will be named `property`)',
+              disabled: disabled
+            ) { !available_props.include?(:property) },
+            disableable_option(
+              value: :value,
+              name: 'Number (attribute will be named `value`)',
+              disabled: disabled
+            ) { !available_props.include?(:value) },
+            disableable_option(
+              value: :add_extra_prop,
+              name: 'Add extra property (attribute will be named the input custom name)',
+              disabled: format_warning('(option disabled - use label/property/value first)')
+            ) do
+              !((!available_props.include?(:label) &&
+                  !available_props.include?(:property)) ||
+                  !available_props.include?(:value))
+            end
+          ]
+          # rubocop:enable Rails/NegateInclude
+
+          selected_property = cli.select(
+            "Which additional property do you want to add to the event?",
+            options,
+            help: format_help("(will reprompt for multiple)"),
+            **select_opts,
+            &disabled_format_callback
+          )
+
+          if selected_property == :none
+            available_props.clear
+          elsif selected_property == :add_extra_prop
+            property_name = prompt_for_add_extra_properties
+            property_description = prompt_for_text('Describe what the field will include:')
+            assign_extra_properties(property_name, property_description)
+          else
+            available_props.delete(selected_property)
+            property_description = prompt_for_text('Describe what the field will include:')
+            assign_extra_properties(selected_property, property_description)
+          end
+        end
+      end
+
+      def assign_extra_properties(property, description = nil)
+        event.additional_properties ||= {}
+        event.additional_properties[property.to_s] = {
+          'description' => description || 'TODO'
+        }
+      end
+
+      def prompt_for_add_extra_properties
+        primary_props = %w[label property value]
+
+        prompt_for_text('Define a name for the attribute:', **input_opts) do |q|
+          q.required true
+          q.validate ->(input) { input =~ NAME_REGEX && primary_props.none?(input) }
+          q.modify :trim
+          q.messages[:required?] = Text::ADDITIONAL_PROPERTIES_ADD_MORE_HELP
+          q.messages[:valid?] = format_warning(
+            "Invalid property name. Only lowercase/numbers/underscores allowed. " \
+              "Ensure %{value} is not one of `property, label, value`.")
+        end
+      end
+
+      def prompt_for_url
+        new_page!(4, 7, STEPS)
+
+        event.introduced_by_url = prompt_for_text('Which MR URL will merge the event definition?')
+      end
+
+      def prompt_for_product_group
+        new_page!(5, 7, STEPS)
+
+        product_group = prompt_for_group_ownership('Which group will own the event?')
+
+        event.product_group = product_group
+      end
+
+      def prompt_for_tier
+        new_page!(6, 7, STEPS)
+
+        event.tiers = prompt_for_array_selection(
+          'Which tiers will the event be recorded on?',
+          [%w[free premium ultimate], %w[premium ultimate], %w[ultimate]]
+        )
+
+        event.distributions = event.tiers.include?('free') ? %w[ce ee] : %w[ee]
+      end
+
+      def create_event_file
+        new_page!(7, 7, STEPS)
+
+        prompt_to_save_file(event.file_path, event.formatted_output)
+      end
+
+      def display_result(outcome)
+        new_page!
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_info('Done with event definition!')}
+
+          #{outcome || '  No files saved.'}
+
+          #{divider}
+
+            Do you need to create a metric? Probably!
+
+            Metrics are required to pull any usage data from self-managed instances or GitLab-Dedicated through Service Ping. Collected metric data can viewed in Tableau. Individual event details from GitLab.com can also be accessed through Snowflake.
+
+            Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake
+
+        TEXT
+      end
+
+      def prompt_for_next_steps
+        next_step = cli.select("How would you like to proceed?", **select_opts) do |menu|
+          menu.enum "."
+
+          menu.choice "New Event -- define another event", :new_event
+
+          choice = if File.exist?(event.file_path)
+                     ["Create Metric -- define a new metric using #{event.action}.yml", :add_metric]
+                   else
+                     ["Save & Create Metric -- save #{event.action}.yml and define a matching metric", :save_and_add]
+                   end
+
+          menu.default choice[0]
+          menu.choice(*choice)
+
+          menu.choice "View Usage -- look at code examples for #{event.action}.yml", :view_usage
+          menu.choice 'Exit', :exit
+        end
+
+        case next_step
+        when :new_event
+          EventDefiner.new(cli).run
+        when :add_metric
+          MetricDefiner.new(cli, event.file_path).run
+        when :save_and_add
+          write_to_file(event.file_path, event.formatted_output, 'create')
+
+          MetricDefiner.new(cli, event.file_path).run
+        when :view_usage
+          UsageViewer.new(cli, event.file_path, event).run
+        when :exit
+          cli.say Text::FEEDBACK_NOTICE
+        end
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/flows/flow_advisor.rb b/scripts/internal_events/cli/flows/flow_advisor.rb
new file mode 100755
index 0000000000000000000000000000000000000000..79435a08af5fa6baed27cd4a9cc4c0ded20a403f
--- /dev/null
+++ b/scripts/internal_events/cli/flows/flow_advisor.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require_relative '../helpers'
+
+# Entrypoint for help flow, which directs the user to the
+# correct flow or documentation based on their goal
+module InternalEventsCli
+  module Flows
+    class FlowAdvisor
+      include Helpers
+
+      attr_reader :cli
+
+      def initialize(cli)
+        @cli = cli
+      end
+
+      def run
+        return use_case_error unless goal_is_tracking_usage?
+        return use_case_error unless usage_trackable_with_internal_events?
+
+        event_already_tracked? ? proceed_to_metric_definition : proceed_to_event_definition
+      end
+
+      private
+
+      def goal_is_tracking_usage?
+        new_page!
+
+        cli.say format_info("First, let's check your objective.\n")
+
+        cli.yes?('Are you trying to track customer usage of a GitLab feature?', **yes_no_opts)
+      end
+
+      def usage_trackable_with_internal_events?
+        new_page!
+
+        cli.say format_info("Excellent! Let's check that this tool will fit your needs.\n")
+        cli.say Text::EVENT_TRACKING_EXAMPLES
+
+        cli.yes?(
+          'Can usage for the feature be measured with a count of specific user actions or events? ' \
+            'Or counting a set of events?',
+          **yes_no_opts
+        )
+      end
+
+      def event_already_tracked?
+        new_page!
+
+        cli.say format_info("Super! Let's figure out if the event is already tracked & usable.\n")
+        cli.say Text::EVENT_EXISTENCE_CHECK_INSTRUCTIONS
+
+        cli.yes?('Is the event already tracked?', **yes_no_opts)
+      end
+
+      def use_case_error
+        new_page!
+
+        cli.error("Oh no! This probably isn't the tool you need!\n")
+        cli.say Text::ALTERNATE_RESOURCES_NOTICE
+        cli.say Text::FEEDBACK_NOTICE
+      end
+
+      def proceed_to_metric_definition
+        new_page!
+
+        cli.say format_info("Amazing! The next step is adding a new metric! (~8-15 min)\n")
+
+        return not_ready_error('New Metric') unless cli.yes?(format_prompt('Ready to start?'))
+
+        MetricDefiner.new(cli).run
+      end
+
+      def proceed_to_event_definition
+        new_page!
+
+        cli.say format_info("Okay! The next step is adding a new event! (~5-10 min)\n")
+
+        return not_ready_error('New Event') unless cli.yes?(format_prompt('Ready to start?'))
+
+        EventDefiner.new(cli).run
+      end
+
+      def not_ready_error(description)
+        cli.say "\nNo problem! When you're ready, run the CLI & select '#{description}'\n"
+        cli.say Text::FEEDBACK_NOTICE
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/flows/metric_definer.rb b/scripts/internal_events/cli/flows/metric_definer.rb
new file mode 100755
index 0000000000000000000000000000000000000000..17ea14772d0b73060fcea8b0bfcc3263de007062
--- /dev/null
+++ b/scripts/internal_events/cli/flows/metric_definer.rb
@@ -0,0 +1,574 @@
+# frozen_string_literal: true
+
+require_relative '../helpers'
+require_relative '../text'
+
+# Entrypoint for flow to create an metric definition file
+module InternalEventsCli
+  module Flows
+    class MetricDefiner
+      include Helpers
+
+      SCHEMA = ::JSONSchemer.schema(Pathname('config/metrics/schema/base.json'))
+      STEPS = [
+        'New Metric',
+        'Type',
+        'Events',
+        'Scope',
+        'Descriptions',
+        'Copy event',
+        'Group',
+        'URL',
+        'Tiers',
+        'Save files'
+      ].freeze
+
+      NAME_REQUIREMENT_REASONS = {
+        filters: {
+          text: 'Metrics using filters are too complex for default naming.',
+          help: Text::METRIC_NAME_FILTER_HELP
+        },
+        length: {
+          text: 'The default filename will be too long.',
+          help: Text::METRIC_NAME_LENGTH_HELP
+        },
+        conflict: {
+          text: 'The default key path is already in use.',
+          help: Text::METRIC_NAME_CONFLICT_HELP
+        }
+      }.freeze
+
+      attr_reader :cli
+
+      def initialize(cli, starting_event = nil)
+        @cli = cli
+        @selected_event_paths = Array(starting_event)
+        @metrics = []
+        @selected_filters = {}
+      end
+
+      def run
+        type = prompt_for_metric_type
+        prompt_for_events(type)
+
+        return unless @selected_event_paths.any?
+
+        prompt_for_metrics
+        prompt_for_event_filters
+
+        return unless @metrics.any?
+
+        prompt_for_descriptions
+        defaults = prompt_for_copying_event_properties
+        prompt_for_product_group(defaults)
+        prompt_for_url(defaults)
+        prompt_for_tier(defaults)
+        outcomes = create_metric_files
+        prompt_for_next_steps(outcomes)
+      end
+
+      private
+
+      # ----- Memoization Helpers -----------------
+
+      def events
+        @events ||= events_by_filepath(@selected_event_paths)
+      end
+
+      def selected_events
+        @selected_events ||= events.values_at(*@selected_event_paths)
+      end
+
+      # ----- Prompts -----------------------------
+
+      def prompt_for_metric_type
+        return if @selected_event_paths.any?
+
+        new_page!(1, 9, STEPS)
+
+        cli.select("Which best describes what the metric should track?", **select_opts) do |menu|
+          menu.enum "."
+
+          menu.choice 'Single event    -- count occurrences of a specific event or user interaction',
+            :event_metric
+          menu.choice 'Multiple events -- count occurrences of several separate events or interactions',
+            :aggregate_metric
+          menu.choice 'Database        -- record value of a particular field or count of database rows',
+            :database_metric
+        end
+      end
+
+      def prompt_for_events(type)
+        return if @selected_event_paths.any?
+
+        new_page!(2, 9, STEPS)
+
+        case type
+        when :event_metric
+          cli.say "For robust event search, use the Metrics Dictionary: https://metrics.gitlab.com/snowplow\n\n"
+
+          @selected_event_paths = [cli.select(
+            'Which event does this metric track?',
+            get_event_options(events),
+            **select_opts,
+            **filter_opts(header_size: 7)
+          )]
+        when :aggregate_metric
+          cli.say "For robust event search, use the Metrics Dictionary: https://metrics.gitlab.com/snowplow\n\n"
+
+          @selected_event_paths = cli.multi_select(
+            'Which events does this metric track? (Space to select)',
+            get_event_options(events),
+            **multiselect_opts,
+            **filter_opts(header_size: 7)
+          )
+        when :database_metric
+          cli.error Text::DATABASE_METRIC_NOTICE
+          cli.say Text::FEEDBACK_NOTICE
+        end
+      end
+
+      def prompt_for_metrics
+        eligible_metrics = get_metric_options(selected_events)
+
+        if eligible_metrics.all? { |metric| metric[:disabled] }
+          cli.error Text::ALL_METRICS_EXIST_NOTICE
+          cli.say Text::FEEDBACK_NOTICE
+
+          return
+        end
+
+        new_page!(3, 9, STEPS)
+        cli.say format_info('SELECTED EVENTS')
+        cli.say selected_events_filter_options.join
+        cli.say "\n"
+
+        @metrics = cli.select(
+          'Which metrics do you want to add?',
+          eligible_metrics,
+          **select_opts,
+          **filter_opts,
+          per_page: 20,
+          &disabled_format_callback
+        )
+
+        assign_shared_attrs(:actions, :milestone) do
+          {
+            actions: selected_events.map(&:action).sort,
+            milestone: MILESTONE
+          }
+        end
+      end
+
+      def prompt_for_event_filters
+        return if @metrics.none?(&:filters_expected?)
+
+        selected_unique_identifier = @metrics.first.identifier.value
+        event_count = selected_events.length
+        previous_inputs = {
+          'label' => nil,
+          'property' => nil,
+          'value' => nil
+        }
+
+        event_filters = selected_events.dup.flat_map.with_index do |event, idx|
+          print_event_filter_header(event, idx, event_count)
+
+          next if deselect_nonfilterable_event?(event) # prompts user
+
+          filter_values = event.additional_properties&.filter_map do |property, _|
+            next if selected_unique_identifier == property
+
+            prompt_for_property_filter(
+              event.action,
+              property,
+              previous_inputs[property]
+            )
+          end
+
+          previous_inputs.merge!(@selected_filters[event.action] || {})
+
+          find_filter_permutations(event.action, filter_values)
+        end.compact
+
+        bulk_assign(filters: event_filters)
+      end
+
+      def prompt_for_descriptions
+        default_description = nil
+        default_key = nil
+
+        separate_page_per_metric = @metrics.any? do |metric|
+          name_requirement_reason(metric)
+        end
+
+        @metrics.each_with_index do |metric, idx|
+          if idx == 0 || separate_page_per_metric
+            new_page!(4, 9, STEPS)
+
+            cli.say Text::METRIC_DESCRIPTION_INTRO
+            cli.say selected_event_descriptions.join
+          end
+
+          cli.say "\n"
+          cli.say format_prompt(format_subheader(
+            'DESCRIBING METRIC',
+            metric.technical_description,
+            idx,
+            @metrics.length
+          ))
+
+          prompt_for_description(metric, default_description).tap do |description|
+            default_description = description
+            metric.description = "#{metric.description_prefix} #{description}"
+          end
+
+          prompt_for_metric_name(metric, default_key)&.tap do |key|
+            default_key = key
+            metric.key = key
+          end
+        end
+      end
+
+      def file_saved_context_message(attributes)
+        format_prefix "  ", <<~TEXT.chomp
+          - Visit #{format_info('https://metrics.gitlab.com')} to find dashboard links for this metric
+          - Metric trend dashboard: #{format_info(metric_trend_path(attributes['key_path']))}
+        TEXT
+      end
+
+      # Check existing event files for attributes to copy over
+      def prompt_for_copying_event_properties
+        shared_values = collect_values_for_shared_event_properties
+        defaults = shared_values.except(:stage, :section)
+
+        return {} if shared_values.none?
+
+        return shared_values if defaults.none?
+
+        new_page!(5, 9, STEPS)
+
+        cli.say <<~TEXT
+          #{format_info('Convenient! We can copy these attributes from the event definition(s):')}
+
+          #{defaults.compact.transform_keys(&:to_s).to_yaml(line_width: 150)}
+          #{format_info('If any of these attributes are incorrect, you can also change them manually from your text editor later.')}
+
+        TEXT
+
+        cli.select('What would you like to do?', **select_opts) do |menu|
+          menu.enum '.'
+          menu.choice 'Copy & continue', -> { bulk_assign(defaults) }
+          menu.choice 'Modify attributes'
+        end
+
+        shared_values
+      end
+
+      def prompt_for_product_group(defaults)
+        assign_shared_attr(:product_group) do
+          new_page!(6, 9, STEPS)
+
+          prompt_for_group_ownership('Which group owns the metric?', defaults)
+        end
+      end
+
+      def prompt_for_url(defaults)
+        assign_shared_attr(:introduced_by_url) do
+          new_page!(7, 9, STEPS)
+
+          prompt_for_text(
+            'Which MR URL introduced the metric?',
+            defaults[:introduced_by_url]
+          )
+        end
+      end
+
+      def prompt_for_tier(defaults)
+        assign_shared_attr(:tier) do
+          new_page!(8, 9, STEPS)
+
+          prompt_for_array_selection(
+            'Which tiers will the metric be reported from?',
+            [%w[free premium ultimate], %w[premium ultimate], %w[ultimate]],
+            defaults[:tier]
+          )
+        end
+
+        assign_shared_attr(:tiers) { |metric| [*metric.tier] }
+
+        assign_shared_attr(:distribution) do |metric|
+          metric.tier.include?('free') ? %w[ce ee] : %w[ee]
+        end
+      end
+
+      def create_metric_files
+        @metrics.map.with_index do |metric, idx|
+          new_page!(9, 9, STEPS) # Repeat the same step number but increment metric counter
+
+          cli.say format_prompt(format_subheader('SAVING FILE', metric.description, idx, @metrics.length))
+          cli.say "\n"
+
+          prompt_to_save_file(metric.file_path, metric.formatted_output)
+        end
+      end
+
+      def prompt_for_next_steps(outcomes = [])
+        new_page!
+
+        outcome = outcomes.any? ? outcomes.compact.join("\n") : '  No files saved.'
+        metric = @metrics.first
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_info('Done with metric definitions!')}
+
+          #{outcome}
+          #{divider}
+
+            Have you instrumented the application code to trigger the event yet? View usage examples to easily copy/paste implementation!
+
+            Want to verify the metrics? Check out the group::#{metric[:product_group]} Metrics Exploration Dashboard in Tableau
+              Note: The Metrics Exploration Dashboard data would be available ~1 week after deploy for Gitlab.com, ~1 week after next release for self-managed
+              Link: #{format_info(metric_exploration_group_path(metric[:product_group], find_stage(metric.product_group)))}
+
+            Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake
+
+        TEXT
+
+        actions = selected_events.map(&:action).join(', ')
+        next_step = cli.select("How would you like to proceed?", **select_opts) do |menu|
+          menu.enum "."
+
+          menu.choice "New Event -- define a new event", :new_event
+          menu.choice "New Metric -- define another metric for #{actions}", :new_metric_with_events
+          menu.choice "New Metric -- define another metric", :new_metric
+          choice = "View Usage -- look at code examples for event #{selected_events.first.action}"
+          menu.default choice
+          menu.choice choice, :view_usage
+          menu.choice 'Exit', :exit
+        end
+
+        case next_step
+        when :new_event
+          EventDefiner.new(cli).run
+        when :new_metric_with_events
+          MetricDefiner.new(cli, @selected_event_paths).run
+        when :new_metric
+          MetricDefiner.new(cli).run
+        when :view_usage
+          UsageViewer.new(cli, @selected_event_paths.first, selected_events.first).run
+        when :exit
+          cli.say Text::FEEDBACK_NOTICE
+        end
+      end
+
+      # ----- Prompt-specific Helpers -------------
+
+      # Helper for #prompt_for_metrics
+      def selected_events_filter_options
+        filterable_events_selected = selected_events.any? { |event| event.additional_properties&.any? }
+
+        selected_events.map do |event|
+          filters = event.additional_properties&.keys
+          filter_phrase = if filters
+                            " (filterable by #{filters&.join(', ')})"
+                          elsif filterable_events_selected
+                            ' -- not filterable'
+                          end
+
+          "  - #{event.action}#{format_help(filter_phrase)}\n"
+        end
+      end
+
+      # Helper for #prompt_for_event_filters
+      def print_event_filter_header(event, idx, total)
+        cli.say "\n"
+        cli.say format_info(format_subheader('SETTING EVENT FILTERS', event.action, idx, total))
+
+        return unless event.additional_properties&.any?
+
+        event_filter_options = event.additional_properties.map do |property, attrs|
+          "  #{property}: #{attrs['description']}\n"
+        end
+
+        cli.say event_filter_options.join
+      end
+
+      # Helper for #prompt_for_event_filters
+      def deselect_nonfilterable_event?(event)
+        cli.say "\n"
+
+        return false if event.additional_properties&.any?
+        return false if cli.yes?("This event is not filterable. Should it be included in the metric?", **yes_no_opts)
+
+        selected_events.delete(event)
+        bulk_assign(actions: selected_events.map(&:action).sort)
+
+        true
+      end
+
+      # Helper for #prompt_for_event_filters
+      def prompt_for_property_filter(action, property, default)
+        formatted_prop = format_info(property)
+        prompt = "Count where #{formatted_prop} equals any of (comma-sep):"
+
+        inputs = prompt_for_text(prompt, default, **input_opts) do |q|
+          if property == 'value'
+            q.convert ->(input) { input.split(',').map(&:to_i).uniq }
+            q.validate %r{^(\d|\s|,)*$}
+            q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric"
+          else
+            q.convert ->(input) { input.split(',').map(&:strip).uniq }
+          end
+        end
+
+        return unless inputs&.any?
+
+        @selected_filters[action] ||= {}
+        @selected_filters[action][property] = inputs.join(',')
+
+        inputs.map { |input| { property => input } }.uniq
+      end
+
+      # Helper for #prompt_for_event_filters
+      #
+      # Gets all the permutations of the provided property values.
+      # @param filters [Array] ex) [{ 'label' => 'red' }, { 'label' => 'blue' }, { value => 16 }]
+      # @return ex) [{ 'label' => 'red', value => 16 }, { 'label' => 'blue', value => 16 }]
+      def find_filter_permutations(action, filters)
+        # Define a filter for all events, regardless of the available props so NewMetric#events is correct
+        return [[action, {}]] unless filters&.any?
+
+        # Uses proc syntax to avoid spliting & type-checking `filters`
+        :product.to_proc.call(*filters).map do |filter|
+          [action, filter.reduce(&:merge)]
+        end
+      end
+
+      # Helper for #prompt_for_descriptions
+      def selected_event_descriptions
+        selected_events.map do |event|
+          filters = @selected_filters[event.action]
+
+          if filters&.any?
+            filter_phrase = filters.map { |k, v| "#{k}=#{v}" }.join(' ')
+            filter_phrase = format_help("(#{filter_phrase})")
+          end
+
+          "  #{event.action}#{filter_phrase} - #{format_selection(event.description)}\n"
+        end
+      end
+
+      # Helper for #prompt_for_descriptions
+      def prompt_for_description(metric, default)
+        description_start = format_info("#{metric.description_prefix}...")
+
+        cli.say <<~TEXT
+
+          #{input_opts[:prefix]} How would you describe this metric to a non-technical person? #{input_required_text}
+
+        TEXT
+
+        prompt_for_text("  Finish the description: #{description_start}", default, multiline: true) do |q|
+          q.required true
+          q.modify :trim
+          q.messages[:required?] = Text::METRIC_DESCRIPTION_HELP
+        end
+      end
+
+      # Helper for #prompt_for_descriptions
+      def prompt_for_metric_name(metric, default)
+        name_reason = name_requirement_reason(metric)
+        default_name = metric.key.value
+        display_name = metric.key.value("\e[0m[REPLACE ME]\e[36m")
+        empty_name = metric.key.value('')
+
+        return unless name_reason
+
+        cli.say <<~TEXT
+
+          #{input_opts[:prefix]} #{name_reason[:text]} How should we refererence this metric? #{input_required_text}
+
+                      ID:  #{format_info(display_name)}
+                Filename:  #{format_info(display_name)}#{format_info('.yml')}
+
+        TEXT
+
+        max_length = MAX_FILENAME_LENGTH - "#{empty_name}.yml".length
+        help_tokens = { name: default_name, count: max_length }
+
+        prompt_for_text('  Replace with: ', default, multiline: true) do |q|
+          q.required true
+          q.messages[:required?] = name_reason[:help] % help_tokens
+          q.messages[:valid?] = Text::METRIC_NAME_ERROR % help_tokens
+          q.validate ->(input) do
+            input.length <= max_length &&
+              input.match?(NAME_REGEX) &&
+              !conflicting_key_path?(metric.key.value(input))
+          end
+        end
+      end
+
+      # Helper for #prompt_for_descriptions
+      def name_requirement_reason(metric)
+        if metric.filters.assigned?
+          NAME_REQUIREMENT_REASONS[:filters]
+        elsif metric.file_name.length > MAX_FILENAME_LENGTH
+          NAME_REQUIREMENT_REASONS[:length]
+        elsif conflicting_key_path?(metric.key_path)
+          NAME_REQUIREMENT_REASONS[:conflict]
+        end
+      end
+
+      # Helper for #prompt_for_descriptions
+      def conflicting_key_path?(key_path)
+        cli.global.metrics.any? do |existing_metric|
+          existing_metric.key_path == key_path
+        end
+      end
+
+      # Helper for #prompt_for_copying_event_properties
+      def collect_values_for_shared_event_properties
+        fields = Hash.new { |h, k| h[k] = [] }
+
+        selected_events.each do |event|
+          fields[:introduced_by_url] << event.introduced_by_url
+          fields[:product_group] << event.product_group
+          fields[:stage] << find_stage(event.product_group)
+          fields[:section] << find_section(event.product_group)
+          fields[:distribution] << event.distributions&.sort
+          fields[:tier] << event.tiers&.sort
+          fields[:tiers] << event.tiers&.sort
+        end
+
+        # Keep event values if every selected event is the same
+        fields.each_with_object({}) do |(attr, values), defaults|
+          next unless values.compact.uniq.length == 1
+
+          defaults[attr] ||= values.first
+        end
+      end
+
+      # ----- Shared Helpers ----------------------
+
+      def assign_shared_attrs(...)
+        metric = @metrics.first
+        attrs = metric.to_h.slice(...)
+        attrs = yield(metric) unless attrs.values.all?
+
+        bulk_assign(attrs)
+      end
+
+      def assign_shared_attr(key)
+        assign_shared_attrs(key) do |metric|
+          { key => yield(metric) }
+        end
+      end
+
+      def bulk_assign(attrs)
+        @metrics.each { |metric| metric.bulk_assign(attrs) }
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/flows/usage_viewer.rb b/scripts/internal_events/cli/flows/usage_viewer.rb
new file mode 100755
index 0000000000000000000000000000000000000000..1b84769a51555de56ddd91f4a8e22a27ad6caa07
--- /dev/null
+++ b/scripts/internal_events/cli/flows/usage_viewer.rb
@@ -0,0 +1,438 @@
+# frozen_string_literal: true
+
+require_relative '../helpers'
+
+# Entrypoint for flow to print examples of how to trigger an
+# event in different languages & different methods of testing
+module InternalEventsCli
+  module Flows
+    class UsageViewer
+      include Helpers
+
+      PROPERTY_EXAMPLES = {
+        'label' => "'string'",
+        'property' => "'string'",
+        'value' => '72',
+        'custom_key' => 'custom_value'
+      }.freeze
+
+      attr_reader :cli, :event
+
+      def initialize(cli, event_path = nil, event = nil)
+        @cli = cli
+        @event = event
+        @selected_event_path = event_path
+      end
+
+      def run
+        prompt_for_eligible_event
+        prompt_for_usage_location
+      end
+
+      def prompt_for_eligible_event
+        return if event
+
+        event_details = events_by_filepath
+
+        @selected_event_path = cli.select(
+          "Show examples for which event?",
+          get_event_options(event_details),
+          **select_opts,
+          **filter_opts
+        )
+
+        @event = event_details[@selected_event_path]
+      end
+
+      def prompt_for_usage_location(default = '1. ruby/rails')
+        choices = [
+          { name: '1. ruby/rails', value: :rails },
+          { name: '2. rspec', value: :rspec },
+          { name: '3. javascript (vue)', value: :vue },
+          { name: '4. javascript (plain)', value: :js },
+          { name: '5. vue template', value: :vue_template },
+          { name: '6. haml', value: :haml },
+          { name: '7. Manual testing in GDK', value: :gdk },
+          { name: '8. Data verification in Tableau', value: :tableau },
+          { name: '9. View examples for a different event', value: :other_event },
+          { name: '10. Exit', value: :exit }
+        ]
+
+        usage_location = cli.select(
+          'Select a use-case to view examples for:',
+          choices,
+          **select_opts,
+          **filter_opts,
+          per_page: 10
+        ) do |menu|
+          menu.default default
+        end
+
+        case usage_location
+        when :rails
+          rails_examples
+          prompt_for_usage_location('1. ruby/rails')
+        when :rspec
+          rspec_examples
+          prompt_for_usage_location('2. rspec')
+        when :haml
+          haml_examples
+          prompt_for_usage_location('6. haml')
+        when :js
+          js_examples
+          prompt_for_usage_location('4. javascript (plain)')
+        when :vue
+          vue_examples
+          prompt_for_usage_location('3. javascript (vue)')
+        when :vue_template
+          vue_template_examples
+          prompt_for_usage_location('5. vue template')
+        when :gdk
+          gdk_examples
+          prompt_for_usage_location('7. Manual testing in GDK')
+        when :tableau
+          service_ping_dashboard_examples
+          prompt_for_usage_location('8. Data verification in Tableau')
+        when :other_event
+          self.class.new(cli).run
+        when :exit
+          cli.say(Text::FEEDBACK_NOTICE)
+        end
+      end
+
+      def rails_examples
+        identifier_args = identifiers.map do |identifier|
+          "  #{identifier}: #{identifier}"
+        end
+
+        property_args = format_additional_properties do |property, value, description|
+          "    #{property}: #{value}, # #{description}"
+        end
+
+        if property_args.any?
+          # remove trailing comma after last arg but keep any other commas
+          property_args.last.sub!(',', '')
+          property_arg = "  additional_properties: {\n#{property_args.join("\n")}\n  }"
+        end
+
+        args = ["'#{action}'", *identifier_args, property_arg].compact.join(",\n")
+        args = "\n  #{args}\n" if args.lines.count > 1
+
+        cli.say format_warning <<~TEXT
+          #{divider}
+          #{format_help('# RAILS')}
+
+          include Gitlab::InternalEventsTracking
+
+          track_internal_event(#{args})
+
+          #{divider}
+        TEXT
+      end
+
+      def rspec_examples
+        identifier_args = identifiers.map do |identifier|
+          "  let(:#{identifier}) { create(:#{identifier}) }\n"
+        end.join('')
+
+        property_args = format_additional_properties do |property, value|
+          "    #{property}: #{value}"
+        end
+
+        if property_args.any?
+          property_arg = format_prefix '  ', <<~TEXT
+            let(:additional_properties) do
+              {
+            #{property_args.join(",\n")}
+              }
+            end
+          TEXT
+        end
+
+        args = [*identifier_args, *property_arg].join('')
+
+        cli.say format_warning <<~TEXT
+          #{divider}
+          #{format_help('# RSPEC')}
+
+          it_behaves_like 'internal event tracking' do
+            let(:event) { '#{action}' }
+          #{args}end
+
+          #{divider}
+        TEXT
+      end
+
+      def haml_examples
+        property_args = format_additional_properties do |property, value, _|
+          "event_#{property}: #{value}"
+        end
+
+        args = ["event_tracking: '#{action}'", *property_args].join(', ')
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_help('# HAML -- ON-CLICK')}
+
+          .inline-block{ #{format_warning("data: { #{args} }")} }
+            = _('Important Text')
+
+          #{divider}
+          #{format_help('# HAML -- COMPONENT ON-CLICK')}
+
+          = render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { #{args} }")} })
+
+          #{divider}
+          #{format_help('# HAML -- COMPONENT ON-LOAD')}
+
+          = render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { event_tracking_load: true, #{args} }")} })
+
+          #{divider}
+        TEXT
+
+        cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
+      end
+
+      def vue_template_examples
+        on_click_args = template_formatted_args('data-event-tracking', indent: 2)
+        on_load_args = template_formatted_args('data-event-tracking-load', indent: 2)
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_help('// VUE TEMPLATE -- ON-CLICK')}
+
+          <script>
+          import { GlButton } from '@gitlab/ui';
+
+          export default {
+            components: { GlButton }
+          };
+          </script>
+
+          <template>
+            <gl-button#{on_click_args}
+              Click Me
+            </gl-button>
+          </template>
+
+          #{divider}
+          #{format_help('// VUE TEMPLATE -- ON-LOAD')}
+
+          <script>
+          import { GlButton } from '@gitlab/ui';
+
+          export default {
+            components: { GlButton }
+          };
+          </script>
+
+          <template>
+            <gl-button#{on_load_args}
+              Click Me
+            </gl-button>
+          </template>
+
+          #{divider}
+        TEXT
+
+        cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
+      end
+
+      def js_examples
+        args = js_formatted_args(indent: 2)
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_help('// FRONTEND -- RAW JAVASCRIPT')}
+
+          #{format_warning("import { InternalEvents } from '~/tracking';")}
+
+          export const performAction = () => {
+            #{format_warning("InternalEvents.trackEvent#{args}")}
+
+            return true;
+          };
+
+          #{divider}
+        TEXT
+
+        # https://docs.snowplow.io/docs/understanding-your-pipeline/schemas/
+        cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
+      end
+
+      def vue_examples
+        args = js_formatted_args(indent: 6)
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_help('// VUE')}
+
+          <script>
+          #{format_warning("import { InternalEvents } from '~/tracking';")}
+          import { GlButton } from '@gitlab/ui';
+
+          #{format_warning('const trackingMixin = InternalEvents.mixin();')}
+
+          export default {
+            #{format_warning('mixins: [trackingMixin]')},
+            components: { GlButton },
+            methods: {
+              performAction() {
+                #{format_warning("this.trackEvent#{args}")}
+              },
+            },
+          };
+          </script>
+
+          <template>
+            <gl-button @click=performAction>Click Me</gl-button>
+          </template>
+
+          #{divider}
+        TEXT
+
+        cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
+      end
+
+      private
+
+      def action
+        event['action']
+      end
+
+      def identifiers
+        Array(event['identifiers']).tap do |ids|
+          # We always auto assign namespace if project is provided
+          ids.delete('namespace') if ids.include?('project')
+        end
+      end
+
+      def additional_properties
+        Array(event['additional_properties'])
+      end
+
+      def format_additional_properties
+        additional_properties.map do |property, details|
+          example_value = PROPERTY_EXAMPLES[property]
+          description = details['description'] || 'TODO'
+
+          yield(property, example_value, description)
+        end
+      end
+
+      def js_formatted_args(indent:)
+        return "('#{action}');" if additional_properties.none?
+
+        property_args = format_additional_properties do |property, value, description|
+          "    #{property}: #{value}, // #{description}"
+        end
+
+        [
+          '(',
+          "  '#{action}',",
+          '  {',
+          *property_args,
+          '  },',
+          ');'
+        ].join("\n#{' ' * indent}")
+      end
+
+      def service_ping_metrics_info
+        product_group = related_metrics.map(&:product_group).uniq
+
+        <<~TEXT
+          #{product_group.map { |group| "#{group}: #{format_info(metric_exploration_group_path(group, find_stage(group)))}" }.join("\n")}
+
+          #{divider}
+          #{format_help("# METRIC TRENDS -- view data for a service ping metric for #{event.action}")}
+
+          #{related_metrics.map { |metric| "#{metric.key_path}: #{format_info(metric_trend_path(metric.key_path))}" }.join("\n")}
+        TEXT
+      end
+
+      def service_ping_no_metric_info
+        <<~TEXT
+          #{format_help("# Warning: There are no metrics for #{event.action} yet.")}
+          #{event.product_group}: #{format_info(metric_exploration_group_path(event.product_group, find_stage(event.product_group)))}
+        TEXT
+      end
+
+      def template_formatted_args(data_attr, indent:)
+        return " #{data_attr}=\"#{action}\">" if additional_properties.none?
+
+        spacer = ' ' * indent
+        property_args = format_additional_properties do |property, value, _|
+          "  data-event-#{property}=#{value.tr("'", '"')}"
+        end
+
+        args = [
+          '', # start args on next line
+          "  #{data_attr}=\"#{action}\"",
+          *property_args
+        ].join("\n#{spacer}")
+
+        "#{format_warning(args)}\n#{spacer}>"
+      end
+
+      def related_metrics
+        cli.global.metrics.select { |metric| metric.actions&.include?(event.action) }
+      end
+
+      def service_ping_dashboard_examples
+        cli.say <<~TEXT
+          #{divider}
+          #{format_help('# GROUP DASHBOARDS -- view all service ping metrics for a specific group')}
+
+          #{related_metrics.any? ? service_ping_metrics_info : service_ping_no_metric_info}
+          #{divider}
+          Note: The metric dashboard links can also be accessed from #{format_info('https://metrics.gitlab.com/')}
+
+          Not what you're looking for? Check this doc:
+            - #{format_info('https://docs.gitlab.com/ee/development/internal_analytics/#data-discovery')}
+
+        TEXT
+      end
+
+      def gdk_examples
+        key_paths = related_metrics.map(&:key_path)
+
+        cli.say <<~TEXT
+          #{divider}
+          #{format_help('# TERMINAL -- monitor events & changes to service ping metrics as they occur')}
+
+          1. From `gitlab/` directory, run the monitor script:
+
+          #{format_warning("bin/rails runner scripts/internal_events/monitor.rb #{event.action}")}
+
+          2. View metric updates within the terminal
+
+          3. [Optional] Configure gdk with snowplow micro to see individual events: https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/snowplow_micro.md
+
+          #{divider}
+          #{format_help('# RAILS CONSOLE -- generate service ping payload, including most recent usage data')}
+
+          #{format_warning("require_relative 'spec/support/helpers/service_ping_helpers.rb'")}
+
+          #{format_help('# Get current value of a metric')}
+          #{
+            if key_paths.any?
+              key_paths.map { |key_path| format_warning("ServicePingHelpers.get_current_usage_metric_value('#{key_path}')") }.join("\n")
+            else
+              format_help("# Warning: There are no metrics for #{event.action} yet. When there are, replace <key_path> below.\n") +
+              format_warning('ServicePingHelpers.get_current_usage_metric_value(<key_path>)')
+            end
+          }
+
+          #{format_help('# View entire service ping payload')}
+          #{format_warning('ServicePingHelpers.get_current_service_ping_payload')}
+          #{divider}
+          Need to test something else? Check these docs:
+          - https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.html
+          - https://docs.gitlab.com/ee/development/internal_analytics/service_ping/troubleshooting.html
+          - https://docs.gitlab.com/ee/development/internal_analytics/review_guidelines.html
+
+        TEXT
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/metric_definer.rb b/scripts/internal_events/cli/metric_definer.rb
deleted file mode 100755
index f8e9d3f090be07b8fd00a8dd4add4a93d3b78428..0000000000000000000000000000000000000000
--- a/scripts/internal_events/cli/metric_definer.rb
+++ /dev/null
@@ -1,568 +0,0 @@
-# frozen_string_literal: true
-
-require_relative './helpers'
-require_relative './text'
-
-module InternalEventsCli
-  class MetricDefiner
-    include Helpers
-
-    SCHEMA = ::JSONSchemer.schema(Pathname('config/metrics/schema/base.json'))
-    STEPS = [
-      'New Metric',
-      'Type',
-      'Events',
-      'Scope',
-      'Descriptions',
-      'Copy event',
-      'Group',
-      'URL',
-      'Tiers',
-      'Save files'
-    ].freeze
-
-    NAME_REQUIREMENT_REASONS = {
-      filters: {
-        text: 'Metrics using filters are too complex for default naming.',
-        help: Text::METRIC_NAME_FILTER_HELP
-      },
-      length: {
-        text: 'The default filename will be too long.',
-        help: Text::METRIC_NAME_LENGTH_HELP
-      },
-      conflict: {
-        text: 'The default key path is already in use.',
-        help: Text::METRIC_NAME_CONFLICT_HELP
-      }
-    }.freeze
-
-    attr_reader :cli
-
-    def initialize(cli, starting_event = nil)
-      @cli = cli
-      @selected_event_paths = Array(starting_event)
-      @metrics = []
-      @selected_filters = {}
-    end
-
-    def run
-      type = prompt_for_metric_type
-      prompt_for_events(type)
-
-      return unless @selected_event_paths.any?
-
-      prompt_for_metrics
-      prompt_for_event_filters
-
-      return unless @metrics.any?
-
-      prompt_for_descriptions
-      defaults = prompt_for_copying_event_properties
-      prompt_for_product_group(defaults)
-      prompt_for_url(defaults)
-      prompt_for_tier(defaults)
-      outcomes = create_metric_files
-      prompt_for_next_steps(outcomes)
-    end
-
-    private
-
-    # ----- Memoization Helpers -----------------
-
-    def events
-      @events ||= events_by_filepath(@selected_event_paths)
-    end
-
-    def selected_events
-      @selected_events ||= events.values_at(*@selected_event_paths)
-    end
-
-    # ----- Prompts -----------------------------
-
-    def prompt_for_metric_type
-      return if @selected_event_paths.any?
-
-      new_page!(1, 9, STEPS)
-
-      cli.select("Which best describes what the metric should track?", **select_opts) do |menu|
-        menu.enum "."
-
-        menu.choice 'Single event    -- count occurrences of a specific event or user interaction', :event_metric
-        menu.choice 'Multiple events -- count occurrences of several separate events or interactions', :aggregate_metric
-        menu.choice 'Database        -- record value of a particular field or count of database rows', :database_metric
-      end
-    end
-
-    def prompt_for_events(type)
-      return if @selected_event_paths.any?
-
-      new_page!(2, 9, STEPS)
-
-      case type
-      when :event_metric
-        cli.say "For robust event search, use the Metrics Dictionary: https://metrics.gitlab.com/snowplow\n\n"
-
-        @selected_event_paths = [cli.select(
-          'Which event does this metric track?',
-          get_event_options(events),
-          **select_opts,
-          **filter_opts(header_size: 7)
-        )]
-      when :aggregate_metric
-        cli.say "For robust event search, use the Metrics Dictionary: https://metrics.gitlab.com/snowplow\n\n"
-
-        @selected_event_paths = cli.multi_select(
-          'Which events does this metric track? (Space to select)',
-          get_event_options(events),
-          **multiselect_opts,
-          **filter_opts(header_size: 7)
-        )
-      when :database_metric
-        cli.error Text::DATABASE_METRIC_NOTICE
-        cli.say Text::FEEDBACK_NOTICE
-      end
-    end
-
-    def prompt_for_metrics
-      eligible_metrics = get_metric_options(selected_events)
-
-      if eligible_metrics.all? { |metric| metric[:disabled] }
-        cli.error Text::ALL_METRICS_EXIST_NOTICE
-        cli.say Text::FEEDBACK_NOTICE
-
-        return
-      end
-
-      new_page!(3, 9, STEPS)
-      cli.say format_info('SELECTED EVENTS')
-      cli.say selected_events_filter_options.join
-      cli.say "\n"
-
-      @metrics = cli.select(
-        'Which metrics do you want to add?',
-        eligible_metrics,
-        **select_opts,
-        **filter_opts,
-        per_page: 20,
-        &disabled_format_callback
-      )
-
-      assign_shared_attrs(:actions, :milestone) do
-        {
-          actions: selected_events.map(&:action).sort,
-          milestone: MILESTONE
-        }
-      end
-    end
-
-    def prompt_for_event_filters
-      return if @metrics.none?(&:filters_expected?)
-
-      selected_unique_identifier = @metrics.first.identifier.value
-      event_count = selected_events.length
-      previous_inputs = {
-        'label' => nil,
-        'property' => nil,
-        'value' => nil
-      }
-
-      event_filters = selected_events.dup.flat_map.with_index do |event, idx|
-        print_event_filter_header(event, idx, event_count)
-
-        next if deselect_nonfilterable_event?(event) # prompts user
-
-        filter_values = event.additional_properties&.filter_map do |property, _|
-          next if selected_unique_identifier == property
-
-          prompt_for_property_filter(
-            event.action,
-            property,
-            previous_inputs[property]
-          )
-        end
-
-        previous_inputs.merge!(@selected_filters[event.action] || {})
-
-        find_filter_permutations(event.action, filter_values)
-      end.compact
-
-      bulk_assign(filters: event_filters)
-    end
-
-    def prompt_for_descriptions
-      default_description = nil
-      default_key = nil
-
-      separate_page_per_metric = @metrics.any? do |metric|
-        name_requirement_reason(metric)
-      end
-
-      @metrics.each_with_index do |metric, idx|
-        if idx == 0 || separate_page_per_metric
-          new_page!(4, 9, STEPS)
-
-          cli.say Text::METRIC_DESCRIPTION_INTRO
-          cli.say selected_event_descriptions.join
-        end
-
-        cli.say "\n"
-        cli.say format_prompt(format_subheader(
-          'DESCRIBING METRIC',
-          metric.technical_description,
-          idx,
-          @metrics.length
-        ))
-
-        prompt_for_description(metric, default_description).tap do |description|
-          default_description = description
-          metric.description = "#{metric.description_prefix} #{description}"
-        end
-
-        prompt_for_metric_name(metric, default_key)&.tap do |key|
-          default_key = key
-          metric.key = key
-        end
-      end
-    end
-
-    def file_saved_context_message(attributes)
-      format_prefix "  ", <<~TEXT.chomp
-        - Visit #{format_info('https://metrics.gitlab.com')} to find dashboard links for this metric
-        - Metric trend dashboard: #{format_info(metric_trend_path(attributes['key_path']))}
-      TEXT
-    end
-
-    # Check existing event files for attributes to copy over
-    def prompt_for_copying_event_properties
-      shared_values = collect_values_for_shared_event_properties
-      defaults = shared_values.except(:stage, :section)
-
-      return {} if shared_values.none?
-
-      return shared_values if defaults.none?
-
-      new_page!(5, 9, STEPS)
-
-      cli.say <<~TEXT
-        #{format_info('Convenient! We can copy these attributes from the event definition(s):')}
-
-        #{defaults.compact.transform_keys(&:to_s).to_yaml(line_width: 150)}
-        #{format_info('If any of these attributes are incorrect, you can also change them manually from your text editor later.')}
-
-      TEXT
-
-      cli.select('What would you like to do?', **select_opts) do |menu|
-        menu.enum '.'
-        menu.choice 'Copy & continue', -> { bulk_assign(defaults) }
-        menu.choice 'Modify attributes'
-      end
-
-      shared_values
-    end
-
-    def prompt_for_product_group(defaults)
-      assign_shared_attr(:product_group) do
-        new_page!(6, 9, STEPS)
-
-        prompt_for_group_ownership('Which group owns the metric?', defaults)
-      end
-    end
-
-    def prompt_for_url(defaults)
-      assign_shared_attr(:introduced_by_url) do
-        new_page!(7, 9, STEPS)
-
-        prompt_for_text(
-          'Which MR URL introduced the metric?',
-          defaults[:introduced_by_url]
-        )
-      end
-    end
-
-    def prompt_for_tier(defaults)
-      assign_shared_attr(:tier) do
-        new_page!(8, 9, STEPS)
-
-        prompt_for_array_selection(
-          'Which tiers will the metric be reported from?',
-          [%w[free premium ultimate], %w[premium ultimate], %w[ultimate]],
-          defaults[:tier]
-        )
-      end
-
-      assign_shared_attr(:tiers) { |metric| [*metric.tier] }
-
-      assign_shared_attr(:distribution) do |metric|
-        metric.tier.include?('free') ? %w[ce ee] : %w[ee]
-      end
-    end
-
-    def create_metric_files
-      @metrics.map.with_index do |metric, idx|
-        new_page!(9, 9, STEPS) # Repeat the same step number but increment metric counter
-
-        cli.say format_prompt(format_subheader('SAVING FILE', metric.description, idx, @metrics.length))
-        cli.say "\n"
-
-        prompt_to_save_file(metric.file_path, metric.formatted_output)
-      end
-    end
-
-    def prompt_for_next_steps(outcomes = [])
-      new_page!
-
-      outcome = outcomes.any? ? outcomes.compact.join("\n") : '  No files saved.'
-      metric = @metrics.first
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_info('Done with metric definitions!')}
-
-        #{outcome}
-        #{divider}
-
-          Have you instrumented the application code to trigger the event yet? View usage examples to easily copy/paste implementation!
-
-          Want to verify the metrics? Check out the group::#{metric[:product_group]} Metrics Exploration Dashboard in Tableau
-            Note: The Metrics Exploration Dashboard data would be available ~1 week after deploy for Gitlab.com, ~1 week after next release for self-managed
-            Link: #{format_info(metric_exploration_group_path(metric[:product_group], find_stage(metric.product_group)))}
-
-          Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake
-
-      TEXT
-
-      actions = selected_events.map(&:action).join(', ')
-      next_step = cli.select("How would you like to proceed?", **select_opts) do |menu|
-        menu.enum "."
-
-        menu.choice "New Event -- define a new event", :new_event
-        menu.choice "New Metric -- define another metric for #{actions}", :new_metric_with_events
-        menu.choice "New Metric -- define another metric", :new_metric
-        choice = "View Usage -- look at code examples for event #{selected_events.first.action}"
-        menu.default choice
-        menu.choice choice, :view_usage
-        menu.choice 'Exit', :exit
-      end
-
-      case next_step
-      when :new_event
-        InternalEventsCli::EventDefiner.new(cli).run
-      when :new_metric_with_events
-        MetricDefiner.new(cli, @selected_event_paths).run
-      when :new_metric
-        MetricDefiner.new(cli).run
-      when :view_usage
-        UsageViewer.new(cli, @selected_event_paths.first, selected_events.first).run
-      when :exit
-        cli.say Text::FEEDBACK_NOTICE
-      end
-    end
-
-    # ----- Prompt-specific Helpers -------------
-
-    # Helper for #prompt_for_metrics
-    def selected_events_filter_options
-      filterable_events_selected = selected_events.any? { |event| event.additional_properties&.any? }
-
-      selected_events.map do |event|
-        filters = event.additional_properties&.keys
-        filter_phrase = if filters
-                          " (filterable by #{filters&.join(', ')})"
-                        elsif filterable_events_selected
-                          ' -- not filterable'
-                        end
-
-        "  - #{event.action}#{format_help(filter_phrase)}\n"
-      end
-    end
-
-    # Helper for #prompt_for_event_filters
-    def print_event_filter_header(event, idx, total)
-      cli.say "\n"
-      cli.say format_info(format_subheader('SETTING EVENT FILTERS', event.action, idx, total))
-
-      return unless event.additional_properties&.any?
-
-      event_filter_options = event.additional_properties.map do |property, attrs|
-        "  #{property}: #{attrs['description']}\n"
-      end
-
-      cli.say event_filter_options.join
-    end
-
-    # Helper for #prompt_for_event_filters
-    def deselect_nonfilterable_event?(event)
-      cli.say "\n"
-
-      return false if event.additional_properties&.any?
-      return false if cli.yes?("This event is not filterable. Should it be included in the metric?", **yes_no_opts)
-
-      selected_events.delete(event)
-      bulk_assign(actions: selected_events.map(&:action).sort)
-
-      true
-    end
-
-    # Helper for #prompt_for_event_filters
-    def prompt_for_property_filter(action, property, default)
-      formatted_prop = format_info(property)
-      prompt = "Count where #{formatted_prop} equals any of (comma-sep):"
-
-      inputs = prompt_for_text(prompt, default, **input_opts) do |q|
-        if property == 'value'
-          q.convert ->(input) { input.split(',').map(&:to_i).uniq }
-          q.validate %r{^(\d|\s|,)*$}
-          q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric"
-        else
-          q.convert ->(input) { input.split(',').map(&:strip).uniq }
-        end
-      end
-
-      return unless inputs&.any?
-
-      @selected_filters[action] ||= {}
-      @selected_filters[action][property] = inputs.join(',')
-
-      inputs.map { |input| { property => input } }.uniq
-    end
-
-    # Helper for #prompt_for_event_filters
-    #
-    # Gets all the permutations of the provided property values.
-    # @param filters [Array] ex) [{ 'label' => 'red' }, { 'label' => 'blue' }, { value => 16 }]
-    # @return ex) [{ 'label' => 'red', value => 16 }, { 'label' => 'blue', value => 16 }]
-    def find_filter_permutations(action, filters)
-      # Define a filter for all events, regardless of the available props so NewMetric#events is correct
-      return [[action, {}]] unless filters&.any?
-
-      # Uses proc syntax to avoid spliting & type-checking `filters`
-      :product.to_proc.call(*filters).map do |filter|
-        [action, filter.reduce(&:merge)]
-      end
-    end
-
-    # Helper for #prompt_for_descriptions
-    def selected_event_descriptions
-      selected_events.map do |event|
-        filters = @selected_filters[event.action]
-
-        if filters&.any?
-          filter_phrase = filters.map { |k, v| "#{k}=#{v}" }.join(' ')
-          filter_phrase = format_help("(#{filter_phrase})")
-        end
-
-        "  #{event.action}#{filter_phrase} - #{format_selection(event.description)}\n"
-      end
-    end
-
-    # Helper for #prompt_for_descriptions
-    def prompt_for_description(metric, default)
-      description_start = format_info("#{metric.description_prefix}...")
-
-      cli.say <<~TEXT
-
-        #{input_opts[:prefix]} How would you describe this metric to a non-technical person? #{input_required_text}
-
-      TEXT
-
-      prompt_for_text("  Finish the description: #{description_start}", default, multiline: true) do |q|
-        q.required true
-        q.modify :trim
-        q.messages[:required?] = Text::METRIC_DESCRIPTION_HELP
-      end
-    end
-
-    # Helper for #prompt_for_descriptions
-    def prompt_for_metric_name(metric, default)
-      name_reason = name_requirement_reason(metric)
-      default_name = metric.key.value
-      display_name = metric.key.value("\e[0m[REPLACE ME]\e[36m")
-      empty_name = metric.key.value('')
-
-      return unless name_reason
-
-      cli.say <<~TEXT
-
-        #{input_opts[:prefix]} #{name_reason[:text]} How should we refererence this metric? #{input_required_text}
-
-                    ID:  #{format_info(display_name)}
-              Filename:  #{format_info(display_name)}#{format_info('.yml')}
-
-      TEXT
-
-      max_length = MAX_FILENAME_LENGTH - "#{empty_name}.yml".length
-      help_tokens = { name: default_name, count: max_length }
-
-      prompt_for_text('  Replace with: ', default, multiline: true) do |q|
-        q.required true
-        q.messages[:required?] = name_reason[:help] % help_tokens
-        q.messages[:valid?] = Text::METRIC_NAME_ERROR % help_tokens
-        q.validate ->(input) do
-          input.length <= max_length &&
-            input.match?(NAME_REGEX) &&
-            !conflicting_key_path?(metric.key.value(input))
-        end
-      end
-    end
-
-    # Helper for #prompt_for_descriptions
-    def name_requirement_reason(metric)
-      if metric.filters.assigned?
-        NAME_REQUIREMENT_REASONS[:filters]
-      elsif metric.file_name.length > MAX_FILENAME_LENGTH
-        NAME_REQUIREMENT_REASONS[:length]
-      elsif conflicting_key_path?(metric.key_path)
-        NAME_REQUIREMENT_REASONS[:conflict]
-      end
-    end
-
-    # Helper for #prompt_for_descriptions
-    def conflicting_key_path?(key_path)
-      cli.global.metrics.any? do |existing_metric|
-        existing_metric.key_path == key_path
-      end
-    end
-
-    # Helper for #prompt_for_copying_event_properties
-    def collect_values_for_shared_event_properties
-      fields = Hash.new { |h, k| h[k] = [] }
-
-      selected_events.each do |event|
-        fields[:introduced_by_url] << event.introduced_by_url
-        fields[:product_group] << event.product_group
-        fields[:stage] << find_stage(event.product_group)
-        fields[:section] << find_section(event.product_group)
-        fields[:distribution] << event.distributions&.sort
-        fields[:tier] << event.tiers&.sort
-        fields[:tiers] << event.tiers&.sort
-      end
-
-      # Keep event values if every selected event is the same
-      fields.each_with_object({}) do |(attr, values), defaults|
-        next unless values.compact.uniq.length == 1
-
-        defaults[attr] ||= values.first
-      end
-    end
-
-    # ----- Shared Helpers ----------------------
-
-    def assign_shared_attrs(...)
-      metric = @metrics.first
-      attrs = metric.to_h.slice(...)
-      attrs = yield(metric) unless attrs.values.all?
-
-      bulk_assign(attrs)
-    end
-
-    def assign_shared_attr(key)
-      assign_shared_attrs(key) do |metric|
-        { key => yield(metric) }
-      end
-    end
-
-    def bulk_assign(attrs)
-      @metrics.each { |metric| metric.bulk_assign(attrs) }
-    end
-  end
-end
diff --git a/scripts/internal_events/cli/usage_viewer.rb b/scripts/internal_events/cli/usage_viewer.rb
deleted file mode 100755
index d91fa6f3c4ebe4d938df77fb2db990cab9d4705f..0000000000000000000000000000000000000000
--- a/scripts/internal_events/cli/usage_viewer.rb
+++ /dev/null
@@ -1,434 +0,0 @@
-# frozen_string_literal: true
-
-require_relative './helpers'
-
-module InternalEventsCli
-  class UsageViewer
-    include Helpers
-
-    PROPERTY_EXAMPLES = {
-      'label' => "'string'",
-      'property' => "'string'",
-      'value' => '72',
-      'custom_key' => 'custom_value'
-    }.freeze
-
-    attr_reader :cli, :event
-
-    def initialize(cli, event_path = nil, event = nil)
-      @cli = cli
-      @event = event
-      @selected_event_path = event_path
-    end
-
-    def run
-      prompt_for_eligible_event
-      prompt_for_usage_location
-    end
-
-    def prompt_for_eligible_event
-      return if event
-
-      event_details = events_by_filepath
-
-      @selected_event_path = cli.select(
-        "Show examples for which event?",
-        get_event_options(event_details),
-        **select_opts,
-        **filter_opts
-      )
-
-      @event = event_details[@selected_event_path]
-    end
-
-    def prompt_for_usage_location(default = '1. ruby/rails')
-      choices = [
-        { name: '1. ruby/rails', value: :rails },
-        { name: '2. rspec', value: :rspec },
-        { name: '3. javascript (vue)', value: :vue },
-        { name: '4. javascript (plain)', value: :js },
-        { name: '5. vue template', value: :vue_template },
-        { name: '6. haml', value: :haml },
-        { name: '7. Manual testing in GDK', value: :gdk },
-        { name: '8. Data verification in Tableau', value: :tableau },
-        { name: '9. View examples for a different event', value: :other_event },
-        { name: '10. Exit', value: :exit }
-      ]
-
-      usage_location = cli.select(
-        'Select a use-case to view examples for:',
-        choices,
-        **select_opts,
-        **filter_opts,
-        per_page: 10
-      ) do |menu|
-        menu.default default
-      end
-
-      case usage_location
-      when :rails
-        rails_examples
-        prompt_for_usage_location('1. ruby/rails')
-      when :rspec
-        rspec_examples
-        prompt_for_usage_location('2. rspec')
-      when :haml
-        haml_examples
-        prompt_for_usage_location('6. haml')
-      when :js
-        js_examples
-        prompt_for_usage_location('4. javascript (plain)')
-      when :vue
-        vue_examples
-        prompt_for_usage_location('3. javascript (vue)')
-      when :vue_template
-        vue_template_examples
-        prompt_for_usage_location('5. vue template')
-      when :gdk
-        gdk_examples
-        prompt_for_usage_location('7. Manual testing in GDK')
-      when :tableau
-        service_ping_dashboard_examples
-        prompt_for_usage_location('8. Data verification in Tableau')
-      when :other_event
-        self.class.new(cli).run
-      when :exit
-        cli.say(Text::FEEDBACK_NOTICE)
-      end
-    end
-
-    def rails_examples
-      identifier_args = identifiers.map do |identifier|
-        "  #{identifier}: #{identifier}"
-      end
-
-      property_args = format_additional_properties do |property, value, description|
-        "    #{property}: #{value}, # #{description}"
-      end
-
-      if property_args.any?
-        # remove trailing comma after last arg but keep any other commas
-        property_args.last.sub!(',', '')
-        property_arg = "  additional_properties: {\n#{property_args.join("\n")}\n  }"
-      end
-
-      args = ["'#{action}'", *identifier_args, property_arg].compact.join(",\n")
-      args = "\n  #{args}\n" if args.lines.count > 1
-
-      cli.say format_warning <<~TEXT
-        #{divider}
-        #{format_help('# RAILS')}
-
-        include Gitlab::InternalEventsTracking
-
-        track_internal_event(#{args})
-
-        #{divider}
-      TEXT
-    end
-
-    def rspec_examples
-      identifier_args = identifiers.map do |identifier|
-        "  let(:#{identifier}) { create(:#{identifier}) }\n"
-      end.join('')
-
-      property_args = format_additional_properties do |property, value|
-        "    #{property}: #{value}"
-      end
-
-      if property_args.any?
-        property_arg = format_prefix '  ', <<~TEXT
-          let(:additional_properties) do
-            {
-          #{property_args.join(",\n")}
-            }
-          end
-        TEXT
-      end
-
-      args = [*identifier_args, *property_arg].join('')
-
-      cli.say format_warning <<~TEXT
-        #{divider}
-        #{format_help('# RSPEC')}
-
-        it_behaves_like 'internal event tracking' do
-          let(:event) { '#{action}' }
-        #{args}end
-
-        #{divider}
-      TEXT
-    end
-
-    def haml_examples
-      property_args = format_additional_properties do |property, value, _|
-        "event_#{property}: #{value}"
-      end
-
-      args = ["event_tracking: '#{action}'", *property_args].join(', ')
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_help('# HAML -- ON-CLICK')}
-
-        .inline-block{ #{format_warning("data: { #{args} }")} }
-          = _('Important Text')
-
-        #{divider}
-        #{format_help('# HAML -- COMPONENT ON-CLICK')}
-
-        = render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { #{args} }")} })
-
-        #{divider}
-        #{format_help('# HAML -- COMPONENT ON-LOAD')}
-
-        = render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { event_tracking_load: true, #{args} }")} })
-
-        #{divider}
-      TEXT
-
-      cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
-    end
-
-    def vue_template_examples
-      on_click_args = template_formatted_args('data-event-tracking', indent: 2)
-      on_load_args = template_formatted_args('data-event-tracking-load', indent: 2)
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_help('// VUE TEMPLATE -- ON-CLICK')}
-
-        <script>
-        import { GlButton } from '@gitlab/ui';
-
-        export default {
-          components: { GlButton }
-        };
-        </script>
-
-        <template>
-          <gl-button#{on_click_args}
-            Click Me
-          </gl-button>
-        </template>
-
-        #{divider}
-        #{format_help('// VUE TEMPLATE -- ON-LOAD')}
-
-        <script>
-        import { GlButton } from '@gitlab/ui';
-
-        export default {
-          components: { GlButton }
-        };
-        </script>
-
-        <template>
-          <gl-button#{on_load_args}
-            Click Me
-          </gl-button>
-        </template>
-
-        #{divider}
-      TEXT
-
-      cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
-    end
-
-    def js_examples
-      args = js_formatted_args(indent: 2)
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_help('// FRONTEND -- RAW JAVASCRIPT')}
-
-        #{format_warning("import { InternalEvents } from '~/tracking';")}
-
-        export const performAction = () => {
-          #{format_warning("InternalEvents.trackEvent#{args}")}
-
-          return true;
-        };
-
-        #{divider}
-      TEXT
-
-      # https://docs.snowplow.io/docs/understanding-your-pipeline/schemas/
-      cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
-    end
-
-    def vue_examples
-      args = js_formatted_args(indent: 6)
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_help('// VUE')}
-
-        <script>
-        #{format_warning("import { InternalEvents } from '~/tracking';")}
-        import { GlButton } from '@gitlab/ui';
-
-        #{format_warning('const trackingMixin = InternalEvents.mixin();')}
-
-        export default {
-          #{format_warning('mixins: [trackingMixin]')},
-          components: { GlButton },
-          methods: {
-            performAction() {
-              #{format_warning("this.trackEvent#{args}")}
-            },
-          },
-        };
-        </script>
-
-        <template>
-          <gl-button @click=performAction>Click Me</gl-button>
-        </template>
-
-        #{divider}
-      TEXT
-
-      cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
-    end
-
-    private
-
-    def action
-      event['action']
-    end
-
-    def identifiers
-      Array(event['identifiers']).tap do |ids|
-        # We always auto assign namespace if project is provided
-        ids.delete('namespace') if ids.include?('project')
-      end
-    end
-
-    def additional_properties
-      Array(event['additional_properties'])
-    end
-
-    def format_additional_properties
-      additional_properties.map do |property, details|
-        example_value = PROPERTY_EXAMPLES[property]
-        description = details['description'] || 'TODO'
-
-        yield(property, example_value, description)
-      end
-    end
-
-    def js_formatted_args(indent:)
-      return "('#{action}');" if additional_properties.none?
-
-      property_args = format_additional_properties do |property, value, description|
-        "    #{property}: #{value}, // #{description}"
-      end
-
-      [
-        '(',
-        "  '#{action}',",
-        '  {',
-        *property_args,
-        '  },',
-        ');'
-      ].join("\n#{' ' * indent}")
-    end
-
-    def service_ping_metrics_info
-      product_group = related_metrics.map(&:product_group).uniq
-
-      <<~TEXT
-        #{product_group.map { |group| "#{group}: #{format_info(metric_exploration_group_path(group, find_stage(group)))}" }.join("\n")}
-
-        #{divider}
-        #{format_help("# METRIC TRENDS -- view data for a service ping metric for #{event.action}")}
-
-        #{related_metrics.map { |metric| "#{metric.key_path}: #{format_info(metric_trend_path(metric.key_path))}" }.join("\n")}
-      TEXT
-    end
-
-    def service_ping_no_metric_info
-      <<~TEXT
-        #{format_help("# Warning: There are no metrics for #{event.action} yet.")}
-        #{event.product_group}: #{format_info(metric_exploration_group_path(event.product_group, find_stage(event.product_group)))}
-      TEXT
-    end
-
-    def template_formatted_args(data_attr, indent:)
-      return " #{data_attr}=\"#{action}\">" if additional_properties.none?
-
-      spacer = ' ' * indent
-      property_args = format_additional_properties do |property, value, _|
-        "  data-event-#{property}=#{value.tr("'", '"')}"
-      end
-
-      args = [
-        '', # start args on next line
-        "  #{data_attr}=\"#{action}\"",
-        *property_args
-      ].join("\n#{spacer}")
-
-      "#{format_warning(args)}\n#{spacer}>"
-    end
-
-    def related_metrics
-      cli.global.metrics.select { |metric| metric.actions&.include?(event.action) }
-    end
-
-    def service_ping_dashboard_examples
-      cli.say <<~TEXT
-        #{divider}
-        #{format_help('# GROUP DASHBOARDS -- view all service ping metrics for a specific group')}
-
-        #{related_metrics.any? ? service_ping_metrics_info : service_ping_no_metric_info}
-        #{divider}
-        Note: The metric dashboard links can also be accessed from #{format_info('https://metrics.gitlab.com/')}
-
-        Not what you're looking for? Check this doc:
-          - #{format_info('https://docs.gitlab.com/ee/development/internal_analytics/#data-discovery')}
-
-      TEXT
-    end
-
-    def gdk_examples
-      key_paths = related_metrics.map(&:key_path)
-
-      cli.say <<~TEXT
-        #{divider}
-        #{format_help('# TERMINAL -- monitor events & changes to service ping metrics as they occur')}
-
-        1. From `gitlab/` directory, run the monitor script:
-
-        #{format_warning("bin/rails runner scripts/internal_events/monitor.rb #{event.action}")}
-
-        2. View metric updates within the terminal
-
-        3. [Optional] Configure gdk with snowplow micro to see individual events: https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/snowplow_micro.md
-
-        #{divider}
-        #{format_help('# RAILS CONSOLE -- generate service ping payload, including most recent usage data')}
-
-        #{format_warning("require_relative 'spec/support/helpers/service_ping_helpers.rb'")}
-
-        #{format_help('# Get current value of a metric')}
-        #{
-          if key_paths.any?
-            key_paths.map { |key_path| format_warning("ServicePingHelpers.get_current_usage_metric_value('#{key_path}')") }.join("\n")
-          else
-            format_help("# Warning: There are no metrics for #{event.action} yet. When there are, replace <key_path> below.\n") +
-            format_warning('ServicePingHelpers.get_current_usage_metric_value(<key_path>)')
-          end
-        }
-
-        #{format_help('# View entire service ping payload')}
-        #{format_warning('ServicePingHelpers.get_current_service_ping_payload')}
-        #{divider}
-        Need to test something else? Check these docs:
-        - https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.html
-        - https://docs.gitlab.com/ee/development/internal_analytics/service_ping/troubleshooting.html
-        - https://docs.gitlab.com/ee/development/internal_analytics/review_guidelines.html
-
-      TEXT
-    end
-  end
-end