Welcome to mirror list, hosted at ThFree Co, Russian Federation.

metric_definer.rb « cli « internal_events « scripts - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 7688f03200fc3961a6c1c6f9b3e12d8bb17d92a7 (plain)
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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