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,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
|