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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.tool-versions +1 -0
  4. data/Gemfile +11 -0
  5. data/Gemfile.lock +90 -0
  6. data/LICENSE.txt +19 -0
  7. data/README.md +164 -0
  8. data/Rakefile +10 -0
  9. data/exe/gitlab-internal-events-cli +7 -0
  10. data/gitlab_internal_events_cli.gemspec +39 -0
  11. data/lib/gitlab_internal_events_cli/cli.rb +59 -0
  12. data/lib/gitlab_internal_events_cli/configuration.rb +115 -0
  13. data/lib/gitlab_internal_events_cli/event.rb +73 -0
  14. data/lib/gitlab_internal_events_cli/flows/event_definer.rb +306 -0
  15. data/lib/gitlab_internal_events_cli/flows/flow_advisor.rb +90 -0
  16. data/lib/gitlab_internal_events_cli/flows/metric_definer.rb +468 -0
  17. data/lib/gitlab_internal_events_cli/flows/usage_viewer.rb +474 -0
  18. data/lib/gitlab_internal_events_cli/gitlab_prompt.rb +9 -0
  19. data/lib/gitlab_internal_events_cli/global_state.rb +63 -0
  20. data/lib/gitlab_internal_events_cli/helpers/cli_inputs.rb +138 -0
  21. data/lib/gitlab_internal_events_cli/helpers/event_options.rb +63 -0
  22. data/lib/gitlab_internal_events_cli/helpers/files.rb +84 -0
  23. data/lib/gitlab_internal_events_cli/helpers/formatting.rb +166 -0
  24. data/lib/gitlab_internal_events_cli/helpers/group_ownership.rb +160 -0
  25. data/lib/gitlab_internal_events_cli/helpers/metric_options.rb +253 -0
  26. data/lib/gitlab_internal_events_cli/helpers/schema_loader.rb +25 -0
  27. data/lib/gitlab_internal_events_cli/helpers/service_ping_dashboards.rb +22 -0
  28. data/lib/gitlab_internal_events_cli/helpers.rb +47 -0
  29. data/lib/gitlab_internal_events_cli/http_cache.rb +52 -0
  30. data/lib/gitlab_internal_events_cli/metric.rb +406 -0
  31. data/lib/gitlab_internal_events_cli/schema_resolver.rb +25 -0
  32. data/lib/gitlab_internal_events_cli/subflows/database_metric_definer.rb +71 -0
  33. data/lib/gitlab_internal_events_cli/subflows/event_metric_definer.rb +258 -0
  34. data/lib/gitlab_internal_events_cli/text/event_definer.rb +166 -0
  35. data/lib/gitlab_internal_events_cli/text/flow_advisor.rb +64 -0
  36. data/lib/gitlab_internal_events_cli/text/metric_definer.rb +138 -0
  37. data/lib/gitlab_internal_events_cli/time_framed_key_path.rb +18 -0
  38. data/lib/gitlab_internal_events_cli/version.rb +5 -0
  39. data/lib/gitlab_internal_events_cli.rb +36 -0
  40. metadata +170 -0
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entrypoint for flow to create an event definition file
4
+ module GitlabInternalEventsCli
5
+ module Flows
6
+ class EventDefiner
7
+ include Helpers
8
+ include Text::EventDefiner
9
+
10
+ def self.schema
11
+ @schema ||= Helpers::SchemaLoader.load(
12
+ GitlabInternalEventsCli.configuration.event_schema_url,
13
+ 'event'
14
+ )
15
+ end
16
+
17
+ STEPS = [
18
+ 'New Event',
19
+ 'Description',
20
+ 'Name',
21
+ 'Context',
22
+ 'URL',
23
+ 'Group',
24
+ 'Categories',
25
+ 'Tiers',
26
+ 'Classification',
27
+ 'Save files'
28
+ ].freeze
29
+
30
+ IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.map { |k| k.join(', ') }.max_by(&:length)}]".length
31
+
32
+ attr_reader :cli, :event
33
+
34
+ def initialize(cli)
35
+ @cli = cli
36
+ @event = Event.new(milestone: milestone)
37
+ end
38
+
39
+ def run
40
+ prompt_for_description
41
+ prompt_for_action
42
+ prompt_for_context
43
+ prompt_for_url
44
+ prompt_for_product_group
45
+ prompt_for_product_categories
46
+ prompt_for_tier
47
+ prompt_for_classification
48
+
49
+ outcome = create_event_file
50
+ display_result(outcome)
51
+
52
+ prompt_for_next_steps
53
+ end
54
+
55
+ private
56
+
57
+ def prompt_for_description
58
+ new_page!(on_step: 'Description', steps: STEPS)
59
+ cli.say DESCRIPTION_INTRO
60
+
61
+ event.description = cli.ask("Describe what the event tracks: #{input_required_text}", **input_opts) do |q|
62
+ q.required true
63
+ q.modify :trim
64
+ q.messages[:required?] = DESCRIPTION_HELP
65
+ end
66
+ end
67
+
68
+ def prompt_for_action
69
+ new_page!(on_step: 'Name', steps: STEPS)
70
+ cli.say ACTION_INTRO
71
+
72
+ event.action = cli.ask("Define the event name: #{input_required_text}", **input_opts) do |q|
73
+ q.required true
74
+ q.validate ->(input) { input =~ NAME_REGEX && cli.global.events.map(&:action).none?(input) }
75
+ q.modify :trim
76
+ q.messages[:valid?] = format_warning(
77
+ 'Invalid event name. Only lowercase/numbers/underscores allowed. ' \
78
+ 'Ensure %<value>s is not an existing event.'
79
+ )
80
+ q.messages[:required?] = ACTION_HELP
81
+ end
82
+ end
83
+
84
+ def prompt_for_context
85
+ new_page!(on_step: 'Context', steps: STEPS)
86
+ cli.say format_prompt("EVENT CONTEXT #{counter(0, 2)}")
87
+ prompt_for_identifiers
88
+
89
+ new_page!(on_step: 'Context', steps: STEPS)
90
+ cli.say format_prompt("EVENT CONTEXT #{counter(1, 2)}")
91
+ prompt_for_additional_properties
92
+ end
93
+
94
+ def prompt_for_identifiers
95
+ cli.say IDENTIFIERS_INTRO % event.action
96
+
97
+ identifiers = prompt_for_array_selection(
98
+ 'Which identifiers are available when the event occurs?',
99
+ IDENTIFIER_OPTIONS.keys,
100
+ per_page: IDENTIFIER_OPTIONS.length
101
+ ) { |choice| format_identifier_choice(choice) }
102
+
103
+ event.identifiers = identifiers if identifiers.any?
104
+ end
105
+
106
+ def format_identifier_choice(choice)
107
+ formatted_choice = choice.empty? ? 'None' : "[#{choice.sort.join(', ')}]"
108
+ buffer = IDENTIFIER_FORMATTING_BUFFER - formatted_choice.length
109
+
110
+ "#{formatted_choice}#{' ' * buffer} -- #{IDENTIFIER_OPTIONS[choice]}"
111
+ end
112
+
113
+ def prompt_for_additional_properties
114
+ cli.say ADDITIONAL_PROPERTIES_INTRO
115
+
116
+ available_props = %i[label property value add_extra_prop]
117
+
118
+ while available_props.any?
119
+ options = property_option_definitions(available_props)
120
+
121
+ selected_property = cli.select(
122
+ 'Which additional property do you want to add to the event?',
123
+ options,
124
+ help: format_help('(will reprompt for multiple)'),
125
+ **select_opts,
126
+ &disabled_format_callback
127
+ )
128
+
129
+ if selected_property == :none
130
+ available_props.clear
131
+ elsif selected_property == :add_extra_prop
132
+ property_name = prompt_for_add_extra_properties
133
+ property_description = prompt_for_text('Describe what the field will include:')
134
+ assign_extra_properties(property_name, property_description)
135
+ else
136
+ available_props.delete(selected_property)
137
+ property_description = prompt_for_text('Describe what the field will include:')
138
+ assign_extra_properties(selected_property, property_description)
139
+ end
140
+ end
141
+ end
142
+
143
+ def property_option_definitions(available_props)
144
+ disabled = format_help('(already defined)')
145
+
146
+ [
147
+ { value: :none, name: 'None! Continue to next section!' },
148
+ disableable_option(
149
+ value: :label,
150
+ name: 'String 1 (attribute will be named `label`)',
151
+ disabled: disabled
152
+ ) { !available_props.include?(:label) },
153
+ disableable_option(
154
+ value: :property,
155
+ name: 'String 2 (attribute will be named `property`)',
156
+ disabled: disabled
157
+ ) { !available_props.include?(:property) },
158
+ disableable_option(
159
+ value: :value,
160
+ name: 'Number (attribute will be named `value`)',
161
+ disabled: disabled
162
+ ) { !available_props.include?(:value) },
163
+ { value: :add_extra_prop, name: 'Add extra property (attribute will be named the input custom name)' }
164
+ ]
165
+ end
166
+
167
+ def assign_extra_properties(property, description = nil)
168
+ event.additional_properties ||= {}
169
+ event.additional_properties[property.to_s] = {
170
+ 'description' => description || 'TODO'
171
+ }
172
+ end
173
+
174
+ def prompt_for_add_extra_properties
175
+ primary_props = %w[label property value]
176
+
177
+ prompt_for_text('Define a name for the attribute:', **input_opts) do |q|
178
+ q.required true
179
+ q.validate ->(input) { input =~ NAME_REGEX && primary_props.none?(input) }
180
+ q.modify :trim
181
+ q.messages[:required?] = ADDITIONAL_PROPERTIES_ADD_MORE_HELP
182
+ q.messages[:valid?] = format_warning(
183
+ 'Invalid property name. Only lowercase/numbers/underscores allowed. ' \
184
+ 'Ensure %<value>s is not one of `property, label, value`.'
185
+ )
186
+ end
187
+ end
188
+
189
+ def prompt_for_url
190
+ new_page!(on_step: 'URL', steps: STEPS)
191
+
192
+ event.introduced_by_url = prompt_for_text('Which MR URL will merge the event definition?')
193
+ end
194
+
195
+ def prompt_for_product_group
196
+ new_page!(on_step: 'Group', steps: STEPS)
197
+
198
+ product_group = prompt_for_group_ownership('Which group will own the event?')
199
+
200
+ event.product_group = product_group
201
+ end
202
+
203
+ def prompt_for_product_categories
204
+ new_page!(on_step: 'Categories', steps: STEPS)
205
+ cli.say <<~TEXT
206
+ #{format_info('FEATURE CATEGORY')}
207
+ Refer to https://handbook.gitlab.com/handbook/product/categories for information on current product categories.
208
+
209
+ TEXT
210
+
211
+ event.product_categories = prompt_for_feature_categories(
212
+ 'Which feature categories best fit this event?',
213
+ [event.product_group]
214
+ )
215
+ end
216
+
217
+ def prompt_for_tier
218
+ new_page!(on_step: 'Tiers', steps: STEPS)
219
+
220
+ event.tiers = prompt_for_array_selection(
221
+ 'Which tiers will the event be recorded on?',
222
+ [%w[free premium ultimate], %w[premium ultimate], %w[ultimate]]
223
+ )
224
+ end
225
+
226
+ def prompt_for_classification
227
+ new_page!(on_step: 'Classification', steps: STEPS)
228
+
229
+ cli.say CLASSIFICATION_INTRO
230
+
231
+ classification_choice = cli.select(
232
+ 'Should this event have "classification: duo"?',
233
+ **select_opts
234
+ ) do |menu|
235
+ menu.enum '.'
236
+ menu.choice 'Yes - this event is related to AI/Duo features', :duo
237
+ menu.choice 'No - this event is not related to AI/Duo features', :none
238
+ end
239
+
240
+ event.classification = 'duo' if classification_choice == :duo
241
+ end
242
+
243
+ def create_event_file
244
+ new_page!(on_step: 'Save files', steps: STEPS)
245
+
246
+ prompt_to_save_file(event.file_path, event.formatted_output)
247
+ end
248
+
249
+ def display_result(outcome)
250
+ new_page!
251
+
252
+ cli.say <<~TEXT
253
+ #{divider}
254
+ #{format_info('Done with event definition!')}
255
+
256
+ #{outcome || ' No files saved.'}
257
+
258
+ #{divider}
259
+
260
+ Do you need to create a metric? Probably!
261
+
262
+ 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.
263
+
264
+ Typical flow: Define event > Define metric > Instrument app code > Merge/Deploy MR > Verify data in Tableau/Snowflake
265
+
266
+ TEXT
267
+ end
268
+
269
+ def prompt_for_next_steps
270
+ next_step = cli.select('How would you like to proceed?', **select_opts) do |menu|
271
+ menu.enum '.'
272
+
273
+ menu.choice 'New Event -- define another event', :new_event
274
+
275
+ full_path = GitlabInternalEventsCli.configuration.absolute_path(event.file_path)
276
+ choice = if File.exist?(full_path)
277
+ ["Create Metric -- define a new metric using #{event.action}.yml", :add_metric]
278
+ else
279
+ ["Save & Create Metric -- save #{event.action}.yml and define a matching metric", :save_and_add]
280
+ end
281
+
282
+ menu.default choice[0]
283
+ menu.choice(*choice)
284
+
285
+ menu.choice "View Usage -- look at code examples for #{event.action}.yml", :view_usage
286
+ menu.choice 'Exit', :exit
287
+ end
288
+
289
+ case next_step
290
+ when :new_event
291
+ EventDefiner.new(cli).run
292
+ when :add_metric
293
+ MetricDefiner.new(cli, event.file_path).run
294
+ when :save_and_add
295
+ write_to_file(event.file_path, event.formatted_output, 'create')
296
+
297
+ MetricDefiner.new(cli, event.file_path).run
298
+ when :view_usage
299
+ UsageViewer.new(cli, event.file_path, event).run
300
+ when :exit
301
+ cli.say feedback_notice
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entrypoint for help flow, which directs the user to the
4
+ # correct flow or documentation based on their goal
5
+ module GitlabInternalEventsCli
6
+ module Flows
7
+ class FlowAdvisor
8
+ include Helpers
9
+ include Text::FlowAdvisor
10
+
11
+ attr_reader :cli
12
+
13
+ def initialize(cli)
14
+ @cli = cli
15
+ end
16
+
17
+ def run
18
+ return use_case_error unless goal_is_tracking_usage?
19
+ return use_case_error unless usage_trackable_with_internal_events?
20
+
21
+ event_already_tracked? ? proceed_to_metric_definition : proceed_to_event_definition
22
+ end
23
+
24
+ private
25
+
26
+ def goal_is_tracking_usage?
27
+ new_page!
28
+
29
+ cli.say format_info("First, let's check your objective.\n")
30
+
31
+ cli.yes?('Are you trying to track customer usage of a GitLab feature?', **yes_no_opts)
32
+ end
33
+
34
+ def usage_trackable_with_internal_events?
35
+ new_page!
36
+
37
+ cli.say format_info("Excellent! Let's check that this tool will fit your needs.\n")
38
+ cli.say EVENT_TRACKING_EXAMPLES
39
+
40
+ cli.yes?(
41
+ 'Can usage for the feature be measured with a count of specific user actions or events? ' \
42
+ 'Or counting a set of events?',
43
+ **yes_no_opts
44
+ )
45
+ end
46
+
47
+ def event_already_tracked?
48
+ new_page!
49
+
50
+ cli.say format_info("Super! Let's figure out if the event is already tracked & usable.\n")
51
+ cli.say EVENT_EXISTENCE_CHECK_INSTRUCTIONS
52
+
53
+ cli.yes?('Is the event already tracked?', **yes_no_opts)
54
+ end
55
+
56
+ def use_case_error
57
+ new_page!
58
+
59
+ cli.error("Oh no! This probably isn't the tool you need!\n")
60
+ cli.say ALTERNATE_RESOURCES_NOTICE
61
+ cli.say feedback_notice
62
+ end
63
+
64
+ def proceed_to_metric_definition
65
+ new_page!
66
+
67
+ cli.say format_info("Amazing! The next step is adding a new metric! (~8-15 min)\n")
68
+
69
+ return not_ready_error('New Metric') unless cli.yes?(format_prompt('Ready to start?'))
70
+
71
+ MetricDefiner.new(cli).run
72
+ end
73
+
74
+ def proceed_to_event_definition
75
+ new_page!
76
+
77
+ cli.say format_info("Okay! The next step is adding a new event! (~5-10 min)\n")
78
+
79
+ return not_ready_error('New Event') unless cli.yes?(format_prompt('Ready to start?'))
80
+
81
+ EventDefiner.new(cli).run
82
+ end
83
+
84
+ def not_ready_error(description)
85
+ cli.say "\nNo problem! When you're ready, run the CLI & select '#{description}'\n"
86
+ cli.say feedback_notice
87
+ end
88
+ end
89
+ end
90
+ end