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,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabInternalEventsCli
4
+ module Subflows
5
+ class EventMetricDefiner
6
+ include Helpers
7
+ include Text::MetricDefiner
8
+
9
+ attr_reader :metric, :selected_event_paths, :selected_filters
10
+
11
+ def initialize(cli, selected_event_paths, type)
12
+ @cli = cli
13
+ @metric = nil
14
+ @selected_event_paths = selected_event_paths
15
+ @selected_filters = {}
16
+ @type = type
17
+ end
18
+
19
+ def run
20
+ prompt_for_events
21
+
22
+ return unless @selected_event_paths.any?
23
+
24
+ prompt_for_metrics
25
+
26
+ return unless metric
27
+
28
+ metric.data_source = 'internal_events'
29
+ prompt_for_event_filters
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :cli, :type
35
+
36
+ # ----- Memoization Helpers -----------------
37
+
38
+ def events
39
+ @events ||= events_by_filepath(@selected_event_paths)
40
+ end
41
+
42
+ def selected_events
43
+ @selected_events ||= events.values_at(*@selected_event_paths)
44
+ end
45
+
46
+ # ----- Prompts -----------------------------
47
+
48
+ def prompt_for_events
49
+ return if @selected_event_paths.any?
50
+
51
+ new_page!(on_step: 'Config', steps: Flows::MetricDefiner::STEPS)
52
+
53
+ case type
54
+ when :event_metric
55
+ cli.say "For robust event search, use the Metrics Dictionary: https://metrics.gitlab.com/snowplow\n\n"
56
+
57
+ @selected_event_paths = [cli.select(
58
+ 'Which event does this metric track?',
59
+ get_event_options(events),
60
+ **select_opts,
61
+ **filter_opts(header_size: 7)
62
+ )]
63
+ when :aggregate_metric
64
+ cli.say "For robust event search, use the Metrics Dictionary: https://metrics.gitlab.com/snowplow\n\n"
65
+
66
+ @selected_event_paths = cli.multi_select(
67
+ 'Which events does this metric track? (Space to select)',
68
+ get_event_options(events),
69
+ **multiselect_opts,
70
+ **filter_opts(header_size: 7)
71
+ )
72
+ end
73
+ end
74
+
75
+ def prompt_for_metrics
76
+ eligible_metrics = get_metric_options(selected_events)
77
+
78
+ if eligible_metrics.all? { |metric| metric[:disabled] }
79
+ cli.error ALL_METRICS_EXIST_NOTICE
80
+ cli.say feedback_notice
81
+
82
+ return
83
+ end
84
+
85
+ new_page!(on_step: 'Scope', steps: Flows::MetricDefiner::STEPS)
86
+
87
+ cli.say format_info('SELECTED EVENTS')
88
+ cli.say selected_events_filter_options.join
89
+ cli.say "\n"
90
+
91
+ @metric = cli.select(
92
+ 'Which metrics do you want to add?',
93
+ eligible_metrics,
94
+ **select_opts,
95
+ **filter_opts,
96
+ per_page: 20,
97
+ &disabled_format_callback
98
+ )
99
+
100
+ assign_shared_attrs(:actions, :milestone) do
101
+ {
102
+ actions: selected_events.map(&:action).sort
103
+ }
104
+ end
105
+ end
106
+
107
+ def prompt_for_event_filters
108
+ return unless metric.filters_expected?
109
+
110
+ selected_unique_identifier = metric.identifier.value
111
+ event_count = selected_events.length
112
+ previous_inputs = {
113
+ 'label' => nil,
114
+ 'property' => nil,
115
+ 'value' => nil
116
+ }
117
+
118
+ event_filters = selected_events.dup.flat_map.with_index do |event, idx|
119
+ print_event_filter_header(event, idx, event_count)
120
+
121
+ next if deselect_nonfilterable_event?(event)
122
+
123
+ filter_values = event.additional_properties&.filter_map do |property, _|
124
+ next if selected_unique_identifier == property
125
+
126
+ prompt_for_property_filter(
127
+ event.action,
128
+ property,
129
+ previous_inputs[property]
130
+ )
131
+ end
132
+
133
+ previous_inputs.merge!(@selected_filters[event.action] || {})
134
+
135
+ find_filter_permutations(event.action, filter_values)
136
+ end.compact
137
+
138
+ bulk_assign(filters: event_filters)
139
+ end
140
+
141
+ # ----- Prompt-specific Helpers -------------
142
+
143
+ # Helper for #prompt_for_metrics
144
+ def selected_events_filter_options
145
+ filterable_events_selected = selected_events.any? { |event| event.additional_properties&.any? }
146
+
147
+ selected_events.map do |event|
148
+ filters = event.additional_properties&.keys
149
+ filter_phrase = if filters
150
+ " (filterable by #{filters&.join(', ')})"
151
+ elsif filterable_events_selected
152
+ ' -- not filterable'
153
+ end
154
+
155
+ " - #{event.action}#{format_help(filter_phrase)}\n"
156
+ end
157
+ end
158
+
159
+ # Helper for #prompt_for_event_filters
160
+ def print_event_filter_header(event, idx, total)
161
+ cli.say "\n"
162
+ cli.say format_info(format_subheader('SETTING EVENT FILTERS', event.action, idx, total))
163
+
164
+ return unless event.additional_properties&.any?
165
+
166
+ event_filter_options = event.additional_properties.map do |property, attrs|
167
+ " #{property}: #{attrs['description']}\n"
168
+ end
169
+
170
+ cli.say event_filter_options.join
171
+ end
172
+
173
+ # Helper for #prompt_for_event_filters
174
+ def deselect_nonfilterable_event?(event)
175
+ cli.say "\n"
176
+
177
+ return false if event.additional_properties&.any?
178
+ return false if cli.yes?('This event is not filterable. Should it be included in the metric?', **yes_no_opts)
179
+
180
+ selected_events.delete(event)
181
+ bulk_assign(actions: selected_events.map(&:action).sort)
182
+
183
+ true
184
+ end
185
+
186
+ # Helper for #prompt_for_event_filters
187
+ def prompt_for_property_filter(action, property, default)
188
+ formatted_prop = format_info(property)
189
+ prompt = "Count where #{formatted_prop} equals any of (comma-sep):"
190
+
191
+ inputs = prompt_for_text(prompt, default, **input_opts) do |q|
192
+ if property == 'value'
193
+ q.convert ->(input) { input.split(',').map(&:to_i).uniq }
194
+ q.validate(/^(\d|\s|,)*$/)
195
+ q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric"
196
+ elsif %w[property label].include?(property)
197
+ q.convert ->(input) { input.split(',').map(&:strip).uniq }
198
+ else
199
+ q.convert lambda { |input|
200
+ input.split(',').map do |value|
201
+ val = value.strip
202
+ cast_if_numeric(val)
203
+ end.uniq
204
+ }
205
+ end
206
+ end
207
+
208
+ return unless inputs&.any?
209
+
210
+ @selected_filters[action] ||= {}
211
+ @selected_filters[action][property] = inputs.join(',')
212
+
213
+ inputs.map { |input| { property => input } }.uniq
214
+ end
215
+
216
+ def cast_if_numeric(text)
217
+ float = Float(text)
218
+ (float % 1).zero? ? float.to_i : float
219
+ rescue ArgumentError
220
+ text
221
+ end
222
+
223
+ # Helper for #prompt_for_event_filters
224
+ #
225
+ # Gets all the permutations of the provided property values.
226
+ # @param filters [Array] ex) [{ 'label' => 'red' }, { 'label' => 'blue' }, { value => 16 }]
227
+ # @return ex) [{ 'label' => 'red', value => 16 }, { 'label' => 'blue', value => 16 }]
228
+ def find_filter_permutations(action, filters)
229
+ # Define a filter for all events, regardless of the available props so NewMetric#events is correct
230
+ return [[action, {}]] unless filters&.any?
231
+
232
+ # Uses proc syntax to avoid spliting & type-checking `filters`
233
+ :product.to_proc.call(*filters).map do |filter|
234
+ [action, filter.reduce(&:merge)]
235
+ end
236
+ end
237
+
238
+ # ----- Shared Helpers ----------------------
239
+
240
+ def assign_shared_attrs(...)
241
+ attrs = metric.to_h.slice(...)
242
+ attrs = yield(metric) unless attrs.values.all?
243
+
244
+ bulk_assign(attrs)
245
+ end
246
+
247
+ def assign_shared_attr(key)
248
+ assign_shared_attrs(key) do |metric|
249
+ { key => yield(metric) }
250
+ end
251
+ end
252
+
253
+ def bulk_assign(attrs)
254
+ metric.bulk_assign(attrs)
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabInternalEventsCli
4
+ module Text
5
+ module EventDefiner
6
+ extend Helpers::Formatting
7
+
8
+ DESCRIPTION_INTRO = <<~TEXT.freeze
9
+ #{format_info('EVENT DESCRIPTION')}
10
+ Include what the event is supposed to track, where, and when.
11
+
12
+ The description field helps others find & reuse this event. This will be used by Engineering, Product, Data team, Support -- and also GitLab customers directly. Be specific and explicit.
13
+ ex - Debian package published to the registry using a deploy token
14
+ ex - Issue confidentiality was changed
15
+
16
+ TEXT
17
+
18
+ DESCRIPTION_HELP = <<~TEXT.freeze
19
+ #{format_warning('Required. 10+ words likely, but length may vary.')}
20
+
21
+ #{format_info('GOOD EXAMPLES:')}
22
+ - Pipeline is created with a CI Template file included in its configuration
23
+ - Quick action `/assign @user1` used to assign a single individual to an issuable
24
+ - Quick action `/target_branch` used on a Merge Request
25
+ - Quick actions `/unlabel` or `/remove_label` used to remove one or more specific labels
26
+ - User edits file using the single file editor
27
+ - User edits file using the Web IDE
28
+ - User removed issue link between issue and incident
29
+ - Debian package published to the registry using a deploy token
30
+
31
+ #{format_info('GUT CHECK:')}
32
+ For your description...
33
+ 1. Would two different engineers likely instrument the event from the same code locations?
34
+ 2. Would a new GitLab user find where the event is triggered in the product?
35
+ 3. Would a GitLab customer understand what the description says?
36
+
37
+
38
+ TEXT
39
+
40
+ ACTION_INTRO = <<~TEXT.freeze
41
+ #{format_info('EVENT NAME')}
42
+ The event name is a unique identifier used from both a) app code and b) metric definitions.
43
+ The name should concisely communicate the same information as the event description.
44
+
45
+ ex - change_time_estimate_on_issue
46
+ ex - push_package_to_repository
47
+ ex - publish_go_module_to_the_registry_from_pipeline
48
+ ex - admin_user_comments_on_issue_while_impersonating_blocked_user
49
+
50
+ #{format_info('EXPECTED FORMAT:')} #{format_selection('<action>_<target_of_action>_<where/when>')}
51
+
52
+ ex) click_save_button_in_issue_description_within_15s_of_page_load
53
+ - ACTION: click
54
+ - TARGET: save button
55
+ - WHERE: in issue description
56
+ - WHEN: within 15s of page load
57
+
58
+ TEXT
59
+
60
+ ACTION_HELP = <<~TEXT.freeze
61
+ #{format_warning('Required. Must be globally unique. Must use only letters/numbers/underscores.')}
62
+
63
+ #{format_info('FAQs:')}
64
+ - Q: Present tense or past tense?
65
+ A: Prefer present tense! But it's up to you.
66
+ - Q: Other event names have prefixes like `i_` or the `g_group_name`. Why?
67
+ A: Those are leftovers from legacy naming schemes. Changing the names of old events/metrics can break dashboards, so stability is better than uniformity.
68
+
69
+
70
+ TEXT
71
+
72
+ IDENTIFIERS_INTRO = <<~TEXT.freeze
73
+ #{format_info('KEY IDENTIFIERS')}
74
+ Indicates the attributes recorded when the event occurs. Generally, we want to include every identifier available to us when the event is triggered.
75
+
76
+ #{format_info('BACKEND')}: Attributes must be specified when the event is triggered
77
+ ex) User, project, and namespace are the identifiers available for backend instrumentation:
78
+ track_internal_event(
79
+ '%s',
80
+ user: user,
81
+ project: project,
82
+ namespace: project.namespace
83
+ )
84
+
85
+ #{format_info('FRONTEND')}: Attributes are automatically included from the URL
86
+ ex) When a user takes an action on the MR list page, the URL is https://gitlab.com/gitlab-org/gitlab/-/merge_requests
87
+ Because this URL is for a project, we know that all of user/project/namespace are available for the event
88
+
89
+ #{format_info('NOTE')}: If you're planning to instrument a unique-by-user metric, you should still include project & namespace when possible. This is especially helpful in the data warehouse, where namespace and project can make events relevant for CSM use-cases.
90
+
91
+ TEXT
92
+
93
+ IDENTIFIER_OPTIONS = {
94
+ %w[project namespace user] =>
95
+ 'Use case: For project-level user actions (ex - issue_assignee_changed) [MOST COMMON]',
96
+ %w[namespace user] =>
97
+ 'Use case: For namespace-level user actions (ex - epic_assigned_to_milestone)',
98
+ %w[user] =>
99
+ 'Use case: For user-only actions (ex - admin_impersonated_user)',
100
+ %w[project namespace] =>
101
+ 'Use case: For project-level events without user interaction (ex - service_desk_request_received)',
102
+ %w[namespace] =>
103
+ 'Use case: For namespace-level events without user interaction (ex - stale_runners_cleaned_up)',
104
+ %w[feature_enabled_by_namespace_ids user] =>
105
+ 'Use case: For user actions attributable to multiple namespaces (ex - Code-Suggestions / Duo Pro)',
106
+ %w[] =>
107
+ 'Use case: For instance-level events without user interaction [LEAST COMMON]'
108
+ }.freeze
109
+
110
+ ADDITIONAL_PROPERTIES_INTRO = <<~TEXT.freeze
111
+ #{format_info('ADDITIONAL PROPERTIES')}
112
+ Describe any related attributes or information which should be tracked when the event occurs. This enables extra capabilities:
113
+ - Service Ping: define metrics filtered to a specific subset of events
114
+ - Snowflake: view/sort/group individual events from GitLab.com
115
+
116
+ BUILT-IN PROPERTIES (recommended)
117
+ For the best performance and flexibility, provide event context using:
118
+
119
+ property (string), label (string), value (numeric)
120
+
121
+ These attribute names correspond to repurposed fields in Snowflake. They have no special meaning other than data type.
122
+
123
+ ex) To add a metric like "Monthly count of unique users who changed an MR status to closed" using a 'change_merge_request_status' event, define an additional property like:
124
+ Attribute: label (string)
125
+ Description: Status of merge request after update (one of opened, merged, closed)
126
+
127
+ CUSTOM PROPERTIES (as-needed)
128
+ If the built-in properties are not suitable or descriptive, properties of any name can be provided.
129
+
130
+ WARNING: Make sure the additional properties don't contain any sensitive information, like customer data or PII.
131
+ For more information, see the Data Classification Standard at https://about.gitlab.com/handbook/security/data-classification-standard/
132
+
133
+ TEXT
134
+
135
+ ADDITIONAL_PROPERTIES_ADD_MORE_HELP = <<~TEXT.freeze
136
+ #{format_warning('Required. Must be unique within the event context. Must use only letters/numbers/underscores.')}
137
+
138
+ #{format_info('It should not be named any of the following:')}
139
+ - property#{' '}
140
+ - label
141
+ - value
142
+
143
+ TEXT
144
+
145
+ CLASSIFICATION_INTRO = <<~TEXT.freeze
146
+ #{format_info('EVENT CLASSIFICATION')}
147
+
148
+ The classification field is used to categorize events based on their data handling requirements.
149
+ Currently, the only supported classification is "duo" for AI and Duo-related features.
150
+
151
+ #{format_info('WHEN TO USE "classification: duo":')}
152
+ - Events related to GitLab Duo features (Duo Agent Platform, Code Suggestions, Duo Chat, etc.)
153
+ - AI-powered functionality and interactions
154
+ - Events owned by an AI Engineering product group such as duo_chat, ai_framework or duo_agent_framework
155
+
156
+ #{format_info('WHEN NOT TO USE "classification: duo":')}
157
+ - GitLab features unrelated to Duo or AI
158
+
159
+ Events with "classification: duo" are treated as operational data with specific data handling requirements.
160
+
161
+ Learn more: https://docs.gitlab.com/development/internal_analytics/internal_event_instrumentation/duo_classification/
162
+
163
+ TEXT
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabInternalEventsCli
4
+ module Text
5
+ module FlowAdvisor
6
+ extend Helpers::Formatting
7
+
8
+ ALTERNATE_RESOURCES_NOTICE = <<~TEXT.freeze
9
+ Other resources:
10
+
11
+ #{format_warning('Tracking GitLab feature usage from database info:')}
12
+ https://docs.gitlab.com/ee/development/internal_analytics/metrics/metrics_instrumentation.html#database-metrics
13
+
14
+ #{format_warning('Migrating existing metrics to use Internal Events:')}
15
+ https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/migration.html
16
+
17
+ #{format_warning('Remove an existing metric:')}
18
+ https://docs.gitlab.com/ee/development/internal_analytics/metrics/metrics_lifecycle.html
19
+
20
+ #{format_warning('Finding existing usage data for GitLab features:')}
21
+ https://metrics.gitlab.com/ (Customize Table > Snowflake query)
22
+ https://10az.online.tableau.com/#/site/gitlab/views/SnowplowEventExplorationLast30Days/SnowplowEventExplorationLast30D
23
+ https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricsExploration
24
+
25
+ #{format_warning('Customer wants usage data for their own GitLab instance:')}
26
+ https://docs.gitlab.com/ee/user/analytics/
27
+
28
+ #{format_warning('Customer wants usage data for their own products:')}
29
+ https://docs.gitlab.com/development/internal_analytics/product_analytics/
30
+ TEXT
31
+
32
+ EVENT_TRACKING_EXAMPLES = <<~TEXT
33
+ Product usage can be tracked in several ways.
34
+
35
+ By tracking events: ex) a user changes the assignee on an issue
36
+ ex) a user uploads a CI template
37
+ ex) a service desk request is received
38
+ ex) all stale runners are cleaned up
39
+ ex) a user copies code to the clipboard from markdown
40
+ ex) a user uploads an issue template OR a user uploads an MR template
41
+
42
+ From database data: ex) track whether each gitlab instance allows signups
43
+ ex) query how many projects are on each gitlab instance
44
+
45
+ TEXT
46
+
47
+ EVENT_EXISTENCE_CHECK_INSTRUCTIONS = <<~TEXT.freeze
48
+ To determine what to do next, let's figure out if the event is already tracked & usable.
49
+
50
+ If you're unsure whether an event exists, you can check the existing defintions.
51
+
52
+ #{format_info('FROM GDK')}: Check `config/events/` or `ee/config/events`
53
+ #{format_info('FROM BROWSER')}: Check https://metrics.gitlab.com/snowplow
54
+
55
+ Find one? Create a new metric for the event.
56
+ Otherwise? Create a new event.
57
+
58
+ If you find a relevant event that does not have the property `internal_events: true`, it can be migrated to
59
+ Internal Events. See https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/migration.html
60
+
61
+ TEXT
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabInternalEventsCli
4
+ module Text
5
+ module MetricDefiner
6
+ extend Helpers::Formatting
7
+
8
+ DATABASE_METRIC_NOTICE = <<~TEXT
9
+
10
+ For right now, this script can only define metrics for internal events.
11
+
12
+ For more info on instrumenting database-backed metrics, see https://docs.gitlab.com/ee/development/internal_analytics/metrics/metrics_instrumentation.html
13
+ TEXT
14
+
15
+ ALL_METRICS_EXIST_NOTICE = <<~TEXT
16
+
17
+ Looks like the potential metrics for this event either already exist or are unsupported.
18
+
19
+ Check out https://metrics.gitlab.com/ for improved event/metric search capabilities.
20
+ TEXT
21
+
22
+ EVENT_METRIC_DESCRIPTION_INTRO = <<~TEXT.freeze
23
+ #{format_info('METRIC DESCRIPTION')}
24
+ Describes which occurrences of an event are tracked in the metric and how they're grouped.
25
+
26
+ The description field is critical for helping others find & reuse this event. This will be used by Engineering, Product, Data team, Support -- and also GitLab customers directly. Be specific and explicit.
27
+
28
+ #{format_info('GOOD EXAMPLES:')}
29
+ - Count of analytics dashboard list views
30
+ - Count of unique users who viewed the analytics dashboard list
31
+ - Monthly count of unique projects where the analytics dashboard list was viewed
32
+ - Total count of issue updates
33
+
34
+ #{format_info('SELECTED EVENT(S):')}
35
+ TEXT
36
+
37
+ DATABASE_METRIC_DESCRIPTION_INTRO = <<~TEXT.freeze
38
+ #{format_info('METRIC DESCRIPTION')}
39
+ Describes what is calculated in the metric.
40
+
41
+ The description field is critical for helping others find this metric. This will be used by Engineering, Product, Data team, Support -- and also GitLab customers directly. Be specific and explicit.
42
+
43
+ #{format_info('GOOD EXAMPLES:')}
44
+ - Count of merge requests
45
+ - Count of users with admin permissions
46
+ - Gitlab version of the instance
47
+ TEXT
48
+
49
+ DESCRIPTION_HELP = <<~TEXT.freeze
50
+ #{format_warning('Required. 10+ words likely, but length may vary.')}
51
+
52
+ An event description can often be rearranged to work as a metric description.
53
+
54
+ ex) Event description: A merge request was created
55
+ Metric description: Total count of merge requests created
56
+ Metric description: Weekly count of unqiue users who created merge requests
57
+
58
+ Look at the event descriptions above to get ideas!
59
+ TEXT
60
+
61
+ NAME_FILTER_HELP = <<~TEXT.freeze
62
+ #{format_warning('Required. Max %<count>s characters. Only lowercase/numbers/underscores allowed.')}
63
+
64
+ Metrics with filters must manually define this portion of their key path.
65
+
66
+ Auto-generated key paths for metrics filters results in long & confusing naming. By defining them manually, clarity and discoverability should be better.
67
+ TEXT
68
+
69
+ NAME_CONFLICT_HELP = <<~TEXT.freeze
70
+ #{format_warning('Required. Max %<count>s characters. Only lowercase/numbers/underscores allowed.')}
71
+
72
+ Conflict! A metric with the same name already exists: %<name>s
73
+ TEXT
74
+
75
+ NAME_LENGTH_HELP = <<~TEXT.freeze
76
+ #{format_warning('Required. Max %<count>s characters. Only lowercase/numbers/underscores allowed.')}
77
+
78
+ Filenames cannot exceed 100 characters. The key path (ID) is not restricted, but keeping them aligned is recommended.
79
+
80
+ If needed, you can modify the key path and filename further after saving.
81
+ TEXT
82
+
83
+ DATABASE_METRIC_NAME_HELP = <<~TEXT.freeze
84
+ #{format_warning('Required. Max %<count>s characters. Only lowercase/numbers/underscores allowed.')}
85
+
86
+ Choose a name considering that it should be clear and discoverable.
87
+ TEXT
88
+
89
+ NAME_REQUIREMENT_REASONS = {
90
+ filters: {
91
+ text: 'Metrics using filters are too complex for default naming.',
92
+ help: NAME_FILTER_HELP
93
+ },
94
+ length: {
95
+ text: 'The default filename will be too long.',
96
+ help: NAME_LENGTH_HELP
97
+ },
98
+ conflict: {
99
+ text: 'The default key path is already in use.',
100
+ help: NAME_CONFLICT_HELP
101
+ },
102
+ database_metric: {
103
+ text: 'Database metrics have no default name.',
104
+ help: DATABASE_METRIC_NAME_HELP
105
+ }
106
+ }.freeze
107
+
108
+ NAME_ERROR = <<~TEXT.freeze
109
+ #{format_warning('Input is invalid. Max %<count>s characters. Only lowercase/numbers/underscores allowed. Ensure this key path (ID) is not already in use.')}
110
+ TEXT
111
+
112
+ INSTRUMENTATION_CLASS_INTRO = <<~TEXT.freeze
113
+ #{format_info('METRIC INSTRUMENATION CLASS')}
114
+ Choose a name for the Ruby class that will be used to calculate the metric.
115
+
116
+ #{format_info('GOOD EXAMPLES:')}
117
+ - CountSnippetsMetric
118
+ - UniqueInstanceIdMetric
119
+ - SnowplowEnabledMetric
120
+ TEXT
121
+
122
+ INSTRUMENTATION_CLASS_HELP = <<~TEXT.freeze
123
+ #{format_warning('Required.')}
124
+
125
+ An instrumentation class is the Ruby class that will be used for calculating the metric.
126
+
127
+ ex) IssuesCountMetric
128
+ CountSlackAppInstallationsMetric
129
+
130
+ Look at the `lib/gitlab/usage/metrics/instrumentations/` folder to get ideas!
131
+ TEXT
132
+
133
+ INSTRUMENTATION_CLASS_ERROR = <<~TEXT.freeze
134
+ #{format_warning('Input is invalid. Only lowercase/uppercase letters are allowed.')}
135
+ TEXT
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers for building time-framed metric key paths
4
+ module GitlabInternalEventsCli
5
+ class TimeFramedKeyPath
6
+ METRIC_TIME_FRAME_DESC = {
7
+ '7d' => 'weekly',
8
+ '28d' => 'monthly',
9
+ 'all' => 'total'
10
+ }.freeze
11
+
12
+ def self.build(base_key_path, time_frame)
13
+ return base_key_path if time_frame == 'all'
14
+
15
+ "#{base_key_path}_#{METRIC_TIME_FRAME_DESC[time_frame]}"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabInternalEventsCli
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require 'json_schemer'
5
+ require 'pastel'
6
+ require 'yaml'
7
+ require 'net/http'
8
+ require 'json'
9
+ require 'delegate'
10
+ require 'timeout'
11
+ require 'fileutils'
12
+
13
+ require_relative 'gitlab_internal_events_cli/version'
14
+ require_relative 'gitlab_internal_events_cli/configuration'
15
+ require_relative 'gitlab_internal_events_cli/http_cache'
16
+ require_relative 'gitlab_internal_events_cli/schema_resolver'
17
+ require_relative 'gitlab_internal_events_cli/time_framed_key_path'
18
+ require_relative 'gitlab_internal_events_cli/event'
19
+ require_relative 'gitlab_internal_events_cli/metric'
20
+ require_relative 'gitlab_internal_events_cli/global_state'
21
+ require_relative 'gitlab_internal_events_cli/helpers'
22
+ require_relative 'gitlab_internal_events_cli/gitlab_prompt'
23
+ require_relative 'gitlab_internal_events_cli/cli'
24
+ require_relative 'gitlab_internal_events_cli/text/event_definer'
25
+ require_relative 'gitlab_internal_events_cli/text/metric_definer'
26
+ require_relative 'gitlab_internal_events_cli/text/flow_advisor'
27
+ require_relative 'gitlab_internal_events_cli/flows/event_definer'
28
+ require_relative 'gitlab_internal_events_cli/flows/metric_definer'
29
+ require_relative 'gitlab_internal_events_cli/flows/flow_advisor'
30
+ require_relative 'gitlab_internal_events_cli/flows/usage_viewer'
31
+ require_relative 'gitlab_internal_events_cli/subflows/event_metric_definer'
32
+ require_relative 'gitlab_internal_events_cli/subflows/database_metric_definer'
33
+
34
+ module GitlabInternalEventsCli
35
+ class Error < StandardError; end
36
+ end