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
|