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,474 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Entrypoint for flow to print examples of how to trigger an
|
|
4
|
+
# event in different languages & different methods of testing
|
|
5
|
+
module GitlabInternalEventsCli
|
|
6
|
+
module Flows
|
|
7
|
+
class UsageViewer
|
|
8
|
+
include Helpers
|
|
9
|
+
|
|
10
|
+
PROPERTY_EXAMPLES = {
|
|
11
|
+
'label' => "'string'",
|
|
12
|
+
'property' => "'string'",
|
|
13
|
+
'value' => '72'
|
|
14
|
+
}.freeze
|
|
15
|
+
DEFAULT_PROPERTY_VALUE = "'custom_value'"
|
|
16
|
+
|
|
17
|
+
attr_reader :cli, :event
|
|
18
|
+
|
|
19
|
+
def initialize(cli, event_path = nil, event = nil)
|
|
20
|
+
@cli = cli
|
|
21
|
+
@event = event
|
|
22
|
+
@selected_event_path = event_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
prompt_for_eligible_event
|
|
27
|
+
prompt_for_usage_location
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def prompt_for_eligible_event
|
|
31
|
+
return if event
|
|
32
|
+
|
|
33
|
+
event_details = events_by_filepath
|
|
34
|
+
|
|
35
|
+
@selected_event_path = cli.select(
|
|
36
|
+
'Show examples for which event?',
|
|
37
|
+
get_event_options(event_details),
|
|
38
|
+
**select_opts,
|
|
39
|
+
**filter_opts
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@event = event_details[@selected_event_path]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def prompt_for_usage_location(default = '1. ruby/rails')
|
|
46
|
+
choices = [
|
|
47
|
+
{ name: '1. ruby/rails', value: :rails },
|
|
48
|
+
{ name: '2. rspec', value: :rspec },
|
|
49
|
+
{ name: '3. javascript (vue)', value: :vue },
|
|
50
|
+
{ name: '4. javascript (plain)', value: :js },
|
|
51
|
+
{ name: '5. vue template', value: :vue_template },
|
|
52
|
+
{ name: '6. haml', value: :haml },
|
|
53
|
+
{ name: '7. Manual testing in GDK', value: :gdk },
|
|
54
|
+
{ name: '8. Data verification in Tableau', value: :tableau },
|
|
55
|
+
{ name: '9. View examples for a different event', value: :other_event },
|
|
56
|
+
{ name: '10. Exit', value: :exit }
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
usage_location = cli.select(
|
|
60
|
+
'Select a use-case to view examples for:',
|
|
61
|
+
choices,
|
|
62
|
+
**select_opts,
|
|
63
|
+
**filter_opts,
|
|
64
|
+
per_page: 10
|
|
65
|
+
) do |menu|
|
|
66
|
+
menu.default default
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
case usage_location
|
|
70
|
+
when :rails
|
|
71
|
+
rails_examples
|
|
72
|
+
prompt_for_usage_location('1. ruby/rails')
|
|
73
|
+
when :rspec
|
|
74
|
+
rspec_examples
|
|
75
|
+
prompt_for_usage_location('2. rspec')
|
|
76
|
+
when :haml
|
|
77
|
+
haml_examples
|
|
78
|
+
prompt_for_usage_location('6. haml')
|
|
79
|
+
when :js
|
|
80
|
+
js_examples
|
|
81
|
+
prompt_for_usage_location('4. javascript (plain)')
|
|
82
|
+
when :vue
|
|
83
|
+
vue_examples
|
|
84
|
+
prompt_for_usage_location('3. javascript (vue)')
|
|
85
|
+
when :vue_template
|
|
86
|
+
vue_template_examples
|
|
87
|
+
prompt_for_usage_location('5. vue template')
|
|
88
|
+
when :gdk
|
|
89
|
+
gdk_examples
|
|
90
|
+
prompt_for_usage_location('7. Manual testing in GDK')
|
|
91
|
+
when :tableau
|
|
92
|
+
service_ping_dashboard_examples
|
|
93
|
+
prompt_for_usage_location('8. Data verification in Tableau')
|
|
94
|
+
when :other_event
|
|
95
|
+
self.class.new(cli).run
|
|
96
|
+
when :exit
|
|
97
|
+
cli.say(feedback_notice)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def rails_examples
|
|
102
|
+
identifier_args = identifiers.map do |identifier|
|
|
103
|
+
" #{identifier}: #{identifier}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
property_args = format_additional_properties do |property, value, description|
|
|
107
|
+
" #{property}: #{value}, # #{description}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if property_args.any?
|
|
111
|
+
property_args.last.sub!(',', '')
|
|
112
|
+
property_arg = " additional_properties: {\n#{property_args.join("\n")}\n }"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
args = ["'#{action}'", *identifier_args, property_arg].compact.join(",\n")
|
|
116
|
+
args = "\n #{args}\n" if args.lines.count > 1
|
|
117
|
+
|
|
118
|
+
cli.say format_warning <<~TEXT
|
|
119
|
+
#{divider}
|
|
120
|
+
#{format_help('# RAILS')}
|
|
121
|
+
|
|
122
|
+
include Gitlab::InternalEventsTracking
|
|
123
|
+
|
|
124
|
+
track_internal_event(#{args})
|
|
125
|
+
|
|
126
|
+
#{divider}
|
|
127
|
+
TEXT
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def rspec_examples
|
|
131
|
+
cli.say format_warning <<~TEXT
|
|
132
|
+
#{divider}
|
|
133
|
+
#{format_help('# RSPEC')}
|
|
134
|
+
|
|
135
|
+
#{format_warning(rspec_composable_matchers)}
|
|
136
|
+
|
|
137
|
+
#{divider}
|
|
138
|
+
TEXT
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def haml_examples
|
|
142
|
+
property_args = format_additional_properties do |property, value, _|
|
|
143
|
+
"event_#{property}: #{value}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
args = ["event_tracking: '#{action}'", *property_args].join(', ')
|
|
147
|
+
|
|
148
|
+
cli.say <<~TEXT
|
|
149
|
+
#{divider}
|
|
150
|
+
#{format_help('# HAML -- ON-CLICK')}
|
|
151
|
+
|
|
152
|
+
.inline-block{ #{format_warning("data: { #{args} }")} }
|
|
153
|
+
= _('Important Text')
|
|
154
|
+
|
|
155
|
+
#{divider}
|
|
156
|
+
#{format_help('# HAML -- COMPONENT ON-CLICK')}
|
|
157
|
+
|
|
158
|
+
= render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { #{args} }")} })
|
|
159
|
+
|
|
160
|
+
#{divider}
|
|
161
|
+
#{format_help('# HAML -- COMPONENT ON-LOAD')}
|
|
162
|
+
|
|
163
|
+
= render Pajamas::ButtonComponent.new(button_options: { #{format_warning("data: { event_tracking_load: true, #{args} }")} })
|
|
164
|
+
|
|
165
|
+
#{divider}
|
|
166
|
+
TEXT
|
|
167
|
+
|
|
168
|
+
cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def vue_template_examples
|
|
172
|
+
on_click_args = template_formatted_args('data-event-tracking', indent: 2)
|
|
173
|
+
on_load_args = template_formatted_args('data-event-tracking-load', indent: 2)
|
|
174
|
+
|
|
175
|
+
cli.say <<~TEXT
|
|
176
|
+
#{divider}
|
|
177
|
+
#{format_help('// VUE TEMPLATE -- ON-CLICK')}
|
|
178
|
+
|
|
179
|
+
<script>
|
|
180
|
+
import { GlButton } from '@gitlab/ui';
|
|
181
|
+
|
|
182
|
+
export default {
|
|
183
|
+
components: { GlButton }
|
|
184
|
+
};
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<template>
|
|
188
|
+
<gl-button#{on_click_args}
|
|
189
|
+
Click Me
|
|
190
|
+
</gl-button>
|
|
191
|
+
</template>
|
|
192
|
+
|
|
193
|
+
#{divider}
|
|
194
|
+
#{format_help('// VUE TEMPLATE -- ON-LOAD')}
|
|
195
|
+
|
|
196
|
+
<script>
|
|
197
|
+
import { GlButton } from '@gitlab/ui';
|
|
198
|
+
|
|
199
|
+
export default {
|
|
200
|
+
components: { GlButton }
|
|
201
|
+
};
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<template>
|
|
205
|
+
<gl-button#{on_load_args}
|
|
206
|
+
Click Me
|
|
207
|
+
</gl-button>
|
|
208
|
+
</template>
|
|
209
|
+
|
|
210
|
+
#{divider}
|
|
211
|
+
TEXT
|
|
212
|
+
|
|
213
|
+
cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def js_examples
|
|
217
|
+
args = js_formatted_args(indent: 2)
|
|
218
|
+
|
|
219
|
+
cli.say <<~TEXT
|
|
220
|
+
#{divider}
|
|
221
|
+
#{format_help('// FRONTEND -- RAW JAVASCRIPT')}
|
|
222
|
+
|
|
223
|
+
#{format_warning("import { InternalEvents } from '~/tracking';")}
|
|
224
|
+
|
|
225
|
+
export const performAction = () => {
|
|
226
|
+
#{format_warning("InternalEvents.trackEvent#{args}")}
|
|
227
|
+
|
|
228
|
+
return true;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
#{divider}
|
|
232
|
+
TEXT
|
|
233
|
+
|
|
234
|
+
# https://docs.snowplow.io/docs/understanding-your-pipeline/schemas/
|
|
235
|
+
cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def vue_examples
|
|
239
|
+
args = js_formatted_args(indent: 6)
|
|
240
|
+
|
|
241
|
+
cli.say <<~TEXT
|
|
242
|
+
#{divider}
|
|
243
|
+
#{format_help('// VUE')}
|
|
244
|
+
|
|
245
|
+
<script>
|
|
246
|
+
#{format_warning("import { InternalEvents } from '~/tracking';")}
|
|
247
|
+
import { GlButton } from '@gitlab/ui';
|
|
248
|
+
|
|
249
|
+
#{format_warning('const trackingMixin = InternalEvents.mixin();')}
|
|
250
|
+
|
|
251
|
+
export default {
|
|
252
|
+
components: { GlButton },
|
|
253
|
+
#{format_warning('mixins: [trackingMixin]')},
|
|
254
|
+
methods: {
|
|
255
|
+
performAction() {
|
|
256
|
+
#{format_warning("this.trackEvent#{args}")}
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
</script>
|
|
261
|
+
|
|
262
|
+
<template>
|
|
263
|
+
<gl-button @click=performAction>Click Me</gl-button>
|
|
264
|
+
</template>
|
|
265
|
+
|
|
266
|
+
#{divider}
|
|
267
|
+
TEXT
|
|
268
|
+
|
|
269
|
+
cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def action
|
|
275
|
+
event['action']
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def identifiers
|
|
279
|
+
Array(event['identifiers']).tap do |ids|
|
|
280
|
+
# We always auto assign namespace if project is provided
|
|
281
|
+
ids.delete('namespace') if ids.include?('project')
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def additional_properties
|
|
286
|
+
Array(event['additional_properties'])
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def format_additional_properties
|
|
290
|
+
additional_properties.map do |property, details|
|
|
291
|
+
example_value = PROPERTY_EXAMPLES.fetch(property, DEFAULT_PROPERTY_VALUE)
|
|
292
|
+
description = details['description'] || 'TODO'
|
|
293
|
+
|
|
294
|
+
yield(property, example_value, description)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def rspec_composable_matchers
|
|
299
|
+
identifier_args = identifiers.map do |identifier|
|
|
300
|
+
" #{identifier}: #{identifier}"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
property_args = format_additional_properties do |property, value|
|
|
304
|
+
" #{property}: #{value}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if property_args.any?
|
|
308
|
+
property_args = format_prefix ' ', <<~TEXT.chomp
|
|
309
|
+
additional_properties: {
|
|
310
|
+
#{property_args.join(",\n")}
|
|
311
|
+
}
|
|
312
|
+
TEXT
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
args = []
|
|
316
|
+
args += [*identifier_args] unless identifier_args.empty?
|
|
317
|
+
args += [*property_args] unless property_args.empty?
|
|
318
|
+
args = args.join(",\n")
|
|
319
|
+
|
|
320
|
+
unless args.empty?
|
|
321
|
+
args = <<~TEXT.chomp
|
|
322
|
+
.with(
|
|
323
|
+
#{args}
|
|
324
|
+
)
|
|
325
|
+
TEXT
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
non_sum_metrics_list = []
|
|
329
|
+
sum_metrics_list = []
|
|
330
|
+
related_metrics.map do |metric|
|
|
331
|
+
target_array = metric.operator == 'sum(value)' ? sum_metrics_list : non_sum_metrics_list
|
|
332
|
+
target_array << " '#{metric.key_path}'"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
metrics_list = ''
|
|
336
|
+
unless non_sum_metrics_list.empty?
|
|
337
|
+
metrics_list += <<~TEXT.chomp
|
|
338
|
+
.and increment_usage_metrics(
|
|
339
|
+
#{non_sum_metrics_list.join(",\n")}
|
|
340
|
+
)
|
|
341
|
+
TEXT
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
unless sum_metrics_list.empty?
|
|
345
|
+
metrics_list += <<~TEXT.chomp
|
|
346
|
+
.and increment_usage_metrics(
|
|
347
|
+
#{sum_metrics_list.join(",\n")}
|
|
348
|
+
).by(#{PROPERTY_EXAMPLES.fetch('value')})
|
|
349
|
+
TEXT
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
<<~TEXT.chomp
|
|
353
|
+
it "triggers an internal event" do
|
|
354
|
+
expect { subject }.to trigger_internal_events('#{action}')#{args}#{metrics_list}
|
|
355
|
+
end
|
|
356
|
+
TEXT
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def js_formatted_args(indent:)
|
|
360
|
+
return "('#{action}');" if additional_properties.none?
|
|
361
|
+
|
|
362
|
+
property_args = format_additional_properties do |property, value, description|
|
|
363
|
+
" #{property}: #{value}, // #{description}"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
[
|
|
367
|
+
'(',
|
|
368
|
+
" '#{action}',",
|
|
369
|
+
' {',
|
|
370
|
+
*property_args,
|
|
371
|
+
' },',
|
|
372
|
+
');'
|
|
373
|
+
].join("\n#{' ' * indent}")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def service_ping_metrics_info
|
|
377
|
+
product_group = related_metrics.map(&:product_group).uniq
|
|
378
|
+
|
|
379
|
+
<<~TEXT
|
|
380
|
+
#{product_group.map { |group| "#{group}: #{format_info(metric_exploration_group_path(group, find_stage(group)))}" }.join("\n")}
|
|
381
|
+
|
|
382
|
+
#{divider}
|
|
383
|
+
#{format_help("# METRIC TRENDS -- view data for a service ping metric for #{event.action}")}
|
|
384
|
+
|
|
385
|
+
#{related_metrics.map { |metric| "#{metric.key_path}: #{format_info(metric_trend_path(metric.key_path))}" }.join("\n")}
|
|
386
|
+
TEXT
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def service_ping_no_metric_info
|
|
390
|
+
<<~TEXT
|
|
391
|
+
#{format_help("# Warning: There are no metrics for #{event.action} yet.")}
|
|
392
|
+
#{event.product_group}: #{format_info(metric_exploration_group_path(event.product_group, find_stage(event.product_group)))}
|
|
393
|
+
TEXT
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def template_formatted_args(data_attr, indent:)
|
|
397
|
+
return " #{data_attr}=\"#{action}\">" if additional_properties.none?
|
|
398
|
+
|
|
399
|
+
spacer = ' ' * indent
|
|
400
|
+
property_args = format_additional_properties do |property, value, _|
|
|
401
|
+
" data-event-#{property}=#{value.tr("'", '"')}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
args = [
|
|
405
|
+
'',
|
|
406
|
+
" #{data_attr}=\"#{action}\"",
|
|
407
|
+
*property_args
|
|
408
|
+
].join("\n#{spacer}")
|
|
409
|
+
|
|
410
|
+
"#{format_warning(args)}\n#{spacer}>"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def related_metrics
|
|
414
|
+
cli.global.metrics.select { |metric| metric.actions&.include?(event.action) }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def service_ping_dashboard_examples
|
|
418
|
+
cli.say <<~TEXT
|
|
419
|
+
#{divider}
|
|
420
|
+
#{format_help('# GROUP DASHBOARDS -- view all service ping metrics for a specific group')}
|
|
421
|
+
|
|
422
|
+
#{related_metrics.any? ? service_ping_metrics_info : service_ping_no_metric_info}
|
|
423
|
+
#{divider}
|
|
424
|
+
Note: The metric dashboard links can also be accessed from #{format_info('https://metrics.gitlab.com/')}
|
|
425
|
+
|
|
426
|
+
Not what you're looking for? Check this doc:
|
|
427
|
+
- #{format_info('https://docs.gitlab.com/ee/development/internal_analytics/#data-discovery')}
|
|
428
|
+
|
|
429
|
+
TEXT
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def gdk_examples
|
|
433
|
+
key_paths = related_metrics.map(&:key_path)
|
|
434
|
+
|
|
435
|
+
cli.say <<~TEXT
|
|
436
|
+
#{divider}
|
|
437
|
+
#{format_help('# TERMINAL -- monitor events & changes to service ping metrics as they occur')}
|
|
438
|
+
|
|
439
|
+
1. From `gitlab/` directory, run the monitor script:
|
|
440
|
+
|
|
441
|
+
#{format_warning("bin/rails runner scripts/internal_events/monitor.rb #{event.action}")}
|
|
442
|
+
|
|
443
|
+
2. View metric updates within the terminal
|
|
444
|
+
|
|
445
|
+
3. [Optional] Configure gdk with snowplow micro to see individual events: https://gitlab-org.gitlab.io/gitlab-development-kit/howto/snowplow_micro/
|
|
446
|
+
|
|
447
|
+
#{divider}
|
|
448
|
+
#{format_help('# RAILS CONSOLE -- generate service ping payload, including most recent usage data')}
|
|
449
|
+
|
|
450
|
+
#{format_warning("require_relative 'spec/support/helpers/service_ping_helpers.rb'")}
|
|
451
|
+
|
|
452
|
+
#{format_help('# Get current value of a metric')}
|
|
453
|
+
#{
|
|
454
|
+
if key_paths.any?
|
|
455
|
+
key_paths.map { |key_path| format_warning("ServicePingHelpers.get_current_usage_metric_value('#{key_path}')") }.join("\n")
|
|
456
|
+
else
|
|
457
|
+
format_help("# Warning: There are no metrics for #{event.action} yet. When there are, replace <key_path> below.\n") +
|
|
458
|
+
format_warning('ServicePingHelpers.get_current_usage_metric_value(<key_path>)')
|
|
459
|
+
end
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#{format_help('# View entire service ping payload')}
|
|
463
|
+
#{format_warning('ServicePingHelpers.get_current_service_ping_payload')}
|
|
464
|
+
#{divider}
|
|
465
|
+
Need to test something else? Check these docs:
|
|
466
|
+
- https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/local_setup_and_debugging.html
|
|
467
|
+
- https://docs.gitlab.com/ee/development/internal_analytics/service_ping/troubleshooting.html
|
|
468
|
+
- https://docs.gitlab.com/ee/development/internal_analytics/review_guidelines.html
|
|
469
|
+
|
|
470
|
+
TEXT
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers for shared state across all CLI flows
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
class GlobalState
|
|
6
|
+
def events
|
|
7
|
+
@events ||= load_definitions(
|
|
8
|
+
Event,
|
|
9
|
+
GitlabInternalEventsCli::NEW_EVENT_FIELDS,
|
|
10
|
+
all_event_paths
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def metrics
|
|
15
|
+
@metrics ||= begin
|
|
16
|
+
loaded_files = load_definitions(
|
|
17
|
+
Metric,
|
|
18
|
+
GitlabInternalEventsCli::NEW_METRIC_FIELDS,
|
|
19
|
+
all_metric_paths
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
loaded_files.flat_map do |metric|
|
|
23
|
+
# copy logic of Gitlab::Usage::MetricDefinition
|
|
24
|
+
next metric unless metric.time_frame.is_a?(Array)
|
|
25
|
+
|
|
26
|
+
metric.time_frame.map do |time_frame|
|
|
27
|
+
current_metric = metric.dup
|
|
28
|
+
current_metric.time_frame = time_frame
|
|
29
|
+
current_metric.key_path = TimeFramedKeyPath.build(current_metric.key_path, time_frame)
|
|
30
|
+
current_metric
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reload_definitions
|
|
37
|
+
@events = nil
|
|
38
|
+
@metrics = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def all_event_paths
|
|
44
|
+
GitlabInternalEventsCli.configuration.resolve_event_paths
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def all_metric_paths
|
|
48
|
+
GitlabInternalEventsCli.configuration.resolve_metric_paths
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load_definitions(klass, fields, paths)
|
|
52
|
+
paths.filter_map do |path|
|
|
53
|
+
details = YAML.safe_load_file(path)
|
|
54
|
+
relevant_fields = fields.map(&:to_s)
|
|
55
|
+
|
|
56
|
+
relative_path = path.sub("#{GitlabInternalEventsCli.configuration.project_root}/", '')
|
|
57
|
+
klass.parse(**details.slice(*relevant_fields), file_path: relative_path)
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
puts "\n\n\e[31mEncountered an error while loading #{path}: #{e.message}\e[0m\n\n\n"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers related to configuration of TTY::Prompt prompts
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module CliInputs
|
|
7
|
+
def prompt_for_array_selection(message, choices, default = nil, **opts, &formatter)
|
|
8
|
+
formatter ||= ->(choice) { choice.sort.join(', ') }
|
|
9
|
+
|
|
10
|
+
choices = choices.map do |choice|
|
|
11
|
+
{ name: formatter.call(choice), value: choice }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
cli.select(message, choices, **select_opts, **opts) do |menu|
|
|
15
|
+
menu.enum '.'
|
|
16
|
+
menu.default formatter.call(default) if default
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Prompts the user to input text. Prefer this over calling cli#ask directly (so styling is consistent).
|
|
21
|
+
#
|
|
22
|
+
#
|
|
23
|
+
# @return [String, nil] user-provided text
|
|
24
|
+
# @param message [String] a single line prompt/question or last line of a prompt
|
|
25
|
+
# @param value [String, nil] prepopulated as the answer which user can accept/modify
|
|
26
|
+
# @option multiline [Boolean] indicates that any help text or prompt prefix will be printed on another line
|
|
27
|
+
# before calling #prompt_for_text --> ex) see MetricDefiner#prompt_for_description
|
|
28
|
+
# @yield [TTY::Prompt::Question]
|
|
29
|
+
# @see https://github.com/piotrmurach/tty-prompt?tab=readme-ov-file#21-ask
|
|
30
|
+
def prompt_for_text(message, value = nil, multiline: false, **opts)
|
|
31
|
+
prompt = message.dup # mutable for concat in #ask callback
|
|
32
|
+
|
|
33
|
+
options = { **input_opts, **opts }
|
|
34
|
+
value ||= options.delete(:value)
|
|
35
|
+
options.delete(:prefix) if multiline
|
|
36
|
+
|
|
37
|
+
cli.ask(prompt, **options) do |q|
|
|
38
|
+
q.value(value) if value
|
|
39
|
+
|
|
40
|
+
yield q if block_given?
|
|
41
|
+
|
|
42
|
+
if multiline
|
|
43
|
+
# wrap error messages so they render nicely with prompt
|
|
44
|
+
q.messages.each do |key, error|
|
|
45
|
+
closing_text = "\n#{format_error('<<|')}" if error.lines.length > 1
|
|
46
|
+
|
|
47
|
+
q.messages[key] = [error, closing_text, "\n\n\n"].join
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
# append help text only if this line includes the formatted 'prompt' prefix,
|
|
51
|
+
# otherwise depend on the caller to print the help text if needed
|
|
52
|
+
prompt.concat(" #{q.required ? input_required_text : input_optional_text(value)}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def input_opts
|
|
58
|
+
{ prefix: format_prompt('Input text: ') }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def yes_no_opts
|
|
62
|
+
{ prefix: format_prompt('Yes/No: ') }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Provide to cli#select as kwargs for consistent style/ux
|
|
66
|
+
def select_opts
|
|
67
|
+
{
|
|
68
|
+
prefix: format_prompt('Select one: '),
|
|
69
|
+
cycle: true,
|
|
70
|
+
show_help: :always,
|
|
71
|
+
# Strip colors so #format_selection is applied uniformly
|
|
72
|
+
active_color: ->(choice) { format_selection(clear_format(choice)) }
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Provide to cli#multiselect as kwargs for consistent style/ux
|
|
77
|
+
def multiselect_opts
|
|
78
|
+
{
|
|
79
|
+
**select_opts,
|
|
80
|
+
prefix: format_prompt('Select multiple: '),
|
|
81
|
+
min: 1,
|
|
82
|
+
help: '(Space to select, Enter to submit, ↑/↓/←/→ to move, Ctrl+A|R to select all|none, letters to filter)'
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Accepts a number of lines occupied by text, so remaining
|
|
87
|
+
# screen real estate can be filled with select options
|
|
88
|
+
def filter_opts(header_size: nil)
|
|
89
|
+
{
|
|
90
|
+
filter: true,
|
|
91
|
+
per_page: header_size ? [(window_height - header_size), 10].max : 30
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Creates divider to be passed to a select or multiselect
|
|
96
|
+
# as a menu item. Use with #format_disabled_options_as_dividers
|
|
97
|
+
# for best formatting.
|
|
98
|
+
def select_option_divider(text)
|
|
99
|
+
{ name: "-- #{text} --", value: nil, disabled: '' }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Styling all disabled options in a menu without indication
|
|
103
|
+
# of being a selectable option
|
|
104
|
+
# @param select_menu [TTY::Prompt]
|
|
105
|
+
def format_disabled_options_as_dividers(select_menu)
|
|
106
|
+
select_menu.symbols(cross: '')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# For use when menu options are disabled by being grayed out
|
|
110
|
+
def disabled_format_callback
|
|
111
|
+
proc { |menu| menu.symbols(cross: format_help('✘')) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Help text to use with required, multiline cli#ask prompts.
|
|
115
|
+
# Otherwise, prefer #prompt_for_text.
|
|
116
|
+
def input_required_text
|
|
117
|
+
format_help('(leave blank for help)')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Help text to use with optional, multiline cli#ask prompts.
|
|
121
|
+
# Otherwise, prefer #prompt_for_text.
|
|
122
|
+
def input_optional_text(value)
|
|
123
|
+
format_help("(enter to #{value ? 'submit' : 'skip'})")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def disableable_option(value:, disabled:, name: nil)
|
|
127
|
+
should_disable = yield
|
|
128
|
+
name ||= value
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
value: value,
|
|
132
|
+
name: (should_disable ? format_help(name) : name),
|
|
133
|
+
disabled: (disabled if should_disable)
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|