gitlab_internal_events_cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.tool-versions +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +19 -0
- data/README.md +164 -0
- data/Rakefile +10 -0
- data/exe/gitlab-internal-events-cli +7 -0
- data/gitlab_internal_events_cli.gemspec +39 -0
- data/lib/gitlab_internal_events_cli/cli.rb +59 -0
- data/lib/gitlab_internal_events_cli/configuration.rb +115 -0
- data/lib/gitlab_internal_events_cli/event.rb +73 -0
- data/lib/gitlab_internal_events_cli/flows/event_definer.rb +306 -0
- data/lib/gitlab_internal_events_cli/flows/flow_advisor.rb +90 -0
- data/lib/gitlab_internal_events_cli/flows/metric_definer.rb +468 -0
- data/lib/gitlab_internal_events_cli/flows/usage_viewer.rb +474 -0
- data/lib/gitlab_internal_events_cli/gitlab_prompt.rb +9 -0
- data/lib/gitlab_internal_events_cli/global_state.rb +63 -0
- data/lib/gitlab_internal_events_cli/helpers/cli_inputs.rb +138 -0
- data/lib/gitlab_internal_events_cli/helpers/event_options.rb +63 -0
- data/lib/gitlab_internal_events_cli/helpers/files.rb +84 -0
- data/lib/gitlab_internal_events_cli/helpers/formatting.rb +166 -0
- data/lib/gitlab_internal_events_cli/helpers/group_ownership.rb +160 -0
- data/lib/gitlab_internal_events_cli/helpers/metric_options.rb +253 -0
- data/lib/gitlab_internal_events_cli/helpers/schema_loader.rb +25 -0
- data/lib/gitlab_internal_events_cli/helpers/service_ping_dashboards.rb +22 -0
- data/lib/gitlab_internal_events_cli/helpers.rb +47 -0
- data/lib/gitlab_internal_events_cli/http_cache.rb +52 -0
- data/lib/gitlab_internal_events_cli/metric.rb +406 -0
- data/lib/gitlab_internal_events_cli/schema_resolver.rb +25 -0
- data/lib/gitlab_internal_events_cli/subflows/database_metric_definer.rb +71 -0
- data/lib/gitlab_internal_events_cli/subflows/event_metric_definer.rb +258 -0
- data/lib/gitlab_internal_events_cli/text/event_definer.rb +166 -0
- data/lib/gitlab_internal_events_cli/text/flow_advisor.rb +64 -0
- data/lib/gitlab_internal_events_cli/text/metric_definer.rb +138 -0
- data/lib/gitlab_internal_events_cli/time_framed_key_path.rb +18 -0
- data/lib/gitlab_internal_events_cli/version.rb +5 -0
- data/lib/gitlab_internal_events_cli.rb +36 -0
- metadata +170 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Entrypoint for flow to create a metric definition file
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Flows
|
|
6
|
+
class MetricDefiner
|
|
7
|
+
include Helpers
|
|
8
|
+
include Text::MetricDefiner
|
|
9
|
+
|
|
10
|
+
def self.schema
|
|
11
|
+
@schema ||= Helpers::SchemaLoader.load(
|
|
12
|
+
GitlabInternalEventsCli.configuration.metric_schema_url,
|
|
13
|
+
'metric'
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
STEPS = [
|
|
18
|
+
'New Metric',
|
|
19
|
+
'Type',
|
|
20
|
+
'Config',
|
|
21
|
+
'Scope',
|
|
22
|
+
'Description',
|
|
23
|
+
'Defaults',
|
|
24
|
+
'Group',
|
|
25
|
+
'Categories',
|
|
26
|
+
'URL',
|
|
27
|
+
'Tiers',
|
|
28
|
+
'Save files'
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
attr_reader :cli
|
|
32
|
+
|
|
33
|
+
def initialize(cli, starting_event = nil)
|
|
34
|
+
@cli = cli
|
|
35
|
+
@selected_event_paths = Array(starting_event)
|
|
36
|
+
@metric = nil
|
|
37
|
+
@selected_filters = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
type = prompt_for_metric_type
|
|
42
|
+
|
|
43
|
+
prompt_for_configuration(type)
|
|
44
|
+
|
|
45
|
+
return unless metric
|
|
46
|
+
|
|
47
|
+
metric.milestone = milestone
|
|
48
|
+
prompt_for_description
|
|
49
|
+
prompt_for_metric_name
|
|
50
|
+
defaults = prompt_for_copying_event_properties
|
|
51
|
+
prompt_for_product_group(defaults)
|
|
52
|
+
prompt_for_product_categories(defaults)
|
|
53
|
+
prompt_for_url(defaults)
|
|
54
|
+
prompt_for_tier(defaults)
|
|
55
|
+
outcome = create_metric_file
|
|
56
|
+
prompt_for_next_steps(outcome)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
attr_reader :metric
|
|
62
|
+
|
|
63
|
+
# ----- Memoization Helpers -----------------
|
|
64
|
+
|
|
65
|
+
def events
|
|
66
|
+
@events ||= events_by_filepath(@selected_event_paths)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def selected_events
|
|
70
|
+
@selected_events ||= events.values_at(*@selected_event_paths)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ----- Prompts -----------------------------
|
|
74
|
+
|
|
75
|
+
def prompt_for_metric_type
|
|
76
|
+
return :event_metric if @selected_event_paths.any?
|
|
77
|
+
|
|
78
|
+
new_page!(on_step: 'Type', steps: STEPS)
|
|
79
|
+
|
|
80
|
+
cli.select('Which best describes what the metric should track?', **select_opts) do |menu|
|
|
81
|
+
menu.enum '.'
|
|
82
|
+
|
|
83
|
+
menu.choice 'Single event -- count occurrences of a specific event or user interaction',
|
|
84
|
+
:event_metric
|
|
85
|
+
menu.choice 'Multiple events -- count occurrences of several separate events or interactions',
|
|
86
|
+
:aggregate_metric
|
|
87
|
+
menu.choice 'Database -- record value of a particular field or count of database rows',
|
|
88
|
+
:database_metric
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def prompt_for_configuration(type)
|
|
93
|
+
case type
|
|
94
|
+
when :database_metric
|
|
95
|
+
# CLI doesn't load rails, so perform a simplified string <-> boolean check
|
|
96
|
+
if [nil, 'false', '0'].include? ENV['ENABLE_DATABASE_METRIC']
|
|
97
|
+
cli.error DATABASE_METRIC_NOTICE
|
|
98
|
+
cli.say feedback_notice
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
db_metric_definer = Subflows::DatabaseMetricDefiner.new(cli)
|
|
103
|
+
db_metric_definer.run
|
|
104
|
+
@metric = db_metric_definer.metric
|
|
105
|
+
when :event_metric, :aggregate_metric
|
|
106
|
+
event_metric_definer = Subflows::EventMetricDefiner.new(cli, @selected_event_paths, type)
|
|
107
|
+
event_metric_definer.run
|
|
108
|
+
@metric = event_metric_definer.metric
|
|
109
|
+
@selected_filters = event_metric_definer.selected_filters
|
|
110
|
+
@selected_event_paths = event_metric_definer.selected_event_paths
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def file_saved_context_message(attributes)
|
|
115
|
+
format_prefix ' ', <<~TEXT.chomp
|
|
116
|
+
- Visit #{format_info('https://metrics.gitlab.com')} to find dashboard links for this metric
|
|
117
|
+
#{metric_dashboard_links(attributes)}
|
|
118
|
+
- Set up Tableau Alerts via the Metric Trend Dashboards to receive notifications when your metrics cross specified thresholds.
|
|
119
|
+
See the Tableau Documentation for details: #{format_info('https://help.tableau.com/current/pro/desktop/en-us/data_alerts.htm')}
|
|
120
|
+
TEXT
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def metric_dashboard_links(attributes)
|
|
124
|
+
time_frames = attributes['time_frame']
|
|
125
|
+
|
|
126
|
+
unless time_frames.is_a?(Array)
|
|
127
|
+
return "- Metric trend dashboard: #{format_info(metric_trend_path(attributes['key_path']))}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
dashboards = time_frames.map do |time_frame|
|
|
131
|
+
key_path = TimeFramedKeyPath.build(attributes['key_path'], time_frame)
|
|
132
|
+
" - #{format_info(metric_trend_path(key_path))}"
|
|
133
|
+
end
|
|
134
|
+
['- Metric trend dashboards:', *dashboards].join("\n")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check existing event files for attributes to copy over
|
|
138
|
+
def prompt_for_copying_event_properties
|
|
139
|
+
shared_values = collect_values_for_shared_event_properties
|
|
140
|
+
defaults = shared_values.except(:stage, :section)
|
|
141
|
+
|
|
142
|
+
return {} if shared_values.none?
|
|
143
|
+
|
|
144
|
+
return shared_values if defaults.none?
|
|
145
|
+
|
|
146
|
+
new_page!(on_step: 'Defaults', steps: STEPS)
|
|
147
|
+
|
|
148
|
+
cli.say <<~TEXT
|
|
149
|
+
#{format_info('Convenient! We can copy these attributes from the event definition(s):')}
|
|
150
|
+
|
|
151
|
+
#{defaults.compact.transform_keys(&:to_s).to_yaml(line_width: 150)}
|
|
152
|
+
#{format_info('If any of these attributes are incorrect, you can also change them manually from your text editor later.')}
|
|
153
|
+
|
|
154
|
+
TEXT
|
|
155
|
+
|
|
156
|
+
cli.select('What would you like to do?', **select_opts) do |menu|
|
|
157
|
+
menu.enum '.'
|
|
158
|
+
menu.choice 'Copy & continue', -> { bulk_assign(defaults) }
|
|
159
|
+
menu.choice 'Modify attributes'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
shared_values
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def prompt_for_product_group(defaults)
|
|
166
|
+
assign_shared_attr(:product_group) do
|
|
167
|
+
new_page!(on_step: 'Group', steps: STEPS)
|
|
168
|
+
|
|
169
|
+
prompt_for_group_ownership('Which group owns the metric?', defaults)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def prompt_for_product_categories(defaults)
|
|
174
|
+
assign_shared_attr(:product_categories) do
|
|
175
|
+
new_page!(on_step: 'Categories', steps: STEPS)
|
|
176
|
+
cli.say <<~TEXT
|
|
177
|
+
#{format_info('FEATURE CATEGORY')}
|
|
178
|
+
Refer to https://handbook.gitlab.com/handbook/product/categories for information on current product categories.
|
|
179
|
+
|
|
180
|
+
TEXT
|
|
181
|
+
|
|
182
|
+
potential_groups = [
|
|
183
|
+
metric.product_group,
|
|
184
|
+
*selected_events.map(&:product_group),
|
|
185
|
+
defaults[:product_group]
|
|
186
|
+
]
|
|
187
|
+
prompt_for_feature_categories(
|
|
188
|
+
'Which feature categories best fit this metric?',
|
|
189
|
+
potential_groups,
|
|
190
|
+
defaults[:product_categories]
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def prompt_for_url(defaults)
|
|
196
|
+
assign_shared_attr(:introduced_by_url) do
|
|
197
|
+
new_page!(on_step: 'URL', steps: STEPS)
|
|
198
|
+
|
|
199
|
+
prompt_for_text(
|
|
200
|
+
'Which MR URL introduced the metric?',
|
|
201
|
+
defaults[:introduced_by_url]
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def prompt_for_tier(defaults)
|
|
207
|
+
assign_shared_attr(:tiers) do
|
|
208
|
+
new_page!(on_step: 'Tiers', steps: STEPS)
|
|
209
|
+
|
|
210
|
+
prompt_for_array_selection(
|
|
211
|
+
'Which tiers will the metric be reported from?',
|
|
212
|
+
[%w[free premium ultimate], %w[premium ultimate], %w[ultimate]],
|
|
213
|
+
defaults[:tiers]
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def create_metric_file
|
|
219
|
+
new_page!(on_step: 'Save files', steps: STEPS)
|
|
220
|
+
|
|
221
|
+
cli.say show_all_metric_paths(metric)
|
|
222
|
+
cli.say "\n"
|
|
223
|
+
|
|
224
|
+
cli.say format_prompt(format_subheader('SAVING FILE', metric.description))
|
|
225
|
+
cli.say "\n"
|
|
226
|
+
|
|
227
|
+
prompt_to_save_file(metric.file_path, metric.formatted_output)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def show_all_metric_paths(metric)
|
|
231
|
+
time_frames = metric.time_frame.value
|
|
232
|
+
|
|
233
|
+
return unless time_frames.is_a?(Array) && time_frames.length > 1
|
|
234
|
+
|
|
235
|
+
cli.say <<~TEXT
|
|
236
|
+
#{format_info "This would create #{time_frames.length} metrics with the following key paths:"}
|
|
237
|
+
|
|
238
|
+
#{time_frames.map do |time_frame|
|
|
239
|
+
"#{TimeFramedKeyPath::METRIC_TIME_FRAME_DESC[time_frame]}: #{format_info(TimeFramedKeyPath.build(metric.key_path, time_frame))}"
|
|
240
|
+
end.join("\n")}
|
|
241
|
+
TEXT
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def prompt_for_next_steps(outcome = nil)
|
|
245
|
+
new_page!
|
|
246
|
+
|
|
247
|
+
outcome ||= ' No files saved.'
|
|
248
|
+
|
|
249
|
+
event_metric_message = "\n Have you instrumented the application code to trigger the event yet? " \
|
|
250
|
+
"View usage examples to easily copy/paste implementation!\n"
|
|
251
|
+
cli.say <<~TEXT
|
|
252
|
+
#{divider}
|
|
253
|
+
#{format_info('Done with metric definitions!')}
|
|
254
|
+
|
|
255
|
+
#{outcome}
|
|
256
|
+
#{divider}
|
|
257
|
+
#{event_metric_message if metric.event_metric?}
|
|
258
|
+
Want to verify the metrics? Check out the group::#{metric[:product_group]} Metrics Exploration Dashboard in Tableau
|
|
259
|
+
Note: The Metrics Exploration Dashboard data would be available ~1 week after deploy for Gitlab.com, ~1 week after next release for self-managed
|
|
260
|
+
Link: #{format_info(metric_exploration_group_path(metric[:product_group], find_stage(metric.product_group)))}
|
|
261
|
+
|
|
262
|
+
Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake
|
|
263
|
+
|
|
264
|
+
TEXT
|
|
265
|
+
|
|
266
|
+
next_step = get_next_step
|
|
267
|
+
|
|
268
|
+
case next_step
|
|
269
|
+
when :new_event
|
|
270
|
+
EventDefiner.new(cli).run
|
|
271
|
+
when :new_metric_with_events
|
|
272
|
+
MetricDefiner.new(cli, @selected_event_paths).run
|
|
273
|
+
when :new_metric
|
|
274
|
+
MetricDefiner.new(cli).run
|
|
275
|
+
when :view_usage
|
|
276
|
+
args = [cli]
|
|
277
|
+
args += [@selected_event_paths.first, selected_events.first] if metric.event_metric?
|
|
278
|
+
UsageViewer.new(*args).run
|
|
279
|
+
when :exit
|
|
280
|
+
cli.say feedback_notice
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# ----- Prompt-specific Helpers -------------
|
|
285
|
+
|
|
286
|
+
# Helper for #prompt_for_description
|
|
287
|
+
def selected_event_descriptions
|
|
288
|
+
selected_events.map do |event|
|
|
289
|
+
filters = @selected_filters[event.action]
|
|
290
|
+
|
|
291
|
+
if filters&.any?
|
|
292
|
+
filter_phrase = filters.map { |k, v| "#{k}=#{v}" }.join(' ')
|
|
293
|
+
filter_phrase = format_help("(#{filter_phrase})")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
" #{event.action}#{filter_phrase} - #{format_selection(event.description)}\n"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def prompt_for_description
|
|
301
|
+
new_page!(on_step: 'Description', steps: STEPS)
|
|
302
|
+
|
|
303
|
+
if metric.event_metric?
|
|
304
|
+
cli.say EVENT_METRIC_DESCRIPTION_INTRO
|
|
305
|
+
cli.say selected_event_descriptions.join
|
|
306
|
+
else
|
|
307
|
+
cli.say DATABASE_METRIC_DESCRIPTION_INTRO
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
cli.say <<~TEXT
|
|
311
|
+
|
|
312
|
+
#{input_opts[:prefix]} How would you describe this metric to a non-technical person? #{input_required_text}
|
|
313
|
+
|
|
314
|
+
TEXT
|
|
315
|
+
|
|
316
|
+
if metric.technical_description
|
|
317
|
+
cli.say <<~TEXT
|
|
318
|
+
#{format_info('Technical description:')} #{metric.technical_description}
|
|
319
|
+
|
|
320
|
+
TEXT
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
has_prefix = !metric.description_prefix.nil?
|
|
324
|
+
|
|
325
|
+
description_start = format_info("#{metric.description_prefix}...") if has_prefix
|
|
326
|
+
command = has_prefix ? 'Finish' : 'Write'
|
|
327
|
+
|
|
328
|
+
description = prompt_for_text(" #{command} the description: #{description_start}", multiline: true) do |q|
|
|
329
|
+
q.required true
|
|
330
|
+
q.modify :trim
|
|
331
|
+
q.messages[:required?] = DESCRIPTION_HELP
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
metric.description = has_prefix ? "#{metric.description_prefix} #{description}" : description
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def prompt_for_metric_name
|
|
338
|
+
name_reason = name_requirement_reason
|
|
339
|
+
|
|
340
|
+
return unless name_reason
|
|
341
|
+
|
|
342
|
+
default_name = metric.key.value
|
|
343
|
+
display_name = metric.key.value("\e[0m[REPLACE ME]\e[36m")
|
|
344
|
+
empty_name = metric.key.value('')
|
|
345
|
+
max_length = MAX_FILENAME_LENGTH - "#{empty_name}.yml".length
|
|
346
|
+
help_tokens = { name: default_name, count: max_length }
|
|
347
|
+
|
|
348
|
+
cli.say <<~TEXT
|
|
349
|
+
|
|
350
|
+
#{input_opts[:prefix]} #{name_reason[:text]} How should we refererence this metric? #{input_required_text}
|
|
351
|
+
|
|
352
|
+
ID: #{format_info(display_name)}
|
|
353
|
+
Filename: #{format_info(display_name)}#{format_info('.yml')}
|
|
354
|
+
|
|
355
|
+
TEXT
|
|
356
|
+
|
|
357
|
+
metric.key = prompt_for_text(' Replace with: ', multiline: true) do |q|
|
|
358
|
+
q.required true
|
|
359
|
+
q.messages[:required?] = name_reason[:help] % help_tokens
|
|
360
|
+
q.messages[:valid?] = NAME_ERROR % help_tokens
|
|
361
|
+
q.validate lambda { |input|
|
|
362
|
+
input.length <= max_length &&
|
|
363
|
+
input.match?(NAME_REGEX) &&
|
|
364
|
+
!conflicting_key_path?(metric.key.value(input))
|
|
365
|
+
}
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Helper for #prompt_for_description
|
|
370
|
+
def name_requirement_reason
|
|
371
|
+
if metric.filters.assigned?
|
|
372
|
+
NAME_REQUIREMENT_REASONS[:filters]
|
|
373
|
+
elsif metric.file_name.length > MAX_FILENAME_LENGTH
|
|
374
|
+
NAME_REQUIREMENT_REASONS[:length]
|
|
375
|
+
elsif conflicting_key_path?(metric.key_path)
|
|
376
|
+
NAME_REQUIREMENT_REASONS[:conflict]
|
|
377
|
+
elsif !metric.event_metric?
|
|
378
|
+
NAME_REQUIREMENT_REASONS[:database_metric]
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Helper for #prompt_for_description
|
|
383
|
+
def conflicting_key_path?(key_path)
|
|
384
|
+
cli.global.metrics.any? do |existing_metric|
|
|
385
|
+
existing_metric.key_path == key_path
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Helper for #prompt_for_copying_event_properties
|
|
390
|
+
def collect_values_for_shared_event_properties
|
|
391
|
+
fields = Hash.new { |h, k| h[k] = [] }
|
|
392
|
+
|
|
393
|
+
selected_events.each do |event|
|
|
394
|
+
fields[:introduced_by_url] << event.introduced_by_url
|
|
395
|
+
fields[:product_group] << event.product_group
|
|
396
|
+
fields[:stage] << find_stage(event.product_group)
|
|
397
|
+
fields[:section] << find_section(event.product_group)
|
|
398
|
+
fields[:product_categories] << event.product_categories
|
|
399
|
+
fields[:tiers] << event.tiers&.sort
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
defaults = {}
|
|
403
|
+
|
|
404
|
+
# Use event value as default if it's the same for all
|
|
405
|
+
# selected events because it's unlikely to be different
|
|
406
|
+
fields.each do |field, values|
|
|
407
|
+
next unless values.compact.uniq.length == 1
|
|
408
|
+
|
|
409
|
+
defaults[field] ||= values.first
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# If an event is relevant to a category, then the metric
|
|
413
|
+
# will be too, so we'll collect all categories
|
|
414
|
+
defaults[:product_categories] = known_categories & fields[:product_categories].flatten
|
|
415
|
+
defaults.delete(:product_categories) if defaults[:product_categories].empty?
|
|
416
|
+
|
|
417
|
+
defaults
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Helper for #prompt_for_next_steps
|
|
421
|
+
def get_next_step
|
|
422
|
+
cli.select('How would you like to proceed?', **select_opts) do |menu|
|
|
423
|
+
menu.enum '.'
|
|
424
|
+
|
|
425
|
+
menu.choice 'New Event -- define a new event', :new_event
|
|
426
|
+
|
|
427
|
+
if metric.event_metric?
|
|
428
|
+
actions = selected_events.map(&:action).join(', ')
|
|
429
|
+
menu.choice "New Metric -- define another metric for #{actions}", :new_metric_with_events
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
menu.choice 'New Metric -- define another metric', :new_metric
|
|
433
|
+
|
|
434
|
+
if metric.event_metric?
|
|
435
|
+
view_usage_message = "View Usage -- look at code examples for event #{selected_events.first.action}"
|
|
436
|
+
default = view_usage_message
|
|
437
|
+
else
|
|
438
|
+
view_usage_message = 'View Usage -- look at code examples'
|
|
439
|
+
default = 'Exit'
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
menu.choice view_usage_message, :view_usage
|
|
443
|
+
menu.choice 'Exit', :exit
|
|
444
|
+
menu.default default
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# ----- Shared Helpers ----------------------
|
|
449
|
+
|
|
450
|
+
def assign_shared_attrs(...)
|
|
451
|
+
attrs = metric.to_h.slice(...)
|
|
452
|
+
attrs = yield(metric) unless attrs.values.all?
|
|
453
|
+
|
|
454
|
+
bulk_assign(attrs)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def assign_shared_attr(key)
|
|
458
|
+
assign_shared_attrs(key) do |metric|
|
|
459
|
+
{ key => yield(metric) }
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def bulk_assign(attrs)
|
|
464
|
+
metric.bulk_assign(attrs)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|