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