diff --git a/doc/development/internal_analytics/internal_event_instrumentation/migration.md b/doc/development/internal_analytics/internal_event_instrumentation/migration.md
index 32a68011e5a6c42ebfa828dbfd45299b37aaea8a..2ef439e21e977cbcc5539652df6993c9bc719f28 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/migration.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/migration.md
@@ -42,17 +42,14 @@ Gitlab::InternalEvents.track_event('ci_templates_unique', namespace: namespace,
 
 In addition, you have to create definitions for the metrics that you would like to track.
 
-To generate metric definitions, you can use the generator like this:
+To generate metric definitions, you can use the generator:
 
 ```shell
-bin/rails g gitlab:analytics:internal_events \
-  --time_frames=7d 28d\
-  --group=project_management \
-  --event=ci_templates_unique \
-  --unique=user.id \
-  --mr=https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121544
+ruby scripts/internal_events/cli.rb
 ```
 
+The generator walks you through the required inputs step-by-step.
+
 ### Frontend
 
 If you are using the `Tracking` mixin in the Vue component, you can replace it with the `InternalEvents` mixin.
diff --git a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
index 34be2080e9537650d771df3260a80fb574c4ee91..6f48f83e7ca320b14326a15dfd3aa85a2e4874dc 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
@@ -17,30 +17,13 @@ In order to instrument your code with Internal Events Tracking you need to do th
 
 ## Defining event and metrics
 
-<div class="video-fallback">
-  See the video about <a href="https://www.youtube.com/watch?v=QICKWznLyy0">adding events and metrics using the generator</a>
-</div>
-<figure class="video_container">
-  <iframe src="https://www.youtube-nocookie.com/embed/QICKWznLyy0" frameborder="0" allowfullscreen="true"> </iframe>
-</figure>
-
-To create an event and metric definitions you can use the `internal_events` generator.
-
-This example creates an event definition for an event called `project_created` and two metric definitions, which are aggregated every 7 and 28 days.
+To create event and/or metric definitions, use the `internal_events` generator from the `gitlab` directory:
 
 ```shell
-bundle exec rails generate gitlab:analytics:internal_events \
---time_frames=7d 28d \
---group=project_management \
---event=project_created \
---unique=user.id \
---mr=https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121544
+ruby scripts/internal_events/cli.rb
 ```
 
-Where:
-
-- `time_frames`: Valid options are `7d` and `28d` if you provide a `unique` value and `7d`, `28d` and `all` for metrics without `unique`.
-- `unique`: Valid options are `user.id`, `project.id`, and `namespace.id`, as they are logged as part of the standard context. We [are actively working](https://gitlab.com/gitlab-org/gitlab/-/issues/411255) on a way to define uniqueness on arbitrary properties sent with the event, such as `merge_request.id`.
+This CLI will help you create the correct defintion files based on your specific use-case, then provide code examples for instrumentation and testing.
 
 ## Trigger events
 
@@ -52,11 +35,11 @@ To trigger an event, call the `Gitlab::InternalEvents.track_event` method with t
 
 ```ruby
 Gitlab::InternalEvents.track_event(
-        "i_code_review_user_apply_suggestion",
-        user: user,
-        namespace: namespace,
-        project: project
-        )
+  "i_code_review_user_apply_suggestion",
+  user: user,
+  namespace: namespace,
+  project: project
+)
 ```
 
 This method automatically increments all RedisHLL metrics relating to the event `i_code_review_user_apply_suggestion`, and sends a corresponding Snowplow event with all named arguments and standard context (SaaS only).
diff --git a/scripts/internal_events/cli.rb b/scripts/internal_events/cli.rb
new file mode 100755
index 0000000000000000000000000000000000000000..6cc9f599608e989aeb539caaf12d163a5f32e338
--- /dev/null
+++ b/scripts/internal_events/cli.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+# !/usr/bin/env ruby
+#
+# Generate a metric/event files in the correct locations.
+
+require 'tty-prompt'
+require 'net/http'
+require 'yaml'
+
+require_relative './cli/helpers'
+require_relative './cli/usage_viewer'
+require_relative './cli/metric_definer'
+require_relative './cli/event_definer'
+require_relative './cli/metric'
+require_relative './cli/event'
+require_relative './cli/text'
+
+class Cli
+  include ::InternalEventsCli::Helpers
+
+  attr_reader :cli
+
+  def initialize(cli)
+    @cli = cli
+  end
+
+  def run
+    cli.say InternalEventsCli::Text::FEEDBACK_NOTICE
+    cli.say InternalEventsCli::Text::CLI_INSTRUCTIONS
+
+    task = cli.select("What would you like to do?", **select_opts) do |menu|
+      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
+      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
+      menu.choice 'View Usage -- look at code examples for an existing event', :view_usage
+      menu.choice '...am I in the right place?', :help_decide
+    end
+
+    case task
+    when :new_event
+      InternalEventsCli::EventDefiner.new(cli).run
+    when :new_metric
+      InternalEventsCli::MetricDefiner.new(cli).run
+    when :view_usage
+      InternalEventsCli::UsageViewer.new(cli).run
+    when :help_decide
+      help_decide
+    end
+  end
+
+  private
+
+  def help_decide
+    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
+
+  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 InternalEventsCli::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 InternalEventsCli::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 InternalEventsCli::Text::ALTERNATE_RESOURCES_NOTICE
+    cli.say InternalEventsCli::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 min)\n")
+
+    return not_ready_error('New Metric') unless cli.yes?(format_prompt('Ready to start?'))
+
+    InternalEventsCli::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 min)\n")
+
+    return not_ready_error('New Event') unless cli.yes?(format_prompt('Ready to start?'))
+
+    InternalEventsCli::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 InternalEventsCli::Text::FEEDBACK_NOTICE
+  end
+end
+
+if $PROGRAM_NAME == __FILE__
+  begin
+    Cli.new(TTY::Prompt.new).run
+  rescue Interrupt
+    puts "\n"
+  end
+end
+
+# vim: ft=ruby
diff --git a/scripts/internal_events/cli/event.rb b/scripts/internal_events/cli/event.rb
new file mode 100755
index 0000000000000000000000000000000000000000..d98aa8a6bd156a1c12921997370d168a0a35f5a5
--- /dev/null
+++ b/scripts/internal_events/cli/event.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module InternalEventsCli
+  NEW_EVENT_FIELDS = [
+    :description,
+    :category,
+    :action,
+    :label_description,
+    :property_description,
+    :value_description,
+    :value_type,
+    :extra_properties,
+    :identifiers,
+    :product_section,
+    :product_stage,
+    :product_group,
+    :milestone,
+    :introduced_by_url,
+    :distributions,
+    :tiers
+  ].freeze
+
+  EVENT_DEFAULTS = {
+    product_section: nil,
+    product_stage: nil,
+    product_group: nil,
+    introduced_by_url: 'TODO',
+    category: 'InternalEventTracking'
+  }.freeze
+
+  Event = Struct.new(*NEW_EVENT_FIELDS, keyword_init: true) do
+    def formatted_output
+      EVENT_DEFAULTS
+        .merge(to_h.compact)
+        .slice(*NEW_EVENT_FIELDS)
+        .transform_keys(&:to_s)
+        .to_yaml(line_width: 150)
+    end
+
+    def file_path
+      File.join(
+        *[
+          ('ee' unless distributions.include?('ce')),
+          'config',
+          'events',
+          "#{action}.yml"
+        ].compact
+      )
+    end
+
+    def bulk_assign(key_value_pairs)
+      key_value_pairs.each { |key, value| self[key] = value }
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/event_definer.rb b/scripts/internal_events/cli/event_definer.rb
new file mode 100755
index 0000000000000000000000000000000000000000..e029f0e7cf64a4fd1df4c7401179ee9209ee2265
--- /dev/null
+++ b/scripts/internal_events/cli/event_definer.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require_relative './helpers'
+
+module InternalEventsCli
+  class EventDefiner
+    include Helpers
+
+    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[] => "Use case: For instance-level events without user interaction [LEAST COMMON]"
+    }.freeze
+
+    IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.max_by(&:length).join(', ')}]".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_identifiers
+      prompt_for_url
+      prompt_for_product_ownership
+      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 =~ /\A\w+\z/ && !events_by_filepath.values.map(&:action).include?(input) } # rubocop:disable Rails/NegateInclude -- Not rails
+        q.modify :trim
+        q.messages[:valid?] = format_warning("Invalid event name. Only letters/numbers/underscores allowed. " \
+                                             "Ensure %{value} is not an existing event.")
+        q.messages[:required?] = Text::EVENT_ACTION_HELP
+      end
+    end
+
+    def prompt_for_identifiers
+      new_page!(3, 7, STEPS)
+      cli.say Text::EVENT_IDENTIFIERS_INTRO % event.action
+
+      identifiers = prompt_for_array_selection(
+        'Which identifiers are available when the event occurs?',
+        IDENTIFIER_OPTIONS.keys
+      ) { |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_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_ownership
+      new_page!(5, 7, STEPS)
+
+      ownership = prompt_for_group_ownership({
+        product_section: 'Which section will own the event?',
+        product_stage: 'Which stage will own the event?',
+        product_group: 'Which group will own the event?'
+      })
+
+      event.bulk_assign(ownership)
+    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}
+
+          Want to have data reported in Snowplow/Sisense/ServicePing? Add a new metric for your event!
+
+      TEXT
+    end
+
+    def prompt_for_next_steps
+      next_step = cli.select("How would you like to proceed?", **select_opts) do |menu|
+        menu.enum "."
+
+        if File.exist?(event.file_path)
+          menu.choice "Create Metric -- define a new metric using #{event.action}.yml", :add_metric
+        else
+          menu.choice "Save & Create Metric -- save #{event.action}.yml and define a matching metric", :save_and_add
+        end
+
+        menu.choice "View Usage -- look at code examples for #{event.action}.yml", :view_usage
+        menu.choice 'Exit', :exit
+      end
+
+      case next_step
+      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/helpers.rb b/scripts/internal_events/cli/helpers.rb
new file mode 100755
index 0000000000000000000000000000000000000000..95672325652e7c91f81807466639672dba4aebab
--- /dev/null
+++ b/scripts/internal_events/cli/helpers.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require_relative './helpers/cli_inputs'
+require_relative './helpers/files'
+require_relative './helpers/formatting'
+require_relative './helpers/group_ownership'
+require_relative './helpers/event_options'
+require_relative './helpers/metric_options'
+
+module InternalEventsCli
+  module Helpers
+    include CliInputs
+    include Files
+    include Formatting
+    include GroupOwnership
+    include EventOptions
+    include MetricOptions
+
+    MILESTONE = File.read('VERSION').strip.match(/(\d+\.\d+)/).captures.first
+
+    def new_page!(page = nil, total = nil, steps = [])
+      cli.say TTY::Cursor.clear_screen
+      cli.say TTY::Cursor.move_to(0, 0)
+      cli.say "#{progress_bar(page, total, steps)}\n" if page && total
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/helpers/cli_inputs.rb b/scripts/internal_events/cli/helpers/cli_inputs.rb
new file mode 100755
index 0000000000000000000000000000000000000000..106d854d0b32e326e199787386ff3f17f039e540
--- /dev/null
+++ b/scripts/internal_events/cli/helpers/cli_inputs.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# Helpers related to configuration of TTY::Prompt prompts
+module InternalEventsCli
+  module Helpers
+    module CliInputs
+      def prompt_for_array_selection(message, choices, default = nil, &formatter)
+        formatter ||= ->(choice) { choice.sort.join(", ") }
+
+        choices = choices.map do |choice|
+          { name: formatter.call(choice), value: choice }
+        end
+
+        cli.select(message, choices, **select_opts) do |menu|
+          menu.enum "."
+          menu.default formatter.call(default) if default
+        end
+      end
+
+      def prompt_for_text(message, value = nil)
+        help_message = "(enter to #{value ? 'submit' : 'skip'})"
+
+        cli.ask(
+          "#{message} #{format_help(help_message)}",
+          value: value || '',
+          **input_opts
+        )
+      end
+
+      def input_opts
+        { prefix: format_prompt('Input text: ') }
+      end
+
+      def yes_no_opts
+        { prefix: format_prompt('Yes/No: ') }
+      end
+
+      def select_opts
+        { prefix: format_prompt('Select one: '), cycle: true, show_help: :always }
+      end
+
+      def multiselect_opts
+        { prefix: format_prompt('Select multiple: '), cycle: true, show_help: :always, min: 1 }
+      end
+
+      # Accepts a number of lines occupied by text, so remaining
+      # screen real estate can be filled with select options
+      def filter_opts(header_size: nil)
+        {
+          filter: true,
+          per_page: header_size ? [(window_height - header_size), 10].max : 30
+        }
+      end
+
+      def input_required_text
+        format_help("(leave blank for help)")
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/helpers/event_options.rb b/scripts/internal_events/cli/helpers/event_options.rb
new file mode 100755
index 0000000000000000000000000000000000000000..f53127798aa06912c24bbeb1dddc352f1a77ef02
--- /dev/null
+++ b/scripts/internal_events/cli/helpers/event_options.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+# Helpers related to listing existing event definitions
+module InternalEventsCli
+  module Helpers
+    module EventOptions
+      def get_event_options(events)
+        options = events.filter_map do |(path, event)|
+          next if duplicate_events?(event.action, events.values)
+
+          description = format_help(" - #{trim_description(event.description)}")
+
+          {
+            name: "#{format_event_name(event)}#{description}",
+            value: path
+          }
+        end
+
+        options.sort_by do |option|
+          category = events.dig(option[:value], 'category')
+          event_sort_param(category, option[:name])
+        end
+      end
+
+      def events_by_filepath(event_paths = [])
+        event_paths = load_event_paths if event_paths.none?
+
+        get_existing_events_for_paths(event_paths)
+      end
+
+      private
+
+      def trim_description(description)
+        return description if description.to_s.length < 50
+
+        "#{description[0, 50]}..."
+      end
+
+      def format_event_name(event)
+        case event.category
+        when 'InternalEventTracking', 'default'
+          event.action
+        else
+          "#{event.category}:#{event.action}"
+        end
+      end
+
+      def event_sort_param(category, name)
+        case category
+        when 'InternalEventTracking'
+          "0#{name}"
+        when 'default'
+          "1#{name}"
+        else
+          "2#{category}#{name}"
+        end
+      end
+
+      def get_existing_events_for_paths(event_paths)
+        event_paths.each_with_object({}) do |filepath, events|
+          details = YAML.safe_load(File.read(filepath))
+          fields = InternalEventsCli::NEW_EVENT_FIELDS.map(&:to_s)
+
+          events[filepath] = Event.new(**details.slice(*fields))
+        end
+      end
+
+      def duplicate_events?(action, events)
+        events.count { |event| action == event.action } > 1
+      end
+
+      def load_event_paths
+        [
+          Dir["config/events/*.yml"],
+          Dir["ee/config/events/*.yml"]
+        ].flatten
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/helpers/files.rb b/scripts/internal_events/cli/helpers/files.rb
new file mode 100755
index 0000000000000000000000000000000000000000..b613350353f469e36459a84c58785fb84bdede39
--- /dev/null
+++ b/scripts/internal_events/cli/helpers/files.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Helpers related reading/writing definition files
+module InternalEventsCli
+  module Helpers
+    module Files
+      def prompt_to_save_file(filepath, content)
+        cli.say <<~TEXT.chomp
+          #{format_info('Preparing to generate definition with these attributes:')}
+          #{filepath}
+          #{content}
+        TEXT
+
+        if File.exist?(filepath)
+          cli.error("Oh no! This file already exists!\n")
+
+          return if cli.no?(format_prompt('Overwrite file?'))
+
+          write_to_file(filepath, content, 'update')
+        elsif cli.yes?(format_prompt('Create file?'))
+          write_to_file(filepath, content, 'create')
+        end
+      end
+
+      def file_saved_message(verb, filepath)
+        "    #{format_selection(verb)} #{filepath}"
+      end
+
+      def write_to_file(filepath, content, verb)
+        File.write(filepath, content)
+
+        file_saved_message(verb, filepath).tap { |message| cli.say "\n#{message}\n" }
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/helpers/formatting.rb b/scripts/internal_events/cli/helpers/formatting.rb
new file mode 100755
index 0000000000000000000000000000000000000000..87be585c7397bc994fd41b48b9f1b75cb2db56a6
--- /dev/null
+++ b/scripts/internal_events/cli/helpers/formatting.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+# Helpers related to visual formatting of outputs
+module InternalEventsCli
+  module Helpers
+    module Formatting
+      DEFAULT_WINDOW_WIDTH = 100
+      DEFAULT_WINDOW_HEIGHT = 30
+
+      def format_info(string)
+        pastel.cyan(string)
+      end
+
+      def format_warning(string)
+        pastel.yellow(string)
+      end
+
+      def format_selection(string)
+        pastel.green(string)
+      end
+
+      def format_help(string)
+        pastel.bright_black(string)
+      end
+
+      def format_prompt(string)
+        pastel.magenta(string)
+      end
+
+      def format_error(string)
+        pastel.red(string)
+      end
+
+      def format_heading(string)
+        [divider, pastel.cyan(string), divider].join("\n")
+      end
+
+      def divider
+        "-" * window_size
+      end
+
+      def progress_bar(step, total, titles = [])
+        breadcrumbs = [
+          titles[0..(step - 1)],
+          format_selection(titles[step]),
+          titles[(step + 1)..]
+        ]
+
+        status = " Step #{step} / #{total} : #{breadcrumbs.flatten.join(' > ')}"
+        total_length = window_size - 4
+        step_length = step / total.to_f * total_length
+
+        incomplete = '-' * [(total_length - step_length - 1), 0].max
+        complete = '=' * [(step_length - 1), 0].max
+        "#{status}\n|==#{complete}>#{incomplete}|\n"
+      end
+
+      def counter(idx, total)
+        format_prompt("(#{idx + 1}/#{total})") if total > 1
+      end
+
+      private
+
+      def pastel
+        @pastel ||= Pastel.new
+      end
+
+      def window_size
+        Integer(fetch_window_size)
+      rescue StandardError
+        DEFAULT_WINDOW_WIDTH
+      end
+
+      def window_height
+        Integer(fetch_window_height)
+      rescue StandardError
+        DEFAULT_WINDOW_HEIGHT
+      end
+
+      def fetch_window_size
+        `tput cols`
+      end
+
+      def fetch_window_height
+        `tput lines`
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/helpers/group_ownership.rb b/scripts/internal_events/cli/helpers/group_ownership.rb
new file mode 100755
index 0000000000000000000000000000000000000000..9846f0ca2f507ffb96aff79ac7a3d74fc9727cc9
--- /dev/null
+++ b/scripts/internal_events/cli/helpers/group_ownership.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+# Helpers related to Stage/Section/Group ownership
+module InternalEventsCli
+  module Helpers
+    module GroupOwnership
+      STAGES_YML = 'https://gitlab.com/gitlab-com/www-gitlab-com/-/raw/master/data/stages.yml'
+
+      def prompt_for_group_ownership(messages, defaults = {})
+        groups = fetch_group_choices
+
+        if groups
+          prompt_for_ownership_from_ssot(messages[:product_group], defaults, groups)
+        else
+          prompt_for_ownership_manually(messages, defaults)
+        end
+      end
+
+      private
+
+      def prompt_for_ownership_from_ssot(prompt, defaults, groups)
+        sorted_defaults = defaults.values_at(:product_section, :product_stage, :product_group)
+        default = sorted_defaults.join(':')
+
+        cli.select(prompt, groups, **select_opts, **filter_opts) do |menu|
+          if sorted_defaults.all?
+            if groups.any? { |group| group[:name] == default }
+              # We have a complete group selection -> set as default in menu
+              menu.default(default)
+            else
+              cli.error format_error(">>> Failed to find group matching #{default}. Select another.\n")
+            end
+          elsif sorted_defaults.any?
+            # We have a partial selection -> filter the list by the most unique field
+            menu.instance_variable_set(:@filter, sorted_defaults.compact.last.split(''))
+          end
+        end
+      end
+
+      def prompt_for_ownership_manually(messages, defaults)
+        {
+          product_section: prompt_for_text(messages[:product_section], defaults[:product_section]),
+          product_stage: prompt_for_text(messages[:product_stage], defaults[:product_stage]),
+          product_group: prompt_for_text(messages[:product_group], defaults[:product_group])
+        }
+      end
+
+      # @return Array[<Hash - matches #prompt_for_ownership_manually output format>]
+      def fetch_group_choices
+        response = Timeout.timeout(5) { Net::HTTP.get(URI(STAGES_YML)) }
+        stages = YAML.safe_load(response)
+
+        stages['stages'].flat_map do |stage, value|
+          value['groups'].map do |group, _|
+            section = value['section']
+
+            {
+              name: [section, stage, group].join(':'),
+              value: {
+                product_group: group,
+                product_section: section,
+                product_stage: stage
+              }
+            }
+          end
+        end
+      rescue StandardError
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/helpers/metric_options.rb b/scripts/internal_events/cli/helpers/metric_options.rb
new file mode 100755
index 0000000000000000000000000000000000000000..01512115e05d7acf60c4de315cfdb031dfb68b15
--- /dev/null
+++ b/scripts/internal_events/cli/helpers/metric_options.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+# Helpers related to listing existing metric definitions
+module InternalEventsCli
+  module Helpers
+    module MetricOptions
+      EVENT_PHRASES = {
+        'user' => "who triggered %s",
+        'namespace' => "where %s occurred",
+        'project' => "where %s occurred",
+        nil => "%s occurrences"
+      }.freeze
+
+      def get_metric_options(events)
+        options = get_all_metric_options
+        identifiers = get_identifiers_for_events(events)
+        existing_metrics = get_existing_metrics_for_events(events)
+        metric_name = format_metric_name_for_events(events)
+
+        options = options.group_by do |metric|
+          [
+            metric.identifier,
+            metric_already_exists?(existing_metrics, metric),
+            metric.time_frame == 'all'
+          ]
+        end
+
+        options.map do |(identifier, defined, _), metrics|
+          format_metric_option(
+            identifier,
+            metric_name,
+            metrics,
+            defined: defined,
+            supported: [*identifiers, nil].include?(identifier)
+          )
+        end
+      end
+
+      private
+
+      def get_all_metric_options
+        [
+          Metric.new(time_frame: '28d', identifier: 'user'),
+          Metric.new(time_frame: '7d', identifier: 'user'),
+          Metric.new(time_frame: '28d', identifier: 'project'),
+          Metric.new(time_frame: '7d', identifier: 'project'),
+          Metric.new(time_frame: '28d', identifier: 'namespace'),
+          Metric.new(time_frame: '7d', identifier: 'namespace'),
+          Metric.new(time_frame: '28d'),
+          Metric.new(time_frame: '7d'),
+          Metric.new(time_frame: 'all')
+        ]
+      end
+
+      def load_metric_paths
+        [
+          Dir["config/metrics/counts_all/*.yml"],
+          Dir["config/metrics/counts_7d/*.yml"],
+          Dir["config/metrics/counts_28d/*.yml"],
+          Dir["ee/config/metrics/counts_all/*.yml"],
+          Dir["ee/config/metrics/counts_7d/*.yml"],
+          Dir["ee/config/metrics/counts_28d/*.yml"]
+        ].flatten
+      end
+
+      def get_existing_metrics_for_events(events)
+        actions = events.map(&:action).sort
+
+        load_metric_paths.filter_map do |path|
+          details = YAML.safe_load(File.read(path))
+          fields = InternalEventsCli::NEW_METRIC_FIELDS.map(&:to_s)
+
+          metric = Metric.new(**details.slice(*fields))
+          next unless metric.actions
+
+          metric if (metric.actions & actions).any?
+        end
+      end
+
+      def format_metric_name_for_events(events)
+        return events.first.action if events.length == 1
+
+        "any of #{events.length} events"
+      end
+
+      # Get only the identifiers in common for all events
+      def get_identifiers_for_events(events)
+        events.map(&:identifiers).reduce(&:&) || []
+      end
+
+      def metric_already_exists?(existing_metrics, metric)
+        existing_metrics.any? do |existing_metric|
+          time_frame = existing_metric.time_frame || 'all'
+          identifier = existing_metric.events&.dig(0, 'unique')&.chomp('.id')
+
+          metric.time_frame == time_frame && metric.identifier == identifier
+        end
+      end
+
+      def format_metric_option(identifier, event_name, metrics, defined:, supported:)
+        time_frame = metrics.map(&:time_frame_prefix).join('/')
+        unique_by = "unique #{identifier}s " if identifier
+        event_phrase = EVENT_PHRASES[identifier] % event_name
+
+        if supported && !defined
+          time_frame = format_info(time_frame)
+          unique_by = format_info(unique_by)
+        end
+
+        name = "#{time_frame} count of #{unique_by}[#{event_phrase}]"
+
+        if supported && defined
+          disabled = format_warning("(already defined)")
+          name = format_help(name)
+        elsif !supported
+          disabled = format_warning("(#{identifier} unavailable)")
+          name = format_help(name)
+        end
+
+        { name: name, value: metrics, disabled: disabled }.compact
+      end
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/metric.rb b/scripts/internal_events/cli/metric.rb
new file mode 100755
index 0000000000000000000000000000000000000000..63961d29810278e55fcc3479b7509322db0052fe
--- /dev/null
+++ b/scripts/internal_events/cli/metric.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+module InternalEventsCli
+  NEW_METRIC_FIELDS = [
+    :key_path,
+    :description,
+    :product_section,
+    :product_stage,
+    :product_group,
+    :performance_indicator_type,
+    :value_type,
+    :status,
+    :milestone,
+    :introduced_by_url,
+    :time_frame,
+    :data_source,
+    :data_category,
+    :product_category,
+    :instrumentation_class,
+    :distribution,
+    :tier,
+    :options,
+    :events
+  ].freeze
+
+  ADDITIONAL_METRIC_FIELDS = [
+    :milestone_removed,
+    :removed_by_url,
+    :removed_by,
+    :repair_issue_url,
+    :value_json_schema,
+    :name
+  ].freeze
+
+  METRIC_DEFAULTS = {
+    product_section: nil,
+    product_stage: nil,
+    product_group: nil,
+    introduced_by_url: 'TODO',
+    value_type: 'number',
+    status: 'active',
+    data_source: 'internal_events',
+    data_category: 'optional',
+    performance_indicator_type: []
+  }.freeze
+
+  Metric = Struct.new(*NEW_METRIC_FIELDS, *ADDITIONAL_METRIC_FIELDS, :identifier, keyword_init: true) do
+    def formatted_output
+      METRIC_DEFAULTS
+        .merge(to_h.compact)
+        .merge(
+          key_path: key_path,
+          instrumentation_class: instrumentation_class,
+          events: events)
+        .slice(*NEW_METRIC_FIELDS)
+        .transform_keys(&:to_s)
+        .to_yaml(line_width: 150)
+    end
+
+    def file_path
+      File.join(
+        *[
+          ('ee' unless distribution.include?('ce')),
+          'config',
+          'metrics',
+          "counts_#{time_frame}",
+          "#{key}.yml"
+        ].compact
+      )
+    end
+
+    def key
+      [
+        'count',
+        (identifier ? "distinct_#{identifier}_id_from" : 'total'),
+        actions.join('_and_'),
+        (time_frame_prefix&.downcase if time_frame != 'all')
+      ].compact.join('_')
+    end
+
+    def key_path
+      self[:key_path] ||= "#{key_path_prefix}.#{key}"
+    end
+
+    def instrumentation_class
+      self[:instrumentation_class] ||= identifier ? 'RedisHLLMetric' : 'TotalCountMetric'
+    end
+
+    def events
+      self[:events] ||= actions.map do |action|
+        if identifier
+          {
+            'name' => action,
+            'unique' => "#{identifier}.id"
+          }
+        else
+          { 'name' => action }
+        end
+      end
+    end
+
+    def key_path_prefix
+      case instrumentation_class
+      when 'RedisHLLMetric'
+        'redis_hll_counters'
+      when 'TotalCountMetric'
+        'counts'
+      end
+    end
+
+    def actions
+      options&.dig('events')&.sort || []
+    end
+
+    def identifier_prefix
+      if identifier
+        "count of unique #{identifier}s"
+      else
+        "count of"
+      end
+    end
+
+    def time_frame_prefix
+      case time_frame
+      when '7d'
+        'Weekly'
+      when '28d'
+        'Monthly'
+      when 'all'
+        'Total'
+      end
+    end
+
+    def prefix
+      [time_frame_prefix, identifier_prefix].join(' ')
+    end
+
+    def technical_description
+      simple_event_list = actions.join(' or ')
+
+      case identifier
+      when 'user'
+        "#{prefix} who triggered #{simple_event_list}"
+      when 'project', 'namespace'
+        "#{prefix} where #{simple_event_list} occurred"
+      else
+        "#{prefix} #{simple_event_list} occurrences"
+      end
+    end
+
+    def bulk_assign(key_value_pairs)
+      key_value_pairs.each { |key, value| self[key] = value }
+    end
+  end
+end
diff --git a/scripts/internal_events/cli/metric_definer.rb b/scripts/internal_events/cli/metric_definer.rb
new file mode 100755
index 0000000000000000000000000000000000000000..7688f03200fc3961a6c1c6f9b3e12d8bb17d92a7
--- /dev/null
+++ b/scripts/internal_events/cli/metric_definer.rb
@@ -0,0 +1,310 @@
+# frozen_string_literal: true
+
+require_relative './helpers'
+
+module InternalEventsCli
+  class MetricDefiner
+    include Helpers
+
+    STEPS = [
+      'New Metric',
+      'Type',
+      'Events',
+      'Scope',
+      'Descriptions',
+      'Copy event',
+      'Group',
+      'URL',
+      'Tiers',
+      'Save files'
+    ].freeze
+
+    attr_reader :cli
+
+    def initialize(cli, starting_event = nil)
+      @cli = cli
+      @selected_event_paths = Array(starting_event)
+      @metrics = []
+    end
+
+    def run
+      type = prompt_for_metric_type
+      prompt_for_events(type)
+
+      return unless @selected_event_paths.any?
+
+      prompt_for_metrics
+
+      return unless @metrics.any?
+
+      prompt_for_description
+      defaults = prompt_for_copying_event_properties
+      prompt_for_product_ownership(defaults)
+      prompt_for_url(defaults)
+      prompt_for_tier(defaults)
+      outcomes = create_metric_files
+      prompt_for_next_steps(outcomes)
+    end
+
+    private
+
+    def events
+      @events ||= events_by_filepath(@selected_event_paths)
+    end
+
+    def selected_events
+      @selected_events ||= events.values_at(*@selected_event_paths)
+    end
+
+    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)
+
+      @metrics = cli.select('Which metrics do you want to add?', eligible_metrics, **select_opts)
+
+      assign_shared_attrs(:options, :milestone) do
+        {
+          options: { 'events' => selected_events.map(&:action) },
+          milestone: MILESTONE
+        }
+      end
+    end
+
+    def prompt_for_description
+      new_page!(4, 9, STEPS)
+
+      cli.say Text::METRIC_DESCRIPTION_INTRO
+      cli.say selected_event_descriptions.join('')
+
+      base_description = nil
+
+      @metrics.each_with_index do |metric, idx|
+        multiline_prompt = [
+          counter(idx, @metrics.length),
+          format_prompt("Complete the text:"),
+          "How would you describe this metric to a non-technical person?",
+          input_required_text,
+          "\n\n   Technical description:  #{metric.technical_description}"
+        ].compact.join(' ')
+
+        last_line_of_prompt = "\n  Finish the description:  #{format_info("#{metric.prefix}...")}"
+
+        cli.say("\n")
+        cli.say(multiline_prompt)
+
+        description_help_message = [
+          Text::METRIC_DESCRIPTION_HELP,
+          multiline_prompt,
+          "\n\n"
+        ].join("\n")
+
+        # Reassign base_description so the next metric's default value is their own input
+        base_description = cli.ask(last_line_of_prompt, value: base_description.to_s) do |q|
+          q.required true
+          q.modify :trim
+          q.messages[:required?] = description_help_message
+        end
+
+        cli.say("\n") # looks like multiline input, but isn't. Spacer improves clarity.
+
+        metric.description = "#{metric.prefix} #{base_description}"
+      end
+    end
+
+    def selected_event_descriptions
+      @selected_event_descriptions ||= selected_events.map do |event|
+        "  #{event.action} - #{format_selection(event.description)}\n"
+      end
+    end
+
+    # Check existing event files for attributes to copy over
+    def prompt_for_copying_event_properties
+      defaults = collect_values_for_shared_event_properties
+
+      return {} 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
+
+      defaults
+    end
+
+    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_section] << event.product_section
+        fields[:product_stage] << event.product_stage
+        fields[:product_group] << event.product_group
+        fields[:distribution] << event.distributions&.sort
+        fields[:tier] << 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
+
+    def prompt_for_product_ownership(defaults)
+      assign_shared_attrs(:product_section, :product_stage, :product_group) do
+        new_page!(6, 9, STEPS)
+
+        prompt_for_group_ownership(
+          {
+            product_section: 'Which section owns the metric?',
+            product_stage: 'Which stage owns the metric?',
+            product_group: 'Which group owns the metric?'
+          },
+          defaults.slice(:product_section, :product_stage, :product_group)
+        )
+      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(: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("SAVING FILE #{counter(idx, @metrics.length)}: #{metric.technical_description}\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.'
+
+      cli.say <<~TEXT
+        #{divider}
+        #{format_info('Done with metric definitions!')}
+
+        #{outcome}
+
+        #{divider}
+      TEXT
+
+      cli.select("How would you like to proceed?", **select_opts) do |menu|
+        menu.enum "."
+        menu.choice "View Usage -- look at code examples for #{@selected_event_paths.first}", -> do
+          UsageViewer.new(cli, @selected_event_paths.first, selected_events.first).run
+        end
+        menu.choice 'Exit', -> { cli.say Text::FEEDBACK_NOTICE }
+      end
+    end
+
+    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/text.rb b/scripts/internal_events/cli/text.rb
new file mode 100755
index 0000000000000000000000000000000000000000..4cb1cc233267843beaf4d9f3f987698e8dc4d225
--- /dev/null
+++ b/scripts/internal_events/cli/text.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+# Blocks of text rendered in CLI
+module InternalEventsCli
+  module Text
+    extend Helpers
+
+    CLI_INSTRUCTIONS = <<~TEXT.freeze
+      #{format_info('INSTRUCTIONS:')}
+      To start tracking usage of a feature...
+
+        1) Define event (using CLI)
+        2) Trigger event (from code)
+        3) Define metric (using CLI)
+        4) View data in Sisense (after merge & deploy)
+
+      This CLI will help you create the correct defintion files, then provide code examples for instrumentation and testing.
+
+      Learn more: https://docs.gitlab.com/ee/development/internal_analytics/#fundamental-concepts
+
+    TEXT
+
+    # TODO: Remove "NEW TOOL" comment after 3 months
+    FEEDBACK_NOTICE = format_heading <<~TEXT.chomp
+      Thanks for using the Internal Events CLI!
+
+      Please reach out with any feedback!
+        About Internal Events: https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/internal/-/issues/687
+        About CLI: https://gitlab.com/gitlab-org/gitlab/-/issues/434038
+        In Slack: #g_analyze_analytics_instrumentation
+
+      Let us know that you used the CLI! React with 👍 on the feedback issue or post in Slack!
+    TEXT
+
+    ALTERNATE_RESOURCES_NOTICE = <<~TEXT.freeze
+      Other resources:
+
+      #{format_warning('Tracking GitLab feature usage from database info:')}
+          https://docs.gitlab.com/ee/development/internal_analytics/metrics/metrics_instrumentation.html#database-metrics
+
+      #{format_warning('Migrating existing metrics to use Internal Events:')}
+          https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/migration.html
+
+      #{format_warning('Remove an existing metric:')}
+          https://docs.gitlab.com/ee/development/internal_analytics/metrics/metrics_lifecycle.html
+
+      #{format_warning('Finding existing usage data for GitLab features:')}
+          https://metrics.gitlab.com/ (Customize Table > Sisense query)
+          https://app.periscopedata.com/app/gitlab/1049395/Service-Ping-Exploration-Dashboard
+
+      #{format_warning('Customer wants usage data for their own GitLab instance:')}
+          https://docs.gitlab.com/ee/user/analytics/
+
+      #{format_warning('Customer wants usage data for their own products:')}
+          https://docs.gitlab.com/ee/user/product_analytics/
+    TEXT
+
+    EVENT_TRACKING_EXAMPLES = <<~TEXT
+      Product usage can be tracked in several ways.
+
+      By tracking events:     ex) a user changes the assignee on an issue
+                              ex) a user uploads a CI template
+                              ex) a service desk request is received
+                              ex) all stale runners are cleaned up
+                              ex) a user copies code to the clipboard from markdown
+                              ex) a user uploads an issue template OR a user uploads an MR template
+
+      From database data:     ex) track whether each gitlab instance allows signups
+                              ex) query how many projects are on each gitlab instance
+
+    TEXT
+
+    EVENT_EXISTENCE_CHECK_INSTRUCTIONS = <<~TEXT.freeze
+      To determine what to do next, let's figure out if the event is already tracked & usable.
+
+      If you're unsure whether an event exists, you can check the existing defintions.
+
+        #{format_info('FROM GDK')}: Check `config/events/` or `ee/config/events`
+        #{format_info('FROM BROWSER')}: Check https://metrics.gitlab.com/snowplow
+
+        Find one? Create a new metric for the event.
+        Otherwise? Create a new event.
+
+      If you find a relevant event that has a different category from 'InternalEventTracking', it can be migrated to
+      Internal Events. See https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/migration.html
+
+    TEXT
+
+    EVENT_DESCRIPTION_INTRO = <<~TEXT.freeze
+      #{format_info('EVENT DESCRIPTION')}
+      Include what the event is supposed to track, where, and when.
+
+      The description field helps others find & reuse this event. This will be used by Engineering, Product, Data team, Support -- and also GitLab customers directly. Be specific and explicit.
+        ex - Debian package published to the registry using a deploy token
+        ex - Issue confidentiality was changed
+
+    TEXT
+
+    EVENT_DESCRIPTION_HELP = <<~TEXT.freeze
+      #{format_warning('Required. 10+ words likely, but length may vary.')}
+
+      #{format_info('GOOD EXAMPLES:')}
+      - Pipeline is created with a CI Template file included in its configuration
+      - Quick action `/assign @user1` used to assign a single individual to an issuable
+      - Quick action `/target_branch` used on a Merge Request
+      - Quick actions `/unlabel` or `/remove_label` used to remove one or more specific labels
+      - User edits file using the single file editor
+      - User edits file using the Web IDE
+      - User removed issue link between issue and incident
+      - Debian package published to the registry using a deploy token
+
+      #{format_info('GUT CHECK:')}
+      For your description...
+        1. Would two different engineers likely instrument the event from the same code locations?
+        2. Would a new GitLab user find where the event is triggered in the product?
+        3. Would a GitLab customer understand what the description says?
+
+
+    TEXT
+
+    EVENT_ACTION_INTRO = <<~TEXT.freeze
+      #{format_info('EVENT NAME')}
+      The event name is a unique identifier used from both a) app code and b) metric definitions.
+      The name should concisely communicate the same information as the event description.
+
+        ex - change_time_estimate_on_issue
+        ex - push_package_to_repository
+        ex - publish_go_module_to_the_registry_from_pipeline
+        ex - admin_user_comments_on_issue_while_impersonating_blocked_user
+
+      #{format_info('EXPECTED FORMAT:')} #{format_selection('<action>_<target_of_action>_<where/when>')}
+
+        ex) click_save_button_in_issue_description_within_15s_of_page_load
+          - TARGET: save button
+          - ACTION: click
+          - WHERE: in issue description
+          - WHEN: within 15s of page load
+
+    TEXT
+
+    EVENT_ACTION_HELP = <<~TEXT.freeze
+      #{format_warning('Required. Must be globally unique. Must use only letters/numbers/underscores.')}
+
+      #{format_info('FAQs:')}
+      - Q: Present tense or past tense?
+        A: Prefer present tense! But it's up to you.
+      - Q: Other event names have prefixes like `i_` or the `g_group_name`. Why?
+        A: Those are leftovers from legacy naming schemes. Changing the names of old events/metrics can break dashboards, so stability is better than uniformity.
+
+
+    TEXT
+
+    EVENT_IDENTIFIERS_INTRO = <<~TEXT.freeze
+      #{format_info('EVENT CONTEXT')}
+      Identifies the attributes recorded when the event occurs. Generally, we want to include every identifier available to us when the event is triggered.
+
+      #{format_info('BACKEND')}: Attributes must be specified when the event is triggered
+        ex) If the backend event was instrumentuser/project/namespace are the identifiers for this backend instrumentation:
+
+          Gitlab::InternalEvents.track_event(
+            '%s',
+            user: user,
+            project: project,
+            namespace: project.namespace
+          )
+
+      #{format_info('FRONTEND')}: Attributes are automatically included from the URL
+        ex) When a user takes an action on the MR list page, the URL is https://gitlab.com/gitlab-org/gitlab/-/merge_requests
+            Because this URL is for a project, we know that all of user/project/namespace are available for the event
+
+      #{format_info('NOTE')}: If you're planning to instrument a unique-by-user metric, you should still include project & namespace when possible. This is especially helpful in the data warehouse, where namespace and project can make events relevant for CSM use-cases.
+
+    TEXT
+
+    DATABASE_METRIC_NOTICE = <<~TEXT
+
+      For right now, this script can only define metrics for internal events.
+
+      For more info on instrumenting database-backed metrics, see https://docs.gitlab.com/ee/development/internal_analytics/metrics/metrics_instrumentation.html
+    TEXT
+
+    ALL_METRICS_EXIST_NOTICE = <<~TEXT
+
+      Looks like the potential metrics for this event either already exist or are unsupported.
+
+      Check out https://metrics.gitlab.com/ for improved event/metric search capabilities.
+    TEXT
+
+    METRIC_DESCRIPTION_INTRO = <<~TEXT.freeze
+      #{format_info('METRIC DESCRIPTION')}
+      Describes which occurrences of an event are tracked in the metric and how they're grouped.
+
+      The description field is critical for helping others find & reuse this event. This will be used by Engineering, Product, Data team, Support -- and also GitLab customers directly. Be specific and explicit.
+
+      #{format_info('GOOD EXAMPLES:')}
+      - Total count of analytics dashboard list views
+      - Weekly count of unique users who viewed the analytics dashboard list
+      - Monthly count of unique projects where the analytics dashboard list was viewed
+      - Total count of issue updates
+
+      #{format_info('SELECTED EVENT(S):')}
+    TEXT
+
+    METRIC_DESCRIPTION_HELP = <<~TEXT.freeze
+      #{format_warning('Required. 10+ words likely, but length may vary.')}
+
+         An event description can often be rearranged to work as a metric description.
+
+         ex) Event description: A merge request was created
+             Metric description: Total count of merge requests created
+             Metric description: Weekly count of unqiue users who created merge requests
+
+         Look at the event descriptions above to get ideas!
+    TEXT
+  end
+end
diff --git a/scripts/internal_events/cli/usage_viewer.rb b/scripts/internal_events/cli/usage_viewer.rb
new file mode 100755
index 0000000000000000000000000000000000000000..6a9be38e25d780fa170bedf0eea894af4017b2a1
--- /dev/null
+++ b/scripts/internal_events/cli/usage_viewer.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+require_relative './helpers'
+
+module InternalEventsCli
+  class UsageViewer
+    include Helpers
+
+    IDENTIFIER_EXAMPLES = {
+      %w[namespace project user] => { "namespace" => "project.namespace" },
+      %w[namespace user] => { "namespace" => "group" }
+    }.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 = 'ruby/rails')
+      choices = [
+        { name: 'ruby/rails', value: :rails },
+        { name: 'rspec', value: :rspec },
+        { name: 'javascript (vue)', value: :vue },
+        { name: 'javascript (plain)', value: :js },
+        { name: 'vue template', value: :vue_template },
+        { name: 'haml', value: :haml },
+        { name: 'View examples for a different event', value: :other_event },
+        { name: 'Exit', value: :exit }
+      ]
+
+      usage_location = cli.select(
+        'Select a use-case to view examples for:',
+        choices,
+        **select_opts,
+        per_page: 10
+      ) do |menu|
+        menu.enum '.'
+        menu.default default
+      end
+
+      case usage_location
+      when :rails
+        rails_examples
+        prompt_for_usage_location('ruby/rails')
+      when :rspec
+        rspec_examples
+        prompt_for_usage_location('rspec')
+      when :haml
+        haml_examples
+        prompt_for_usage_location('haml')
+      when :js
+        js_examples
+        prompt_for_usage_location('javascript (plain)')
+      when :vue
+        vue_examples
+        prompt_for_usage_location('javascript (vue)')
+      when :vue_template
+        vue_template_examples
+        prompt_for_usage_location('vue template')
+      when :other_event
+        self.class.new(cli).run
+      when :exit
+        cli.say(Text::FEEDBACK_NOTICE)
+      end
+    end
+
+    def rails_examples
+      args = Array(event['identifiers']).map do |identifier|
+        "  #{identifier}: #{identifier_examples[identifier]}"
+      end
+      action = args.any? ? "\n  '#{event['action']}',\n" : "'#{event['action']}'"
+
+      cli.say format_warning <<~TEXT
+        #{divider}
+        #{format_help('# RAILS')}
+
+        Gitlab::InternalEvents.track_event(#{action}#{args.join(",\n")}#{"\n" unless args.empty?})
+
+        #{divider}
+      TEXT
+    end
+
+    def rspec_examples
+      cli.say format_warning <<~TEXT
+        #{divider}
+        #{format_help('# RSPEC')}
+
+        it_behaves_like 'internal event tracking' do
+          let(:event) { '#{event['action']}' }
+        #{
+          Array(event['identifiers']).map do |identifier|
+            "  let(:#{identifier}) { #{identifier_examples[identifier]} }\n"
+          end.join('')
+        }end
+
+        #{divider}
+      TEXT
+    end
+
+    def identifier_examples
+      event['identifiers']
+        .to_h { |identifier| [identifier, identifier] }
+        .merge(IDENTIFIER_EXAMPLES[event['identifiers'].sort] || {})
+    end
+
+    def haml_examples
+      cli.say <<~TEXT
+        #{divider}
+        #{format_help('# HAML -- ON-CLICK')}
+
+        .gl-display-inline-block{ #{format_warning("data: { event_tracking: '#{event['action']}' }")} }
+          = _('Important Text')
+
+        #{divider}
+        #{format_help('# HAML -- COMPONENT ON-CLICK')}
+
+        = render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { event_tracking: '#{event['action']}' }")} })
+
+        #{divider}
+        #{format_help('# HAML -- COMPONENT ON-LOAD')}
+
+        = render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { event_tracking_load: true, event_tracking: '#{event['action']}' }")} })
+
+        #{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
+      cli.say <<~TEXT
+        #{divider}
+        #{format_help('// VUE TEMPLATE -- ON-CLICK')}
+
+        <script>
+        import { GlButton } from '@gitlab/ui';
+
+        export default {
+          components: { GlButton }
+        };
+        </script>
+
+        <template>
+          <gl-button #{format_warning("data-event-tracking=\"#{event['action']}\"")}>
+            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 #{format_warning("data-event-tracking-load=\"#{event['action']}\"")}>
+            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
+      cli.say <<~TEXT
+        #{divider}
+        #{format_help('// FRONTEND -- RAW JAVASCRIPT')}
+
+        #{format_warning("import { InternalEvents } from '~/tracking';")}
+
+        export const performAction = () => {
+          #{format_warning("InternalEvents.trackEvent('#{event['action']}');")}
+
+          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
+      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('#{event['action']}');")}
+            },
+          },
+        };
+        </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
+  end
+end
diff --git a/spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml b/spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..07f606fbe33c15d3c012fd6ce092556e5bf843fd
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml
@@ -0,0 +1,14 @@
+---
+description: Internal Event CLI is opened
+category: InternalEventTracking
+action: internal_events_cli_opened
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+milestone: '16.6'
+introduced_by_url: TODO
+distributions:
+- ee
+tiers:
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml b/spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5050953920dec4b1297b2c7b62d03967dad43b34
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
@@ -0,0 +1,20 @@
+---
+description: Engineer uses Internal Event CLI to define a new event
+category: InternalEventTracking
+action: internal_events_cli_used
+identifiers:
+- project
+- namespace
+- user
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+milestone: '16.6'
+introduced_by_url: TODO
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml b/spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c0ccbc03af762b9d50f0ffbc4be73c85392fda90
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml
@@ -0,0 +1,20 @@
+---
+description: random event string
+category: InternalEventTracking
+action: random_name
+identifiers:
+- project
+- namespace
+- user
+product_section: core_platform
+product_stage: manage
+product_group: import_and_integrate
+milestone: '16.6'
+introduced_by_url: TODO
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml b/spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4e2e77e0c5c9d020d1fe94c75a8812a2a52a78f8
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml
@@ -0,0 +1,20 @@
+---
+description: Engineer closes Internal Event CLI
+category: InternalEventTracking
+action: internal_events_cli_closed
+identifiers:
+- project
+- namespace
+- user
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+milestone: '16.6'
+introduced_by_url: TODO
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ba56d7828714fe95dd4b0681fd7d5556b743694a
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml
@@ -0,0 +1,25 @@
+---
+key_path: counts.count_total_internal_events_cli_used_monthly
+description: Monthly count of when an event was defined using the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+instrumentation_class: TotalCountMetric
+distribution:
+- ee
+tier:
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_used
diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e6bdcb9d2ae34dc85992ee377637ef834968991e
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml
@@ -0,0 +1,25 @@
+---
+key_path: counts.count_total_internal_events_cli_used_weekly
+description: Weekly count of when an event was defined using the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+instrumentation_class: TotalCountMetric
+distribution:
+- ee
+tier:
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_used
diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b1bf89dc09567ab6fd225e2757c7929e096901e0
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
@@ -0,0 +1,25 @@
+---
+key_path: counts.count_total_internal_events_cli_used
+description: Total count of when an event was defined using the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: all
+data_source: internal_events
+data_category: optional
+instrumentation_class: TotalCountMetric
+distribution:
+- ee
+tier:
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_used
diff --git a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8476cb8561b9b78bd11a933f7bff58c51510f2a4
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
@@ -0,0 +1,28 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_random_name_monthly
+description: Monthly count of unique users random metric string
+product_section: core_platform
+product_stage: manage
+product_group: import_and_integrate
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - random_name
+events:
+- name: random_name
+  unique: user.id
diff --git a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b4cc2fc8b55aabd3ec460d41d8a3ce5ebbdb55a0
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
@@ -0,0 +1,28 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_random_name_weekly
+description: Weekly count of unique users random metric string
+product_section: core_platform
+product_stage: manage
+product_group: import_and_integrate
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - random_name
+events:
+- name: random_name
+  unique: user.id
diff --git a/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml b/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
new file mode 100644
index 0000000000000000000000000000000000000000..754702c8c7497df90d555a9a31a5bec110db1dc3
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
@@ -0,0 +1,31 @@
+---
+key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly
+description: Monthly count of unique projects where a defition file was created with the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_closed
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_closed
+  unique: project.id
+- name: internal_events_cli_used
+  unique: project.id
diff --git a/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml b/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
new file mode 100644
index 0000000000000000000000000000000000000000..95f429e9b40e78fc332edde2f402b4bb7c23aa69
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
@@ -0,0 +1,31 @@
+---
+key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly
+description: Weekly count of unique projects where a defition file was created with the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_closed
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_closed
+  unique: project.id
+- name: internal_events_cli_used
+  unique: project.id
diff --git a/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5bdb4c45a5238f36ad8d11c9f22b45fccb5e175a
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
@@ -0,0 +1,27 @@
+---
+key_path: counts.count_total_internal_events_cli_used
+description: Total count of when an event was defined using the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: all
+data_source: internal_events
+data_category: optional
+instrumentation_class: TotalCountMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_used
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b176b23b46af601e046c89dd64bb845794143849
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
@@ -0,0 +1,28 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_monthly
+description: Monthly count of unique users who defined an internal event using the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_used
+  unique: user.id
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8a0fca2cbdc88164e2a069b7a941bca6f8b39525
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
@@ -0,0 +1,28 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_weekly
+description: Weekly count of unique users who defined an internal event using the CLI
+product_section: analytics
+product_stage: monitor
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+  events:
+  - internal_events_cli_used
+events:
+- name: internal_events_cli_used
+  unique: user.id
diff --git a/spec/fixtures/scripts/internal_events/new_events.yml b/spec/fixtures/scripts/internal_events/new_events.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6f39fc5e93cefadc931fe920cf72a73228d1ff7f
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/new_events.yml
@@ -0,0 +1,183 @@
+- description: Creates a new event and flows directly into metric creation
+  inputs:
+    keystrokes:
+    - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "Engineer uses Internal Event CLI to define a new event\n" # Submit description
+    - "internal_events_cli_used\n" # Submit action name
+    - "1\n" # Select: [namespace, project, user]
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "1\n" # Select: [free, premium, ultimate]
+    - "y\n" # Create file
+    - "1\n" # Select: Create Metric --- define a new metric
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "1\n" # Select: Copy & continue
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
+
+- description: Requires description & action before continuing
+  inputs:
+    keystrokes:
+    - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "\n" # Attempt to skip writing description --> should get help message
+    - "Engineer uses Internal Event CLI to define a new event\n" # Submit description
+    - "\n" # Attempt to skip naming action --> should get help message
+    - "internal_events_cli_used\n" # Submit action name
+    - "1\n" # Select [namespace, project, user]
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "1\n" # Select [free, premium, ultimate]
+    - "y\n" # Create file
+    - "3\n" # Exit
+  outputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+
+- description: Does not allow existing events for action
+  inputs:
+    files:
+      - path: config/events/internal_events_cli_used.yml
+        content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    keystrokes:
+    - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "Engineer closes Internal Event CLI\n" # Submit description
+    - "internal_events_cli_used\n" # Submit already-existing action name
+    - "internal_events_cli_closed\n" # Submit alterred action name
+    - "1\n" # Select [namespace, project, user]
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "1\n" # Select [free, premium, ultimate]
+    - "y\n" # Create file
+    - "3\n" # Exit
+  outputs:
+    files:
+    - path: config/events/internal_events_cli_closed.yml
+      content: spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml
+
+- description: Creates a new event without identifiers
+  inputs:
+    keystrokes:
+    - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "Internal Event CLI is opened\n" # Submit description
+    - "internal_events_cli_opened\n" # Submit action name
+    - "6\n" # Select: None
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "2\n" # Select [premium, ultimate]
+    - "y\n" # Create file
+    - "3\n" # Exit
+  outputs:
+    files:
+    - path: ee/config/events/internal_events_cli_opened.yml
+      content: spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml
+
+- description: Smashing the keyboard/return creates an event & metrics with the most common attributes, then shows usage
+  inputs:
+    keystrokes:
+    - "\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "random event string\n" # Submit keyboard-smashing description
+    - "random_name\n" # Submit keyboard-smashing action name
+    - "\n" # Select: [namespace, project, user]
+    - "\n" # Skip MR URL
+    - "\n" # Select core_platform:manage:import_and_integrate
+    - "\n" # Select [free, premium, ultimate]
+    - "\n" # Create file
+    - "\n" # Select: Create Metric --- define a new metric
+    - "\n" # Select: Weekly/Monthly count of unique users
+    - "random metric string\n" # Submit keyboard-smashing description
+    - "\n" # Accept weekly description for monthly
+    - "\n" # Select: Copy & continue
+    - "\n" # Skip URL
+    - "\n" # Create file
+    - "\n" # Create file
+    - "\n" # Select: View Usage -- look at code examples
+    - "\n" # Select: Ruby/Rails
+    - "8\n" # Exit
+  outputs:
+    files:
+    - path: config/events/random_name.yml
+      content: spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml
+    - path: config/metrics/counts_28d/count_distinct_user_id_from_random_name_monthly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
+    - path: config/metrics/counts_7d/count_distinct_user_id_from_random_name_weekly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
+
+- description: Creates an event after helping the user figure out next steps
+  inputs:
+    keystrokes:
+    - "4\n" # Enum-select: ...am I in the right place?
+    - "y\n" # Yes --> Are you trying to track customer usage of a GitLab feature?
+    - "y\n" # Yes --> Can usage for the feature be measured by tracking a specific user action?
+    - "n\n" # No --> Is the event already tracked?
+    - "y\n" # Yes --> Ready to start?
+    - "Internal Event CLI is opened\n" # Submit description
+    - "internal_events_cli_opened\n" # Submit action name
+    - "6\n" # Select: None
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "2\n" # Select [premium, ultimate]
+    - "y\n" # Create file
+    - "3\n" # Exit
+  outputs:
+    files:
+    - path: ee/config/events/internal_events_cli_opened.yml
+      content: spec/fixtures/scripts/internal_events/events/ee_event_without_identifiers.yml
+
+- description: Creates a new event and flows directly into usage examples
+  inputs:
+    keystrokes:
+    - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "Engineer uses Internal Event CLI to define a new event\n" # Submit description
+    - "internal_events_cli_used\n" # Submit action name
+    - "1\n" # Select: [namespace, project, user]
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "1\n" # Select: [free, premium, ultimate]
+    - "y\n" # Create file
+    - "2\n" # Select: View Usage
+    - "8\n" # Exit
+  outputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+
+- description: Skips event creation, then saves event & flows directly into metric creation
+  inputs:
+    keystrokes:
+    - "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+    - "Engineer uses Internal Event CLI to define a new event\n" # Submit description
+    - "internal_events_cli_used\n" # Submit action name
+    - "1\n" # Select: [namespace, project, user]
+    - "\n" # Skip MR URL
+    - "instrumentation" # Filters to the analytics instrumentation group
+    - "\n" # Accept analytics:monitor:analytics_instrumentation
+    - "1\n" # Select: [free, premium, ultimate]
+    - "n\n" # Create file
+    - "1\n" # Select: Save event & create Metric --- define a new metric
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "1\n" # Select: Copy & continue
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
diff --git a/spec/fixtures/scripts/internal_events/new_metrics.yml b/spec/fixtures/scripts/internal_events/new_metrics.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2a207ee84f407a56e033d1aa64c7ee98bb85d266
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/new_metrics.yml
@@ -0,0 +1,196 @@
+- description: Create a weekly/monthly metric for a single event
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    keystrokes:
+    - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+    - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+    - 'internal_events_cli_used' # Filters to this event
+    - "\n" # Select: config/events/internal_events_cli_used.yml
+    - "\n" # Select: Weekly count of unique users
+    - "who defined an internal event using the CLI\n" # Input description
+    - "\n" # Submit weekly description for monthly
+    - "1\n" # Enum-select: Copy & continue
+    - "y\n" # Create file
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
+    - path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
+
+- description: Create a weekly/monthly metric for a multiple events, but select only one event
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "2\n" # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+    - 'internal_events_cli_used' # Filters to this event
+    - " " # Multi-select: config/events/internal_events_cli_used.yml
+    - "\n" # Submit selections
+    - "\n" # Select: Weekly count of unique projects
+    - "who defined an internal event using the CLI\n" # Input description
+    - "\n" # Submit weekly description for monthly
+    - "1\n" # Enum-select: Copy & continue
+    - "y\n" # Create file
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
+    - path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
+
+- description: Create a weekly/monthly metric for multiple events
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    - path: config/events/internal_events_cli_closed.yml
+      content: spec/fixtures/scripts/internal_events/events/secondary_event_with_identifiers.yml
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "2\n" # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+    - 'internal_events_cli' # Filters to the relevant events
+    - ' ' # Multi-select: internal_events_cli_closed
+    - "\e[B" # Arrow down to: internal_events_cli_used
+    - ' ' # Multi-select: internal_events_cli_used
+    - "\n" # Submit selections
+    - "\e[B" # Arrow down to: Weekly count of unique projects
+    - "\n" # Select: Weekly count of unique projects
+    - "where a defition file was created with the CLI\n" # Input description
+    - "\n" # Submit weekly description for monthly
+    - "1\n" # Select: Copy & continue
+    - "y\n" # Create file
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
+    - path: config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml
+      content: spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
+
+- description: Create an all time total metric for a single event
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+    - 'internal_events_cli_used' # Filters to this event
+    - "\n" # Select: config/events/internal_events_cli_used.yml
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "1\n" # Select: Copy & continue
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
+
+- description: Try to create a database metric
+  inputs:
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "3\n" # Enum-select: Database -- record value of a particular field or count of database rows
+
+- description: Create an all time total metric for a single event, and confirm each attribute copied from event
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+    - 'internal_events_cli_used' # Filters to this event
+    - "\n" # Select: config/events/internal_events_cli_used.yml
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "2\n" # Enum-select: Modify attributes
+    - "\n" # Accept group/section/stage from event definition
+    - "\n" # Accept URL from event definition
+    - "2\n" # Override tier -> Select: [premium, ultimate]
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: ee/config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
+
+- description: Create a metric after helping the user figure out next steps
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    keystrokes:
+    - "4\n" # Enum-select: ...am I in the right place?
+    - "y\n" # Yes --> Are you trying to track customer usage of a GitLab feature?
+    - "y\n" # Yes --> Can usage for the feature be measured by tracking a specific user action?
+    - "y\n" # Yes --> Is the event already tracked?
+    - "y\n" # Yes --> Ready to start?
+    - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+    - 'internal_events_cli_used' # Filters to this event
+    - "\n" # Select: config/events/internal_events_cli_used.yml
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "1\n" # Select: Copy & continue
+    - "y\n" # Create file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
+
+- description: User overwrites metric that already exists
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml # wrong content
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+    - 'internal_events_cli_used' # Filters to this event
+    - "\n" # Select: config/events/internal_events_cli_used.yml
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "1\n" # Select: Copy & continue
+    - "y\n" # Overwrite file
+    - "2\n" # Exit
+  outputs:
+    files:
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
+
+- description: User opts not to overwrite metric that already exists
+  inputs:
+    files:
+    - path: config/events/internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
+    - path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
+      content: spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml # wrong content
+    keystrokes:
+    - "2\n" # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+    - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+    - 'internal_events_cli_used' # Filters to this event
+    - "\n" # Select: config/events/internal_events_cli_used.yml
+    - "\e[A" # Arrow up to: Total count of events
+    - "\n" # Select: Total count of events
+    - "when an event was defined using the CLI\n" # Input description
+    - "1\n" # Select: Copy & continue
+    - "n\n" # Don't overwrite file
+    - "2\n" # Exit
diff --git a/spec/fixtures/scripts/internal_events/stages.yml b/spec/fixtures/scripts/internal_events/stages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d5db9dcbe6d308326208ab95ad87d86c96cd2809
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/stages.yml
@@ -0,0 +1,78 @@
+stages:
+  manage:
+    display_name: "Manage"
+    section: core_platform
+    groups:
+      import_and_integrate:
+        name: Import and Integrate
+      foundations:
+        name: Foundations
+
+  plan:
+    display_name: "Plan"
+    section: dev
+    groups:
+      project_management:
+        name: Project Management
+      product_planning:
+        name: Product Planning
+      knowledge:
+        name: Knowledge
+      optimize:
+        name: Optimize
+
+  create:
+    display_name: "Create"
+    section: dev
+    slack:
+      channel: s_create
+    groups:
+      source_code:
+        name: Source Code
+      code_review:
+        name: Code Review
+      ide:
+        name: IDE
+      editor_extensions:
+        name: Editor Extensions
+      code_creation:
+        name: Code Creation
+
+  verify:
+    display_name: "Verify"
+    section: ci
+    slack:
+      channel: s_verify
+    groups:
+      pipeline_execution:
+        name: "Pipeline Execution"
+      pipeline_authoring:
+        name: "Pipeline Authoring"
+      runner:
+        name: "Runner"
+      runner_saas:
+        name: "Runner SaaS"
+      pipeline_security:
+        name: "Pipeline Security"
+
+  package:
+    display_name: "Package"
+    section: ci
+    slack:
+      channel: s_package
+    groups:
+      package_registry:
+        name: Package Registry
+      container_registry:
+        name: Container Registry
+
+  monitor:
+    display_name: Monitor
+    section: analytics
+    groups:
+      analytics_instrumentation:
+        name: Analytics Instrumentation
+      product_analytics:
+        name: Product Analytics
+      observability:
+        name: "Observability"
diff --git a/spec/scripts/internal_events/cli_spec.rb b/spec/scripts/internal_events/cli_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d84a4498fe8f6d7f2898fa24d7814d31a6bd1d41
--- /dev/null
+++ b/spec/scripts/internal_events/cli_spec.rb
@@ -0,0 +1,866 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'tty/prompt/test'
+require_relative '../../../scripts/internal_events/cli'
+
+RSpec.describe Cli, feature_category: :service_ping do
+  let(:prompt) { TTY::Prompt::Test.new }
+  let(:files_to_cleanup) { [] }
+
+  let(:event1_filepath) { 'config/events/internal_events_cli_used.yml' }
+  let(:event1_content) { internal_event_fixture('events/event_with_identifiers.yml') }
+  let(:event2_filepath) { 'ee/config/events/internal_events_cli_opened.yml' }
+  let(:event2_content) { internal_event_fixture('events/ee_event_without_identifiers.yml') }
+  let(:event3_filepath) { 'config/events/internal_events_cli_closed.yml' }
+  let(:event3_content) { internal_event_fixture('events/secondary_event_with_identifiers.yml') }
+
+  before do
+    stub_milestone('16.6')
+    collect_file_writes(files_to_cleanup)
+    stub_product_groups(File.read('spec/fixtures/scripts/internal_events/stages.yml'))
+    stub_helper(:fetch_window_size, '50')
+  end
+
+  after do
+    delete_files(files_to_cleanup)
+  end
+
+  shared_examples 'creates the right defintion files' do |description, test_case = {}|
+    # For expected keystroke mapping, see https://github.com/piotrmurach/tty-reader/blob/master/lib/tty/reader/keys.rb
+    let(:keystrokes) { test_case.dig('inputs', 'keystrokes') || [] }
+    let(:input_files) { test_case.dig('inputs', 'files') || [] }
+    let(:output_files) { test_case.dig('outputs', 'files') || [] }
+
+    subject { run_with_verbose_timeout }
+
+    it "in scenario: #{description}" do
+      delete_old_ouputs # just in case
+      prep_input_files
+      queue_cli_inputs(keystrokes)
+      expect_file_creation
+
+      subject
+    end
+
+    private
+
+    def delete_old_ouputs
+      [input_files, output_files].flatten.each do |file_info|
+        FileUtils.rm_f(Rails.root.join(file_info['path']))
+      end
+    end
+
+    def prep_input_files
+      input_files.each do |file|
+        File.write(
+          Rails.root.join(file['path']),
+          File.read(Rails.root.join(file['content']))
+        )
+      end
+    end
+
+    def expect_file_creation
+      if output_files.any?
+        output_files.each do |file|
+          expect(File).to receive(:write).with(file['path'], File.read(file['content']))
+        end
+      else
+        expect(File).not_to receive(:write)
+      end
+    end
+  end
+
+  context 'when creating new events' do
+    YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_events.yml')).each do |test_case|
+      it_behaves_like 'creates the right defintion files', test_case['description'], test_case
+    end
+  end
+
+  context 'when creating new metrics' do
+    YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_metrics.yml')).each do |test_case|
+      it_behaves_like 'creates the right defintion files', test_case['description'], test_case
+    end
+
+    context 'when creating a metric from multiple events' do
+      let(:events) do
+        [{
+          action: '00_event1', category: 'InternalEventTracking',
+          product_section: 'dev', product_stage: 'plan', product_group: 'optimize'
+        }, {
+          action: '00_event2', category: 'InternalEventTracking',
+          product_section: 'dev', product_stage: 'create', product_group: 'ide'
+        }, {
+          action: '00_event3', category: 'InternalEventTracking',
+          product_section: 'dev', product_stage: 'create', product_group: 'source_code'
+        }]
+      end
+
+      before do
+        events.each do |event|
+          File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml)
+        end
+      end
+
+      it 'filters the product group options based on common section' do
+        # Select 00_event1 & #00_event2
+        queue_cli_inputs([
+          "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+          "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+          " ", # Multi-select: __event1
+          "\e[B", # Arrow down to: __event2
+          " ", # Multi-select: __event2
+          "\n", # Submit selections
+          "\n", # Select: Weekly/Monthly count of unique users
+          "aggregate metric description\n", # Submit description
+          "\n", # Accept description for weekly
+          "\n" # Copy & continue
+        ])
+
+        run_with_timeout
+
+        # Filter down to "dev" options
+        expect(plain_last_lines(9)).to eq <<~TEXT.chomp
+        ‣ dev:plan:project_management
+          dev:plan:product_planning
+          dev:plan:knowledge
+          dev:plan:optimize
+          dev:create:source_code
+          dev:create:code_review
+          dev:create:ide
+          dev:create:editor_extensions
+          dev:create:code_creation
+        TEXT
+      end
+
+      it 'filters the product group options based on common section & stage' do
+        # Select 00_event2 & #00_event3
+        queue_cli_inputs([
+          "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+          "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+          "\e[B", # Arrow down to: __event2
+          " ", # Multi-select: __event2
+          "\e[B", # Arrow down to: __event3
+          " ", # Multi-select: __event3
+          "\n", # Submit selections
+          "\n", # Select: Weekly/Monthly count of unique users
+          "aggregate metric description\n", # Submit description
+          "\n", # Accept description for weekly
+          "\n" # Copy & continue
+        ])
+
+        run_with_timeout
+
+        # Filter down to "dev:create" options
+        expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+        ‣ dev:create:source_code
+          dev:create:code_review
+          dev:create:ide
+          dev:create:editor_extensions
+          dev:create:code_creation
+        TEXT
+      end
+    end
+
+    context 'when product group for event no longer exists' do
+      let(:event) do
+        {
+          action: '00_event1', category: 'InternalEventTracking',
+          product_section: 'other', product_stage: 'other', product_group: 'other'
+        }
+      end
+
+      before do
+        File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml)
+      end
+
+      it 'prompts user to select another group' do
+        queue_cli_inputs([
+          "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+          "1\n", # Enum-select: Single event    -- count occurrences of a specific event or user interaction
+          "\n", # Select: 00__event1
+          "\n", # Select: Weekly/Monthly count of unique users
+          "aggregate metric description\n", # Submit description
+          "\n", # Accept description for weekly
+          "2\n" # Modify attributes
+        ])
+
+        run_with_timeout
+
+        # Filter down to "dev" options
+        expect(plain_last_lines(50)).to include 'Select one: Which group owns the metric?'
+      end
+    end
+
+    context 'when creating a metric for an event which has metrics' do
+      before do
+        File.write(event1_filepath, File.read(event1_content))
+      end
+
+      it 'shows all metrics options' do
+        select_event_from_list
+
+        expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+        ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
+          Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
+          Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
+          Monthly/Weekly count of [internal_events_cli_used occurrences]
+          Total count of [internal_events_cli_used occurrences]
+        TEXT
+      end
+
+      context 'with an existing weekly metric' do
+        before do
+          File.write(
+            'ee/config/metrics/counts_7d/count_total_internal_events_cli_used_weekly.yml',
+            File.read('spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml')
+          )
+        end
+
+        it 'partially filters metric options' do
+          select_event_from_list
+
+          expect(plain_last_lines(6)).to eq <<~TEXT.chomp
+          ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
+            Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
+            Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
+            Monthly count of [internal_events_cli_used occurrences]
+          ✘ Weekly count of [internal_events_cli_used occurrences] (already defined)
+            Total count of [internal_events_cli_used occurrences]
+          TEXT
+        end
+      end
+
+      context 'with an existing total metric' do
+        before do
+          File.write(
+            'ee/config/metrics/counts_all/count_total_internal_events_cli_used.yml',
+            File.read('spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml')
+          )
+        end
+
+        it 'filters whole metric options' do
+          select_event_from_list
+
+          expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+          ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
+            Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
+            Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
+            Monthly/Weekly count of [internal_events_cli_used occurrences]
+          ✘ Total count of [internal_events_cli_used occurrences] (already defined)
+          TEXT
+        end
+      end
+
+      private
+
+      def select_event_from_list
+        queue_cli_inputs([
+          "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+          "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+          'internal_events_cli_used', # Filters to this event
+          "\n" # Select: config/events/internal_events_cli_used.yml
+        ])
+
+        run_with_timeout
+      end
+    end
+
+    context 'when event excludes identifiers' do
+      before do
+        File.write(event2_filepath, File.read(event2_content))
+      end
+
+      it 'filters unavailable identifiers' do
+        queue_cli_inputs([
+          "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+          "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+          'internal_events_cli_opened', # Filters to this event
+          "\n" # Select: config/events/internal_events_cli_opened.yml
+        ])
+
+        run_with_timeout
+
+        expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+        ✘ Monthly/Weekly count of unique users [who triggered internal_events_cli_opened] (user unavailable)
+        ✘ Monthly/Weekly count of unique projects [where internal_events_cli_opened occurred] (project unavailable)
+        ✘ Monthly/Weekly count of unique namespaces [where internal_events_cli_opened occurred] (namespace unavailable)
+        ‣ Monthly/Weekly count of [internal_events_cli_opened occurrences]
+          Total count of [internal_events_cli_opened occurrences]
+        TEXT
+      end
+    end
+
+    context 'when all metrics already exist' do
+      let(:event) { { action: '00_event1', category: 'InternalEventTracking' } }
+      let(:metric) { { options: { 'events' => ['00_event1'] }, events: [{ 'name' => '00_event1' }] } }
+
+      let(:files) do
+        [
+          ['config/events/00_event1.yml', event],
+          ['config/metrics/counts_all/count_total_00_event1.yml', metric.merge(time_frame: 'all')],
+          ['config/metrics/counts_7d/count_total_00_event1_weekly.yml', metric.merge(time_frame: '7d')],
+          ['config/metrics/counts_28d/count_total_00_event1_monthly.yml', metric.merge(time_frame: '28d')]
+        ]
+      end
+
+      before do
+        files.each do |path, content|
+          File.write(path, content.transform_keys(&:to_s).to_yaml)
+        end
+      end
+
+      it 'exits the script and directs user to search for existing metrics' do
+        queue_cli_inputs([
+          "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+          "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+          '00_event1', # Filters to this event
+          "\n" # Select: config/events/00_event1.yml
+        ])
+
+        run_with_timeout
+
+        expect(plain_last_lines(15)).to include 'Looks like the potential metrics for this event ' \
+                                                'either already exist or are unsupported.'
+      end
+    end
+  end
+
+  context 'when showing usage examples' do
+    let(:expected_example_prompt) do
+      <<~TEXT.chomp
+      Select one: Select a use-case to view examples for: (Press ↑/↓ arrow or 1-8 number to move and Enter to select)
+      ‣ 1. ruby/rails
+        2. rspec
+        3. javascript (vue)
+        4. javascript (plain)
+        5. vue template
+        6. haml
+        7. View examples for a different event
+        8. Exit
+      TEXT
+    end
+
+    context 'for an event with identifiers' do
+      let(:expected_rails_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # RAILS
+
+        Gitlab::InternalEvents.track_event(
+          'internal_events_cli_used',
+          project: project,
+          namespace: project.namespace,
+          user: user
+        )
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_rspec_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # RSPEC
+
+        it_behaves_like 'internal event tracking' do
+          let(:event) { 'internal_events_cli_used' }
+          let(:project) { project }
+          let(:namespace) { project.namespace }
+          let(:user) { user }
+        end
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      before do
+        File.write(event1_filepath, File.read(event1_content))
+      end
+
+      it 'shows backend examples' do
+        queue_cli_inputs([
+          "3\n", # Enum-select: View Usage -- look at code examples for an existing event
+          'internal_events_cli_used', # Filters to this event
+          "\n", # Select: config/events/internal_events_cli_used.yml
+          "\n", # Select: ruby/rails
+          "\e[B", # Arrow down to: rspec
+          "\n", # Select: rspec
+          "8\n" # Exit
+        ])
+
+        run_with_timeout
+
+        output = plain_last_lines(100)
+
+        expect(output).to include expected_example_prompt
+        expect(output).to include expected_rails_example
+        expect(output).to include expected_rspec_example
+      end
+    end
+
+    context 'for an event without identifiers' do
+      let(:expected_rails_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # RAILS
+
+        Gitlab::InternalEvents.track_event('internal_events_cli_opened')
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_rspec_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # RSPEC
+
+        it_behaves_like 'internal event tracking' do
+          let(:event) { 'internal_events_cli_opened' }
+        end
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_vue_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        // VUE
+
+        <script>
+        import { InternalEvents } from '~/tracking';
+        import { GlButton } from '@gitlab/ui';
+
+        const trackingMixin = InternalEvents.mixin();
+
+        export default {
+          mixins: [trackingMixin],
+          components: { GlButton },
+          methods: {
+            performAction() {
+              this.trackEvent('internal_events_cli_opened');
+            },
+          },
+        };
+        </script>
+
+        <template>
+          <gl-button @click=performAction>Click Me</gl-button>
+        </template>
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_js_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        // FRONTEND -- RAW JAVASCRIPT
+
+        import { InternalEvents } from '~/tracking';
+
+        export const performAction = () => {
+          InternalEvents.trackEvent('internal_events_cli_opened');
+
+          return true;
+        };
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_vue_template_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        // VUE TEMPLATE -- ON-CLICK
+
+        <script>
+        import { GlButton } from '@gitlab/ui';
+
+        export default {
+          components: { GlButton }
+        };
+        </script>
+
+        <template>
+          <gl-button data-event-tracking="internal_events_cli_opened">
+            Click Me
+          </gl-button>
+        </template>
+
+        --------------------------------------------------
+        // VUE TEMPLATE -- ON-LOAD
+
+        <script>
+        import { GlButton } from '@gitlab/ui';
+
+        export default {
+          components: { GlButton }
+        };
+        </script>
+
+        <template>
+          <gl-button data-event-tracking-load="internal_events_cli_opened">
+            Click Me
+          </gl-button>
+        </template>
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_haml_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # HAML -- ON-CLICK
+
+        .gl-display-inline-block{ data: { event_tracking: 'internal_events_cli_opened' } }
+          = _('Important Text')
+
+        --------------------------------------------------
+        # HAML -- COMPONENT ON-CLICK
+
+        = render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking: 'internal_events_cli_opened' } })
+
+        --------------------------------------------------
+        # HAML -- COMPONENT ON-LOAD
+
+        = render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking_load: true, event_tracking: 'internal_events_cli_opened' } })
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      before do
+        File.write(event2_filepath, File.read(event2_content))
+      end
+
+      it 'shows all examples' do
+        queue_cli_inputs([
+          "3\n", # Enum-select: View Usage -- look at code examples for an existing event
+          'internal_events_cli_opened', # Filters to this event
+          "\n", # Select: config/events/internal_events_cli_used.yml
+          "\n", # Select: ruby/rails
+          "\e[B", # Arrow down to: rspec
+          "\n", # Select: rspec
+          "\e[B", # Arrow down to: js vue
+          "\n", # Select: js vue
+          "\e[B", # Arrow down to: js plain
+          "\n", # Select: js plain
+          "\e[B", # Arrow down to: vue template
+          "\n", # Select: vue template
+          "\e[B", # Arrow down to: haml
+          "\n", # Select: haml
+          "8\n" # Exit
+        ])
+
+        run_with_timeout
+
+        output = plain_last_lines(1000)
+
+        expect(output).to include expected_example_prompt
+        expect(output).to include expected_rails_example
+        expect(output).to include expected_rspec_example
+        expect(output).to include expected_vue_example
+        expect(output).to include expected_js_example
+        expect(output).to include expected_vue_template_example
+        expect(output).to include expected_haml_example
+      end
+    end
+
+    context 'when viewing examples for multiple events' do
+      let(:expected_event1_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # RAILS
+
+        Gitlab::InternalEvents.track_event(
+          'internal_events_cli_used',
+          project: project,
+          namespace: project.namespace,
+          user: user
+        )
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      let(:expected_event2_example) do
+        <<~TEXT.chomp
+        --------------------------------------------------
+        # RAILS
+
+        Gitlab::InternalEvents.track_event('internal_events_cli_opened')
+
+        --------------------------------------------------
+        TEXT
+      end
+
+      before do
+        File.write(event1_filepath, File.read(event1_content))
+        File.write(event2_filepath, File.read(event2_content))
+      end
+
+      it 'switches between events gracefully' do
+        queue_cli_inputs([
+          "3\n", # Enum-select: View Usage -- look at code examples for an existing event
+          'internal_events_cli_used', # Filters to this event
+          "\n", # Select: config/events/internal_events_cli_used.yml
+          "\n", # Select: ruby/rails
+          "7\n", # Select: View examples for a different event
+          'internal_events_cli_opened', # Filters to this event
+          "\n", # Select: config/events/internal_events_cli_opened.yml
+          "\n", # Select: ruby/rails
+          "8\n" # Exit
+        ])
+
+        run_with_timeout
+
+        output = plain_last_lines(300)
+
+        expect(output).to include expected_example_prompt
+        expect(output).to include expected_event1_example
+        expect(output).to include expected_event2_example
+      end
+    end
+  end
+
+  context 'when offline' do
+    before do
+      stub_product_groups(nil)
+    end
+
+    it_behaves_like 'creates the right defintion files',
+      'Creates a new event with product stage/section/group input manually' do
+      let(:keystrokes) do
+        [
+          "1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+          "Internal Event CLI is opened\n", # Submit description
+          "internal_events_cli_opened\n", # Submit action name
+          "6\n", # Select: None
+          "\n", # Skip MR URL
+          "analytics\n", # Input section
+          "monitor\n", # Input stage
+          "analytics_instrumentation\n", # Input group
+          "2\n", # Select [premium, ultimate]
+          "y\n", # Create file
+          "3\n" # Exit
+        ]
+      end
+
+      let(:output_files) { [{ 'path' => event2_filepath, 'content' => event2_content }] }
+    end
+
+    it_behaves_like 'creates the right defintion files',
+      'Creates a new metric with product stage/section/group input manually' do
+      let(:keystrokes) do
+        [
+          "2\n", # Enum-select: New Metric   -- calculate how often one or more existing events occur over time
+          "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+          'internal_events_cli', # Filters to the relevant events
+          ' ', # Multi-select: internal_events_cli_closed
+          "\e[B", # Arrow down to: internal_events_cli_used
+          ' ', # Multi-select: internal_events_cli_used
+          "\n", # Submit selections
+          "\e[B", # Arrow down to: Weekly count of unique projects
+          "\n", # Select: Weekly count of unique projects
+          "where a defition file was created with the CLI\n", # Input description
+          "\n", # Submit weekly description for monthly
+          "2\n", # Select: Modify attributes
+          "\n", # Accept section
+          "\n", # Accept stage
+          "\n", # Accept group
+          "\n", # Skip URL
+          "1\n", # Select: [free, premium, ultimate]
+          "y\n", # Create file
+          "y\n", # Create file
+          "2\n" # Exit
+        ]
+      end
+
+      let(:input_files) do
+        [
+          { 'path' => event1_filepath, 'content' => event1_content },
+          { 'path' => event3_filepath, 'content' => event3_content }
+        ]
+      end
+
+      let(:output_files) do
+        # rubocop:disable Layout/LineLength -- Long filepaths read better unbroken
+        [{
+          'path' => 'config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml',
+          'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml'
+        }, {
+          'path' => 'config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml',
+          'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml'
+        }]
+        # rubocop:enable Layout/LineLength
+      end
+    end
+  end
+
+  context 'when window size is unavailable' do
+    before do
+      # `tput <cmd>` returns empty string on error
+      stub_helper(:fetch_window_size, '')
+      stub_helper(:fetch_window_height, '')
+    end
+
+    it_behaves_like 'creates the right defintion files',
+      'Terminal size does not prevent file creation' do
+      let(:keystrokes) do
+        [
+          "1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+          "Internal Event CLI is opened\n", # Submit description
+          "internal_events_cli_opened\n", # Submit action name
+          "6\n", # Select: None
+          "\n", # Skip MR URL
+          "instrumentation\n", # Filter & select group
+          "2\n", # Select [premium, ultimate]
+          "y\n", # Create file
+          "3\n" # Exit
+        ]
+      end
+
+      let(:output_files) { [{ 'path' => event2_filepath, 'content' => event2_content }] }
+    end
+  end
+
+  context "when user doesn't know what they're trying to do" do
+    it "handles when user isn't trying to track product usage" do
+      queue_cli_inputs([
+        "4\n", # Enum-select: ...am I in the right place?
+        "n\n" # No --> Are you trying to track customer usage of a GitLab feature?
+      ])
+
+      run_with_timeout
+
+      expect(plain_last_lines(50)).to include("Oh no! This probably isn't the tool you need!")
+    end
+
+    it "handles when product usage can't be tracked with events" do
+      queue_cli_inputs([
+        "4\n", # Enum-select: ...am I in the right place?
+        "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature?
+        "n\n" # No --> Can usage for the feature be measured by tracking a specific user action?
+      ])
+
+      run_with_timeout
+
+      expect(plain_last_lines(50)).to include("Oh no! This probably isn't the tool you need!")
+    end
+
+    it 'handles when user needs to add a new event' do
+      queue_cli_inputs([
+        "4\n", # Enum-select: ...am I in the right place?
+        "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature?
+        "y\n", # Yes --> Can usage for the feature be measured by tracking a specific user action?
+        "n\n", # No --> Is the event already tracked?
+        "n\n" # No --> Ready to start?
+      ])
+
+      run_with_timeout
+
+      expect(plain_last_lines(30)).to include("Okay! The next step is adding a new event! (~5 min)")
+    end
+
+    it 'handles when user needs to add a new metric' do
+      queue_cli_inputs([
+        "4\n", # Enum-select: ...am I in the right place?
+        "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature?
+        "y\n", # Yes --> Can usage for the feature be measured by tracking a specific user action?
+        "y\n", # Yes --> Is the event already tracked?
+        "n\n" # No --> Ready to start?
+      ])
+
+      run_with_timeout
+
+      expect(plain_last_lines(30)).to include("Amazing! The next step is adding a new metric! (~8 min)")
+    end
+  end
+
+  private
+
+  def queue_cli_inputs(keystrokes)
+    prompt.input << keystrokes.join('')
+    prompt.input.rewind
+  end
+
+  def run_with_timeout(duration = 1)
+    Timeout.timeout(duration) { described_class.new(prompt).run }
+  rescue Timeout::Error
+    # Timeout is needed to break out of the CLI, but we may want
+    # to make assertions afterwards
+  end
+
+  def run_with_verbose_timeout(duration = 1)
+    Timeout.timeout(duration) { described_class.new(prompt).run }
+  rescue Timeout::Error => e
+    # Re-raise error so CLI output is printed with the error
+    message = <<~TEXT
+    Awaiting input too long. Entire CLI output:
+
+    #{
+      prompt.output.string.lines
+        .map { |line| "\e[0;37m#{line}\e[0m" } # wrap in white
+        .join('')
+        .gsub("\e[1G", "\e[1G       ") # align to error indent
+    }
+
+
+    TEXT
+
+    raise e.class, message, e.backtrace
+  end
+
+  def plain_last_lines(size)
+    prompt.output.string
+      .lines
+      .last(size)
+      .join('')
+      .gsub(/\e[^\sm]{2,4}[mh]/, '')
+  end
+
+  def collect_file_writes(collector)
+    allow(File).to receive(:write).and_wrap_original do |original_method, *args, &block|
+      filepath = args.first
+      collector << filepath
+
+      dirname = Pathname.new(filepath).dirname
+      unless dirname.directory?
+        FileUtils.mkdir_p dirname
+        collector << dirname.to_s
+      end
+
+      original_method.call(*args, &block)
+    end
+  end
+
+  def stub_milestone(milestone)
+    stub_const("InternalEventsCli::Helpers::MILESTONE", milestone)
+  end
+
+  def stub_product_groups(body)
+    allow(Net::HTTP).to receive(:get)
+      .with(URI('https://gitlab.com/gitlab-com/www-gitlab-com/-/raw/master/data/stages.yml'))
+      .and_return(body)
+  end
+
+  def stub_helper(helper, value)
+    # rubocop:disable RSpec/AnyInstanceOf -- 'Next' helper not included in fast_spec_helper & next is insufficient
+    allow_any_instance_of(InternalEventsCli::Helpers).to receive(helper).and_return(value)
+    # rubocop:enable RSpec/AnyInstanceOf
+  end
+
+  def delete_files(files)
+    files.each do |filepath|
+      FileUtils.rm_f(Rails.root.join(filepath))
+    end
+  end
+
+  def internal_event_fixture(filepath)
+    Rails.root.join('spec', 'fixtures', 'scripts', 'internal_events', filepath)
+  end
+end