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,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers related to listing existing metric definitions
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module MetricOptions
|
|
7
|
+
# Creates a list of metrics to be used as options in a
|
|
8
|
+
# select/multiselect menu; existing metrics and metrics for
|
|
9
|
+
# unavailable identifiers are marked as disabled
|
|
10
|
+
#
|
|
11
|
+
# @param events [Array<ExistingEvent>]
|
|
12
|
+
# @return [Array<Hash>] hash (compact) has keys/values:
|
|
13
|
+
# value: [Array<NewMetric>]
|
|
14
|
+
# name: [String] formatted description of the metrics
|
|
15
|
+
# disabled: [String] reason metrics are disabled
|
|
16
|
+
def get_metric_options(events)
|
|
17
|
+
selection = EventSelection.new(events)
|
|
18
|
+
available_options = []
|
|
19
|
+
disabled_options = []
|
|
20
|
+
|
|
21
|
+
options = get_all_metric_options(selection.actions)
|
|
22
|
+
|
|
23
|
+
options.each do |metric|
|
|
24
|
+
# Filters & breaks up menu items based on existing metrics
|
|
25
|
+
# and supported functionality, appending formatted options
|
|
26
|
+
# to available_options and disabled_options arrays
|
|
27
|
+
collect_options!(metric, selection, available_options, disabled_options)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Push disabled options to the end for better skimability;
|
|
31
|
+
# retain relative order for continuity
|
|
32
|
+
available_options + disabled_options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Lists all potential metrics supported in service ping,
|
|
38
|
+
# ordered by: identifier > filters > time_frame
|
|
39
|
+
#
|
|
40
|
+
# @param actions [Array<String>] event names
|
|
41
|
+
# @return [Array<NewMetric>]
|
|
42
|
+
def get_all_metric_options(actions)
|
|
43
|
+
[
|
|
44
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'user' },
|
|
45
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'project' },
|
|
46
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'namespace' },
|
|
47
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'user', filters: [] },
|
|
48
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'project', filters: [] },
|
|
49
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'namespace', filters: [] },
|
|
50
|
+
{ time_frame: %w[7d 28d all], operator: 'count' },
|
|
51
|
+
{ time_frame: %w[7d 28d all], operator: 'count', filters: [] },
|
|
52
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'label' },
|
|
53
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'property' },
|
|
54
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'value' },
|
|
55
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'label', filters: [] },
|
|
56
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'property', filters: [] },
|
|
57
|
+
{ time_frame: %w[7d 28d], operator: 'unique_count', identifier: 'value', filters: [] },
|
|
58
|
+
{ time_frame: %w[7d 28d all], operator: 'sum', identifier: 'value' },
|
|
59
|
+
{ time_frame: %w[7d 28d all], operator: 'sum', identifier: 'value', filters: [] }
|
|
60
|
+
].map do |attributes|
|
|
61
|
+
Metric.new(**attributes, actions: actions)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def collect_options!(metric, selection, available_options, disabled_options)
|
|
66
|
+
identifier = metric.identifier.value
|
|
67
|
+
|
|
68
|
+
# Hide the filtered version of an option if unsupported; it just adds noise without value. Still,
|
|
69
|
+
# showing unsupported options is valuable, because it advertises possibilities and explains why
|
|
70
|
+
# those options aren't available.
|
|
71
|
+
return if metric.filtered? && !selection.supports_operations?(identifier)
|
|
72
|
+
return if metric.filtered? && !selection.can_filter_when_operated_on?(identifier)
|
|
73
|
+
return if selection.exclude_filter_identifier?(identifier)
|
|
74
|
+
|
|
75
|
+
option = Option.new(
|
|
76
|
+
metric: metric,
|
|
77
|
+
events_name: selection.events_name,
|
|
78
|
+
filter_name: (selection.filter_name(identifier) if metric.filtered?),
|
|
79
|
+
defined: false,
|
|
80
|
+
supported: true
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
conflicting_timeframes = get_conflicting_timeframes(metric)
|
|
84
|
+
available_timeframes = metric.time_frame.value - conflicting_timeframes
|
|
85
|
+
|
|
86
|
+
if conflicting_timeframes.any?
|
|
87
|
+
disabled_metric = metric.dup
|
|
88
|
+
disabled_metric[:time_frame] = conflicting_timeframes
|
|
89
|
+
|
|
90
|
+
disabled_option = option.dup
|
|
91
|
+
disabled_option.defined = true
|
|
92
|
+
disabled_option.metric = disabled_metric
|
|
93
|
+
|
|
94
|
+
disabled_options << disabled_option.formatted
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return if available_timeframes.none?
|
|
98
|
+
|
|
99
|
+
metric[:time_frame] = available_timeframes.sort
|
|
100
|
+
|
|
101
|
+
if selection.supports_operations?(identifier)
|
|
102
|
+
available_options << option.formatted
|
|
103
|
+
else
|
|
104
|
+
option.supported = false
|
|
105
|
+
disabled_options << option.formatted
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def get_conflicting_timeframes(metric)
|
|
110
|
+
return [] if metric.filters_expected?
|
|
111
|
+
|
|
112
|
+
cli.global.metrics.flat_map do |existing_metric|
|
|
113
|
+
next if existing_metric.filtered?
|
|
114
|
+
next unless (existing_metric.unique_ids & metric.unique_ids).any?
|
|
115
|
+
|
|
116
|
+
existing_metric.time_frame
|
|
117
|
+
end.compact.uniq
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Represents the attributes of set of events that depend on
|
|
121
|
+
# the other events in the set
|
|
122
|
+
EventSelection = Struct.new(:events) do
|
|
123
|
+
def actions
|
|
124
|
+
events.map(&:action)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Very brief summary of the provided events to use in a
|
|
128
|
+
# basic description of the metric
|
|
129
|
+
# This ignores filters for simplicity & skimability
|
|
130
|
+
def events_name
|
|
131
|
+
return actions.first if actions.length == 1
|
|
132
|
+
|
|
133
|
+
"any of #{actions.length} events"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Formatted list of filter options for these events, given
|
|
137
|
+
# the provided uniqueness constraint
|
|
138
|
+
def filter_name(identifier)
|
|
139
|
+
filter_options.difference([identifier]).join('/')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# We accept different filters for each event, so we want
|
|
143
|
+
# any filter options available for any event
|
|
144
|
+
def filter_options
|
|
145
|
+
events.flat_map(&:available_filters).uniq
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# We require the same uniqueness constraint for all events,
|
|
149
|
+
# so we want only the options they have in common
|
|
150
|
+
def operable_identifiers
|
|
151
|
+
[*shared_identifiers, *shared_filters, nil]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Whether there are any filtering options other than the
|
|
155
|
+
# selected uniqueness constraint
|
|
156
|
+
def can_filter_when_operated_on?(identifier)
|
|
157
|
+
supports_operations?(identifier) && filter_options.difference([identifier]).any?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Whether the given identifier is available for all events
|
|
161
|
+
# and can be used as a uniqueness constraint
|
|
162
|
+
def supports_operations?(identifier)
|
|
163
|
+
operable_identifiers.include?(identifier)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Common values for identifiers shared across all the events
|
|
167
|
+
def shared_identifiers
|
|
168
|
+
events.map(&:identifiers).reduce(&:&)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Common values for filters shared across all the events
|
|
172
|
+
def shared_filters
|
|
173
|
+
events.map(&:available_filters).reduce(&:&)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Whether none of the events have additional properties
|
|
177
|
+
# and the given identifier is an additional property.
|
|
178
|
+
# In this case, it makes sense to exclude these from the
|
|
179
|
+
# menu to keep the flow simple when the use-case is simple
|
|
180
|
+
def exclude_filter_identifier?(identifier)
|
|
181
|
+
return false if identifier.nil? || Metric::Identifier.new(identifier).default?
|
|
182
|
+
|
|
183
|
+
filter_options.empty?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Formats & structures a single select/multiselect menu item
|
|
188
|
+
#
|
|
189
|
+
# @param identifier [String, nil] if present, used in unique-by-identifier metrics
|
|
190
|
+
# @param events_name [String] how the selected events will be referred to as a group
|
|
191
|
+
# @param filter_name [String] how the potential filters will be referred to as a group
|
|
192
|
+
# @param metrics [Array<NewMetric>]
|
|
193
|
+
# @option defined [Boolean] whether this metric already exists
|
|
194
|
+
# @option supported [Boolean] whether unique metrics are supported for this identifier
|
|
195
|
+
Option = Struct.new(:events_name, :filter_name, :metric, :defined, :supported,
|
|
196
|
+
keyword_init: true) do
|
|
197
|
+
include GitlabInternalEventsCli::Helpers::Formatting
|
|
198
|
+
|
|
199
|
+
# @return [Hash] see #get_metric_options for format
|
|
200
|
+
# ex) Monthly/Weekly count of unique users who triggered cli_template_included where label/property is...
|
|
201
|
+
# ex) Monthly/Weekly count of unique users who triggered cli_template_included (user unavailable)
|
|
202
|
+
def formatted
|
|
203
|
+
name = [time_frame_phrase, operator_phrase, filter_phrase].compact.join(' ')
|
|
204
|
+
name = format_help(name) if disabled
|
|
205
|
+
|
|
206
|
+
{ name: name, disabled: disabled, value: metric }.compact
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def identifier
|
|
210
|
+
metric.identifier
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# ex) "Monthly/Weekly"
|
|
214
|
+
def time_frame_phrase
|
|
215
|
+
phrase = metric.time_frame.description
|
|
216
|
+
|
|
217
|
+
disabled ? phrase : format_info(phrase)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# ex) "count of unique users who triggered cli_template_included"
|
|
221
|
+
def operator_phrase
|
|
222
|
+
phrase = "#{metric.operator.description} #{identifier.description % events_name}"
|
|
223
|
+
phrase.gsub!(highlighted_phrase, format_info(highlighted_phrase)) unless disabled
|
|
224
|
+
|
|
225
|
+
phrase
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# ex) "unique users"
|
|
229
|
+
def highlighted_phrase
|
|
230
|
+
"#{metric.operator.qualifier} #{identifier.plural}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# ex) "where label/property is..."
|
|
234
|
+
def filter_phrase
|
|
235
|
+
return unless filter_name
|
|
236
|
+
return 'where filtered' if disabled
|
|
237
|
+
|
|
238
|
+
"#{format_info("where #{filter_name}")} is..."
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Returns the string to include at the end of disabled
|
|
242
|
+
# menu items. Nil if menu item shouldn't be disabled
|
|
243
|
+
def disabled
|
|
244
|
+
if defined
|
|
245
|
+
pastel.bold(format_help('(already defined)'))
|
|
246
|
+
elsif !supported
|
|
247
|
+
pastel.bold(format_help("(#{identifier.value} unavailable)"))
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabInternalEventsCli
|
|
4
|
+
module Helpers
|
|
5
|
+
# Helper module for loading JSON schemas with caching and reference resolution
|
|
6
|
+
module SchemaLoader
|
|
7
|
+
def self.load(schema_url, schema_type)
|
|
8
|
+
schema_json = HttpCache.get(schema_url)
|
|
9
|
+
return nil unless schema_json
|
|
10
|
+
|
|
11
|
+
base_uri = "#{File.dirname(schema_url)}/"
|
|
12
|
+
|
|
13
|
+
resolver = SchemaResolver.new(base_uri)
|
|
14
|
+
JSONSchemer.schema(
|
|
15
|
+
JSON.parse(schema_json),
|
|
16
|
+
base_uri: URI(base_uri),
|
|
17
|
+
ref_resolver: resolver
|
|
18
|
+
)
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
warn "Failed to load #{schema_type} schema: #{e.message}"
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers for generating service ping exploration dashboards links
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module ServicePingDashboards
|
|
7
|
+
def metric_exploration_group_path(product_group, stage_name)
|
|
8
|
+
"#{tableau_base_path}/MetricExplorationbyGroup?Group%20Name=#{product_group}&Stage%20Name=#{stage_name}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def metric_trend_path(key_path)
|
|
12
|
+
"#{tableau_base_path}/MetricTrend?Metrics%20Path=#{key_path}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def tableau_base_path
|
|
18
|
+
@tableau_base_path ||= 'https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'helpers/cli_inputs'
|
|
4
|
+
require_relative 'helpers/files'
|
|
5
|
+
require_relative 'helpers/formatting'
|
|
6
|
+
require_relative 'helpers/group_ownership'
|
|
7
|
+
require_relative 'helpers/event_options'
|
|
8
|
+
require_relative 'helpers/metric_options'
|
|
9
|
+
require_relative 'helpers/schema_loader'
|
|
10
|
+
require_relative 'helpers/service_ping_dashboards'
|
|
11
|
+
|
|
12
|
+
module GitlabInternalEventsCli
|
|
13
|
+
module Helpers
|
|
14
|
+
include CliInputs
|
|
15
|
+
include Files
|
|
16
|
+
include Formatting
|
|
17
|
+
include GroupOwnership
|
|
18
|
+
include EventOptions
|
|
19
|
+
include MetricOptions
|
|
20
|
+
include ServicePingDashboards
|
|
21
|
+
|
|
22
|
+
NAME_REGEX = /\A[a-z0-9_]+\z/
|
|
23
|
+
|
|
24
|
+
def milestone
|
|
25
|
+
GitlabInternalEventsCli.configuration.milestone
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def new_page!(on_step: nil, steps: [])
|
|
29
|
+
cli.say TTY::Cursor.clear_screen
|
|
30
|
+
cli.say TTY::Cursor.move_to(0, 0)
|
|
31
|
+
cli.say "#{progress_bar(on_step, steps)}\n" if on_step && steps&.any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def feedback_notice
|
|
35
|
+
format_heading <<~TEXT.chomp
|
|
36
|
+
Thanks for using the Internal Events CLI!
|
|
37
|
+
|
|
38
|
+
Please reach out with any feedback!
|
|
39
|
+
About Internal Events: https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/internal/-/issues/687
|
|
40
|
+
About CLI: https://gitlab.com/gitlab-org/gitlab/-/issues/434038
|
|
41
|
+
In Slack: #g_analyze_analytics_instrumentation
|
|
42
|
+
|
|
43
|
+
Let us know that you used the CLI! React with 👍 on the feedback issue or post in Slack!
|
|
44
|
+
TEXT
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabInternalEventsCli
|
|
4
|
+
class HttpCache
|
|
5
|
+
class << self
|
|
6
|
+
def instance
|
|
7
|
+
@instance ||= new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get(url, timeout: 5)
|
|
11
|
+
instance.get(url, timeout: timeout)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clear!
|
|
15
|
+
@instance = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def preload!
|
|
19
|
+
instance.preload!
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@cache = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get(url, timeout: 5)
|
|
28
|
+
return @cache[url] if @cache.key?(url)
|
|
29
|
+
|
|
30
|
+
@cache[url] = Timeout.timeout(timeout) { Net::HTTP.get(URI(url)) }
|
|
31
|
+
rescue StandardError
|
|
32
|
+
@cache[url] = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def preload!
|
|
36
|
+
config = GitlabInternalEventsCli.configuration
|
|
37
|
+
|
|
38
|
+
urls = [
|
|
39
|
+
config.event_schema_url,
|
|
40
|
+
config.metric_schema_url,
|
|
41
|
+
config.stages_url,
|
|
42
|
+
config.feature_categories_url
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
threads = urls.map do |url|
|
|
46
|
+
Thread.new { get(url) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
threads.each(&:join)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|