File: event_definer.rb

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (273 lines) | stat: -rw-r--r-- 9,169 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# frozen_string_literal: true

require_relative '../helpers'
require_relative '../text/event_definer'

# Entrypoint for flow to create an event definition file
module InternalEventsCli
  module Flows
    class EventDefiner
      include Helpers
      include Text::EventDefiner

      SCHEMA = ::JSONSchemer.schema(Pathname('config/events/schema.json'))
      STEPS = [
        'New Event',
        'Description',
        'Name',
        'Context',
        'URL',
        'Group',
        'Tiers',
        'Save files'
      ].freeze

      IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.map { |k| k.join(', ') }.max_by(&:length)}]".length

      attr_reader :cli, :event

      def initialize(cli)
        @cli = cli
        @event = Event.new(milestone: MILESTONE)
      end

      def run
        prompt_for_description
        prompt_for_action
        prompt_for_context
        prompt_for_url
        prompt_for_product_group
        prompt_for_tier

        outcome = create_event_file
        display_result(outcome)

        prompt_for_next_steps
      end

      private

      def prompt_for_description
        new_page!(1, 7, STEPS)
        cli.say 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?] = DESCRIPTION_HELP
        end
      end

      def prompt_for_action
        new_page!(2, 7, STEPS)
        cli.say ACTION_INTRO

        event.action = cli.ask("Define the event name: #{input_required_text}", **input_opts) do |q|
          q.required true
          q.validate ->(input) { input =~ NAME_REGEX && cli.global.events.map(&:action).none?(input) }
          q.modify :trim
          q.messages[:valid?] = format_warning(
            "Invalid event name. Only lowercase/numbers/underscores allowed. " \
              "Ensure %{value} is not an existing event.")
          q.messages[:required?] = ACTION_HELP
        end
      end

      def prompt_for_context
        new_page!(3, 7, STEPS)
        cli.say format_prompt("EVENT CONTEXT #{counter(0, 2)}")
        prompt_for_identifiers

        new_page!(3, 7, STEPS) # Same "step" but increment counter
        cli.say format_prompt("EVENT CONTEXT #{counter(1, 2)}")
        prompt_for_additional_properties
      end

      def prompt_for_identifiers
        cli.say IDENTIFIERS_INTRO % event.action

        identifiers = prompt_for_array_selection(
          'Which identifiers are available when the event occurs?',
          IDENTIFIER_OPTIONS.keys,
          per_page: IDENTIFIER_OPTIONS.length
        ) { |choice| format_identifier_choice(choice) }

        event.identifiers = identifiers if identifiers.any?
      end

      def format_identifier_choice(choice)
        formatted_choice = choice.empty? ? 'None' : "[#{choice.sort.join(', ')}]"
        buffer = IDENTIFIER_FORMATTING_BUFFER - formatted_choice.length

        "#{formatted_choice}#{' ' * buffer} -- #{IDENTIFIER_OPTIONS[choice]}"
      end

      def prompt_for_additional_properties
        cli.say ADDITIONAL_PROPERTIES_INTRO

        available_props = [:label, :property, :value, :add_extra_prop]

        while available_props.any?
          disabled = format_help('(already defined)')

          # rubocop:disable Rails/NegateInclude -- this isn't Rails
          options = [
            { value: :none, name: 'None! Continue to next section!' },
            disableable_option(
              value: :label,
              name: 'String 1 (attribute will be named `label`)',
              disabled: disabled
            ) { !available_props.include?(:label) },
            disableable_option(
              value: :property,
              name: 'String 2 (attribute will be named `property`)',
              disabled: disabled
            ) { !available_props.include?(:property) },
            disableable_option(
              value: :value,
              name: 'Number (attribute will be named `value`)',
              disabled: disabled
            ) { !available_props.include?(:value) },
            disableable_option(
              value: :add_extra_prop,
              name: 'Add extra property (attribute will be named the input custom name)',
              disabled: format_warning('(option disabled - use label/property/value first)')
            ) do
              !((!available_props.include?(:label) &&
                  !available_props.include?(:property)) ||
                  !available_props.include?(:value))
            end
          ]
          # rubocop:enable Rails/NegateInclude

          selected_property = cli.select(
            "Which additional property do you want to add to the event?",
            options,
            help: format_help("(will reprompt for multiple)"),
            **select_opts,
            &disabled_format_callback
          )

          if selected_property == :none
            available_props.clear
          elsif selected_property == :add_extra_prop
            property_name = prompt_for_add_extra_properties
            property_description = prompt_for_text('Describe what the field will include:')
            assign_extra_properties(property_name, property_description)
          else
            available_props.delete(selected_property)
            property_description = prompt_for_text('Describe what the field will include:')
            assign_extra_properties(selected_property, property_description)
          end
        end
      end

      def assign_extra_properties(property, description = nil)
        event.additional_properties ||= {}
        event.additional_properties[property.to_s] = {
          'description' => description || 'TODO'
        }
      end

      def prompt_for_add_extra_properties
        primary_props = %w[label property value]

        prompt_for_text('Define a name for the attribute:', **input_opts) do |q|
          q.required true
          q.validate ->(input) { input =~ NAME_REGEX && primary_props.none?(input) }
          q.modify :trim
          q.messages[:required?] = ADDITIONAL_PROPERTIES_ADD_MORE_HELP
          q.messages[:valid?] = format_warning(
            "Invalid property name. Only lowercase/numbers/underscores allowed. " \
              "Ensure %{value} is not one of `property, label, value`.")
        end
      end

      def prompt_for_url
        new_page!(4, 7, STEPS)

        event.introduced_by_url = prompt_for_text('Which MR URL will merge the event definition?')
      end

      def prompt_for_product_group
        new_page!(5, 7, STEPS)

        product_group = prompt_for_group_ownership('Which group will own the event?')

        event.product_group = product_group
      end

      def prompt_for_tier
        new_page!(6, 7, STEPS)

        event.tiers = prompt_for_array_selection(
          'Which tiers will the event be recorded on?',
          [%w[free premium ultimate], %w[premium ultimate], %w[ultimate]]
        )

        event.distributions = event.tiers.include?('free') ? %w[ce ee] : %w[ee]
      end

      def create_event_file
        new_page!(7, 7, STEPS)

        prompt_to_save_file(event.file_path, event.formatted_output)
      end

      def display_result(outcome)
        new_page!

        cli.say <<~TEXT
          #{divider}
          #{format_info('Done with event definition!')}

          #{outcome || '  No files saved.'}

          #{divider}

            Do you need to create a metric? Probably!

            Metrics are required to pull any usage data from self-managed instances or GitLab-Dedicated through Service Ping. Collected metric data can viewed in Tableau. Individual event details from GitLab.com can also be accessed through Snowflake.

            Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake

        TEXT
      end

      def prompt_for_next_steps
        next_step = cli.select("How would you like to proceed?", **select_opts) do |menu|
          menu.enum "."

          menu.choice "New Event -- define another event", :new_event

          choice = if File.exist?(event.file_path)
                     ["Create Metric -- define a new metric using #{event.action}.yml", :add_metric]
                   else
                     ["Save & Create Metric -- save #{event.action}.yml and define a matching metric", :save_and_add]
                   end

          menu.default choice[0]
          menu.choice(*choice)

          menu.choice "View Usage -- look at code examples for #{event.action}.yml", :view_usage
          menu.choice 'Exit', :exit
        end

        case next_step
        when :new_event
          EventDefiner.new(cli).run
        when :add_metric
          MetricDefiner.new(cli, event.file_path).run
        when :save_and_add
          write_to_file(event.file_path, event.formatted_output, 'create')

          MetricDefiner.new(cli, event.file_path).run
        when :view_usage
          UsageViewer.new(cli, event.file_path, event).run
        when :exit
          cli.say feedback_notice
        end
      end
    end
  end
end