gitlab_internal_events_cli 0.0.1 → 0.1.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 +4 -4
- data/README.md +21 -25
- data/lib/gitlab_internal_events_cli/cli.rb +29 -0
- data/lib/gitlab_internal_events_cli/configuration.rb +10 -0
- data/lib/gitlab_internal_events_cli/flows/event_definer.rb +20 -1
- data/lib/gitlab_internal_events_cli/flows/flow_advisor.rb +34 -2
- data/lib/gitlab_internal_events_cli/flows/metric_definer.rb +76 -12
- data/lib/gitlab_internal_events_cli/flows/usage_viewer.rb +121 -21
- data/lib/gitlab_internal_events_cli/helpers/event_options.rb +36 -0
- data/lib/gitlab_internal_events_cli/helpers/files.rb +14 -7
- data/lib/gitlab_internal_events_cli/metric.rb +122 -1
- data/lib/gitlab_internal_events_cli/subflows/database_metric_definer.rb +34 -1
- data/lib/gitlab_internal_events_cli/text/flow_advisor.rb +13 -0
- data/lib/gitlab_internal_events_cli/text/metric_definer.rb +26 -11
- data/lib/gitlab_internal_events_cli/version.rb +1 -1
- data/lib/gitlab_internal_events_cli/version_checker.rb +40 -0
- data/lib/gitlab_internal_events_cli.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 488d06f3e32cbc1e6c1a32bb6cf214181762cda5078069f725892cd9d65a523b
|
|
4
|
+
data.tar.gz: d05a91f4a982dd1cc3941a0d61f4333054cb8cd6f2d9f5be69bd940926be3a4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 123411c52cbd00083d1bef970f4c2361c4c4b8ddebb06f4a736ddf4f11188ebbfbc4cb9cf9141ac17099bfd75911caf29290244f30c31ba62147271ce7fcdcff
|
|
7
|
+
data.tar.gz: bcf890d1283412da086d3bdee6eb6b91d8b49ed3a2f0c6bbe9a85fa433dcf8e6bf085ebba204831fc85be132f88c264bd979537b496bf90789de89c37af7def4
|
data/README.md
CHANGED
|
@@ -14,19 +14,7 @@ This gem extracts the Internal Events CLI from the GitLab monorepo, making it an
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
gem 'gitlab_internal_events_cli', path: 'path/to/this/repo'
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
And then execute:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
bundle install
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Or install it yourself as:
|
|
17
|
+
Install the gem globally as:
|
|
30
18
|
|
|
31
19
|
```bash
|
|
32
20
|
gem install gitlab_internal_events_cli
|
|
@@ -36,18 +24,34 @@ gem install gitlab_internal_events_cli
|
|
|
36
24
|
|
|
37
25
|
### Running the CLI
|
|
38
26
|
|
|
39
|
-
From the root of a
|
|
27
|
+
From the root of a project:
|
|
40
28
|
|
|
41
29
|
```bash
|
|
42
|
-
|
|
30
|
+
gem exec gitlab_internal_events_cli
|
|
43
31
|
```
|
|
44
32
|
|
|
45
33
|
The CLI provides an interactive menu to:
|
|
46
34
|
- **New Event** - Define a new event to track specific scenarios
|
|
47
|
-
- **New Metric** - Define metrics that count events over time
|
|
35
|
+
- **New Metric** - Define metrics that count events over time, or that are derived from the database
|
|
48
36
|
- **View Usage** - See code examples for existing events
|
|
49
37
|
- **Help/Flow Advisor** - Get guidance on which tool to use
|
|
50
38
|
|
|
39
|
+
### Database metrics
|
|
40
|
+
|
|
41
|
+
Selecting **Database** at the metric-type prompt scaffolds three files in one go:
|
|
42
|
+
|
|
43
|
+
- A YAML metric definition under `config/metrics/counts_*/` (or `ee/config/metrics/counts_*/` for non-`free` tiers).
|
|
44
|
+
- A Ruby instrumentation class under `lib/gitlab/usage/metrics/instrumentations/` (or `ee/lib/...`).
|
|
45
|
+
- A matching RSpec file under `spec/lib/gitlab/usage/metrics/instrumentations/` (or `ee/spec/lib/...`)
|
|
46
|
+
using the `a correct instrumented metric value and query` shared example.
|
|
47
|
+
|
|
48
|
+
You will be prompted for the instrumentation class name and the operation
|
|
49
|
+
(`count`, `distinct_count`, `estimate_batch_distinct_count`, `sum`, `average`).
|
|
50
|
+
After generation, fill in the ActiveRecord relation in the class and the
|
|
51
|
+
`expected_value` / `expected_query` in the spec. See the
|
|
52
|
+
[database metrics documentation](https://docs.gitlab.com/development/internal_analytics/metrics/metrics_instrumentation/#database-metrics)
|
|
53
|
+
for details.
|
|
54
|
+
|
|
51
55
|
### Configuration
|
|
52
56
|
|
|
53
57
|
The CLI can be configured via a `.gitlab_internal_events_cli.yml` file in your project root:
|
|
@@ -90,7 +94,7 @@ The CLI expects the following structure (configurable):
|
|
|
90
94
|
|
|
91
95
|
```
|
|
92
96
|
project/
|
|
93
|
-
├── VERSION # Milestone detection
|
|
97
|
+
├── VERSION # Milestone detection (optional)
|
|
94
98
|
├── config/
|
|
95
99
|
│ ├── events/*.yml # Event definitions
|
|
96
100
|
│ ├── metrics/
|
|
@@ -151,14 +155,6 @@ Alternatively, run directly from the repo without installing the gem:
|
|
|
151
155
|
bundle exec exe/gitlab-internal-events-cli
|
|
152
156
|
```
|
|
153
157
|
|
|
154
|
-
## Migration from GitLab Monorepo
|
|
155
|
-
|
|
156
|
-
This gem was extracted from the original CLI at `gitlab/scripts/internal_events/cli.rb`. Key changes:
|
|
157
|
-
|
|
158
|
-
1. **Configuration System**: All hardcoded paths are now configurable
|
|
159
|
-
2. **Standalone Gem**: Can be used outside the GitLab monorepo with proper configuration
|
|
160
|
-
3. **Module Renamed**: `InternalEventsCli` → `GitlabInternalEventsCli`
|
|
161
|
-
|
|
162
158
|
## License
|
|
163
159
|
|
|
164
160
|
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -14,6 +14,7 @@ module GitlabInternalEventsCli
|
|
|
14
14
|
HttpCache.preload!
|
|
15
15
|
|
|
16
16
|
cli.say feedback_notice
|
|
17
|
+
check_for_updates
|
|
17
18
|
cli.say instructions
|
|
18
19
|
|
|
19
20
|
task = cli.select('What would you like to do?', **select_opts) do |menu|
|
|
@@ -39,6 +40,34 @@ module GitlabInternalEventsCli
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def check_for_updates
|
|
46
|
+
checker = VersionChecker.new
|
|
47
|
+
return unless checker.update_available?
|
|
48
|
+
|
|
49
|
+
cli.say format_warning(
|
|
50
|
+
"\nUpdate available! Current version: #{VERSION}, Latest version: #{checker.latest_version}\n"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return unless cli.yes?(
|
|
54
|
+
'Would you like to update now?',
|
|
55
|
+
**yes_no_opts
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
cli.say format_info("\nUpdating #{VersionChecker::GEM_NAME}...\n")
|
|
59
|
+
|
|
60
|
+
if checker.self_update!
|
|
61
|
+
cli.say format_selection("\nUpdate successful! Please restart the CLI to use the new version.\n")
|
|
62
|
+
exit 0
|
|
63
|
+
else
|
|
64
|
+
cli.say format_error("\nUpdate failed. You can update manually with: " \
|
|
65
|
+
"gem install #{VersionChecker::GEM_NAME}\n")
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
42
71
|
def instructions
|
|
43
72
|
cli.say <<~TEXT.freeze
|
|
44
73
|
#{format_info('INSTRUCTIONS:')}
|
|
@@ -42,6 +42,12 @@ module GitlabInternalEventsCli
|
|
|
42
42
|
@event_schema_url = DEFAULT_EVENT_SCHEMA_URL
|
|
43
43
|
@metric_schema_url = DEFAULT_METRIC_SCHEMA_URL
|
|
44
44
|
@stages_url = DEFAULT_STAGES_URL
|
|
45
|
+
@python_filepaths = [
|
|
46
|
+
'poetry.lock',
|
|
47
|
+
'pyproject.toml',
|
|
48
|
+
'.python-version',
|
|
49
|
+
'requirements.txt'
|
|
50
|
+
]
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
def milestone
|
|
@@ -68,6 +74,10 @@ module GitlabInternalEventsCli
|
|
|
68
74
|
apply_config(config)
|
|
69
75
|
end
|
|
70
76
|
|
|
77
|
+
def python_project?
|
|
78
|
+
@python_filepaths.any? { |f| File.exist?(absolute_path(f)) }
|
|
79
|
+
end
|
|
80
|
+
|
|
71
81
|
private
|
|
72
82
|
|
|
73
83
|
def apply_config(config)
|
|
@@ -27,6 +27,8 @@ module GitlabInternalEventsCli
|
|
|
27
27
|
'Save files'
|
|
28
28
|
].freeze
|
|
29
29
|
|
|
30
|
+
DUO_EVENT_CLASS = 'duo'
|
|
31
|
+
|
|
30
32
|
IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.map { |k| k.join(', ') }.max_by(&:length)}]".length
|
|
31
33
|
|
|
32
34
|
attr_reader :cli, :event
|
|
@@ -237,7 +239,7 @@ module GitlabInternalEventsCli
|
|
|
237
239
|
menu.choice 'No - this event is not related to AI/Duo features', :none
|
|
238
240
|
end
|
|
239
241
|
|
|
240
|
-
event.classification =
|
|
242
|
+
event.classification = DUO_EVENT_CLASS if classification_choice == :duo
|
|
241
243
|
end
|
|
242
244
|
|
|
243
245
|
def create_event_file
|
|
@@ -255,6 +257,7 @@ module GitlabInternalEventsCli
|
|
|
255
257
|
|
|
256
258
|
#{outcome || ' No files saved.'}
|
|
257
259
|
|
|
260
|
+
#{ai_tracking_suggestion}#{python_codebase_advice}
|
|
258
261
|
#{divider}
|
|
259
262
|
|
|
260
263
|
Do you need to create a metric? Probably!
|
|
@@ -301,6 +304,22 @@ module GitlabInternalEventsCli
|
|
|
301
304
|
cli.say feedback_notice
|
|
302
305
|
end
|
|
303
306
|
end
|
|
307
|
+
|
|
308
|
+
def ai_tracking_suggestion
|
|
309
|
+
return unless duo_event?
|
|
310
|
+
|
|
311
|
+
generate_ai_event_suggestion(event.action)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def python_codebase_advice
|
|
315
|
+
return unless GitlabInternalEventsCli.configuration.python_project?
|
|
316
|
+
|
|
317
|
+
generate_python_advice
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def duo_event?
|
|
321
|
+
event.classification == DUO_EVENT_CLASS
|
|
322
|
+
end
|
|
304
323
|
end
|
|
305
324
|
end
|
|
306
325
|
end
|
|
@@ -16,9 +16,14 @@ module GitlabInternalEventsCli
|
|
|
16
16
|
|
|
17
17
|
def run
|
|
18
18
|
return use_case_error unless goal_is_tracking_usage?
|
|
19
|
-
return use_case_error unless usage_trackable_with_internal_events?
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
if usage_trackable_with_internal_events?
|
|
21
|
+
event_already_tracked? ? proceed_to_metric_definition : proceed_to_event_definition
|
|
22
|
+
elsif usage_trackable_from_database?
|
|
23
|
+
proceed_to_database_metric_definition
|
|
24
|
+
else
|
|
25
|
+
use_case_error
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
private
|
|
@@ -44,6 +49,19 @@ module GitlabInternalEventsCli
|
|
|
44
49
|
)
|
|
45
50
|
end
|
|
46
51
|
|
|
52
|
+
def usage_trackable_from_database?
|
|
53
|
+
new_page!
|
|
54
|
+
|
|
55
|
+
cli.say format_info("Got it! Let's check whether a database metric fits.\n")
|
|
56
|
+
cli.say DATABASE_METRIC_EXAMPLES
|
|
57
|
+
|
|
58
|
+
cli.yes?(
|
|
59
|
+
'Can the data for this metric be derived from a database query (e.g. a count, sum, ' \
|
|
60
|
+
'distinct count or average over an ActiveRecord relation)?',
|
|
61
|
+
**yes_no_opts
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
47
65
|
def event_already_tracked?
|
|
48
66
|
new_page!
|
|
49
67
|
|
|
@@ -81,6 +99,20 @@ module GitlabInternalEventsCli
|
|
|
81
99
|
EventDefiner.new(cli).run
|
|
82
100
|
end
|
|
83
101
|
|
|
102
|
+
def proceed_to_database_metric_definition
|
|
103
|
+
new_page!
|
|
104
|
+
|
|
105
|
+
cli.say format_info("Great! The next step is adding a database metric.\n")
|
|
106
|
+
cli.say(
|
|
107
|
+
'When prompted for the metric type, choose ' \
|
|
108
|
+
"#{format_info('Database')} to scaffold the YAML, Ruby class, and spec files.\n"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return not_ready_error('New Metric') unless cli.yes?(format_prompt('Ready to start?'))
|
|
112
|
+
|
|
113
|
+
MetricDefiner.new(cli).run
|
|
114
|
+
end
|
|
115
|
+
|
|
84
116
|
def not_ready_error(description)
|
|
85
117
|
cli.say "\nNo problem! When you're ready, run the CLI & select '#{description}'\n"
|
|
86
118
|
cli.say feedback_notice
|
|
@@ -92,13 +92,6 @@ module GitlabInternalEventsCli
|
|
|
92
92
|
def prompt_for_configuration(type)
|
|
93
93
|
case type
|
|
94
94
|
when :database_metric
|
|
95
|
-
# CLI doesn't load rails, so perform a simplified string <-> boolean check
|
|
96
|
-
if [nil, 'false', '0'].include? ENV['ENABLE_DATABASE_METRIC']
|
|
97
|
-
cli.error DATABASE_METRIC_NOTICE
|
|
98
|
-
cli.say feedback_notice
|
|
99
|
-
return
|
|
100
|
-
end
|
|
101
|
-
|
|
102
95
|
db_metric_definer = Subflows::DatabaseMetricDefiner.new(cli)
|
|
103
96
|
db_metric_definer.run
|
|
104
97
|
@metric = db_metric_definer.metric
|
|
@@ -224,7 +217,31 @@ module GitlabInternalEventsCli
|
|
|
224
217
|
cli.say format_prompt(format_subheader('SAVING FILE', metric.description))
|
|
225
218
|
cli.say "\n"
|
|
226
219
|
|
|
227
|
-
prompt_to_save_file(metric.file_path, metric.formatted_output)
|
|
220
|
+
yaml_outcome = prompt_to_save_file(metric.file_path, metric.formatted_output)
|
|
221
|
+
|
|
222
|
+
return yaml_outcome unless metric.database_metric?
|
|
223
|
+
|
|
224
|
+
instrumentation_outcome = save_instrumentation_class_files
|
|
225
|
+
|
|
226
|
+
[yaml_outcome, instrumentation_outcome].compact.join("\n")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# For database metrics, also scaffold the Ruby instrumentation class
|
|
230
|
+
# and a matching spec file. These mirror the output of the upstream
|
|
231
|
+
# `rails generate gitlab:usage_metric` generator.
|
|
232
|
+
def save_instrumentation_class_files
|
|
233
|
+
targets = [
|
|
234
|
+
[metric.instrumentation_class_file_path, metric.instrumentation_class_content],
|
|
235
|
+
[metric.instrumentation_class_spec_file_path, metric.instrumentation_class_spec_content]
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
targets.filter_map do |filepath, content|
|
|
239
|
+
cli.say "\n"
|
|
240
|
+
cli.say format_prompt(format_subheader('SAVING FILE', filepath))
|
|
241
|
+
cli.say "\n"
|
|
242
|
+
|
|
243
|
+
prompt_to_save_file(filepath, content)
|
|
244
|
+
end.join("\n")
|
|
228
245
|
end
|
|
229
246
|
|
|
230
247
|
def show_all_metric_paths(metric)
|
|
@@ -248,6 +265,7 @@ module GitlabInternalEventsCli
|
|
|
248
265
|
|
|
249
266
|
event_metric_message = "\n Have you instrumented the application code to trigger the event yet? " \
|
|
250
267
|
"View usage examples to easily copy/paste implementation!\n"
|
|
268
|
+
database_metric_message = "\n#{format_prefix(' ', DATABASE_INSTRUMENTATION_FILES_NEXT_STEPS)}"
|
|
251
269
|
cli.say <<~TEXT
|
|
252
270
|
#{divider}
|
|
253
271
|
#{format_info('Done with metric definitions!')}
|
|
@@ -255,6 +273,7 @@ module GitlabInternalEventsCli
|
|
|
255
273
|
#{outcome}
|
|
256
274
|
#{divider}
|
|
257
275
|
#{event_metric_message if metric.event_metric?}
|
|
276
|
+
#{database_metric_message if metric.database_metric?}
|
|
258
277
|
Want to verify the metrics? Check out the group::#{metric[:product_group]} Metrics Exploration Dashboard in Tableau
|
|
259
278
|
Note: The Metrics Exploration Dashboard data would be available ~1 week after deploy for Gitlab.com, ~1 week after next release for self-managed
|
|
260
279
|
Link: #{format_info(metric_exploration_group_path(metric[:product_group], find_stage(metric.product_group)))}
|
|
@@ -339,23 +358,34 @@ module GitlabInternalEventsCli
|
|
|
339
358
|
|
|
340
359
|
return unless name_reason
|
|
341
360
|
|
|
342
|
-
|
|
343
|
-
|
|
361
|
+
suggested_name = suggested_metric_name
|
|
362
|
+
default_name = suggested_name || metric.key.value
|
|
363
|
+
display_name = metric.key.value(suggested_name || "\e[0m[REPLACE ME]\e[36m")
|
|
344
364
|
empty_name = metric.key.value('')
|
|
345
365
|
max_length = MAX_FILENAME_LENGTH - "#{empty_name}.yml".length
|
|
346
|
-
help_tokens = {
|
|
366
|
+
help_tokens = {
|
|
367
|
+
name: default_name,
|
|
368
|
+
count: max_length,
|
|
369
|
+
class_name: metric.instrumentation_class.to_s,
|
|
370
|
+
suggested_name: suggested_name || '(none — please enter a snake_case name)'
|
|
371
|
+
}
|
|
347
372
|
|
|
348
373
|
cli.say <<~TEXT
|
|
349
374
|
|
|
350
|
-
#{input_opts[:prefix]} #{name_reason[:text]} How should we
|
|
375
|
+
#{input_opts[:prefix]} #{name_reason[:text]} How should we reference this metric? #{input_required_text}
|
|
351
376
|
|
|
352
377
|
ID: #{format_info(display_name)}
|
|
353
378
|
Filename: #{format_info(display_name)}#{format_info('.yml')}
|
|
354
379
|
|
|
380
|
+
#{metric_name_format_hint(suggested_name)}
|
|
355
381
|
TEXT
|
|
356
382
|
|
|
357
383
|
metric.key = prompt_for_text(' Replace with: ', multiline: true) do |q|
|
|
358
384
|
q.required true
|
|
385
|
+
# Use #default (not #value) so the suggested name is accepted on
|
|
386
|
+
# blank Enter but does NOT prefill the input buffer — prefilling
|
|
387
|
+
# would concatenate the user's typing onto the suggestion.
|
|
388
|
+
q.default suggested_name if suggested_name
|
|
359
389
|
q.messages[:required?] = name_reason[:help] % help_tokens
|
|
360
390
|
q.messages[:valid?] = NAME_ERROR % help_tokens
|
|
361
391
|
q.validate lambda { |input|
|
|
@@ -366,6 +396,40 @@ module GitlabInternalEventsCli
|
|
|
366
396
|
end
|
|
367
397
|
end
|
|
368
398
|
|
|
399
|
+
# Inline hint printed alongside the metric name prompt. For database
|
|
400
|
+
# metrics this is critical context: users have just entered a CamelCase
|
|
401
|
+
# instrumentation class name (e.g. `CountIssuesMetric`), and without
|
|
402
|
+
# this hint they typically retype it here and hit the snake_case
|
|
403
|
+
# validation.
|
|
404
|
+
def metric_name_format_hint(suggested_name)
|
|
405
|
+
return '' unless metric.database_metric?
|
|
406
|
+
|
|
407
|
+
suggestion_line =
|
|
408
|
+
if suggested_name
|
|
409
|
+
" Suggested: #{format_info(suggested_name)} (press Enter to accept)"
|
|
410
|
+
else
|
|
411
|
+
' Format: snake_case — lowercase letters, numbers, and underscores only'
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
<<~HINT.chomp
|
|
415
|
+
#{format_help('Use snake_case here — this is the metric key/filename, not the Ruby class name.')}
|
|
416
|
+
#{suggestion_line}
|
|
417
|
+
HINT
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# For database metrics, propose a snake_case default derived from
|
|
421
|
+
# the instrumentation class. Returns nil for event metrics and when
|
|
422
|
+
# the derived value is empty or not unique in the project.
|
|
423
|
+
def suggested_metric_name
|
|
424
|
+
return unless metric.database_metric?
|
|
425
|
+
|
|
426
|
+
suggestion = metric.default_name_from_instrumentation_class
|
|
427
|
+
return if suggestion.nil? || suggestion.empty?
|
|
428
|
+
return if conflicting_key_path?(metric.key.value(suggestion))
|
|
429
|
+
|
|
430
|
+
suggestion
|
|
431
|
+
end
|
|
432
|
+
|
|
369
433
|
# Helper for #prompt_for_description
|
|
370
434
|
def name_requirement_reason
|
|
371
435
|
if metric.filters.assigned?
|
|
@@ -12,6 +12,22 @@ module GitlabInternalEventsCli
|
|
|
12
12
|
'property' => "'string'",
|
|
13
13
|
'value' => '72'
|
|
14
14
|
}.freeze
|
|
15
|
+
|
|
16
|
+
CHOICES = [
|
|
17
|
+
{ name: '1. ruby/rails', value: :rails },
|
|
18
|
+
{ name: '2. rspec', value: :rspec },
|
|
19
|
+
{ name: '3. python', value: :python },
|
|
20
|
+
{ name: '4. pytest', value: :pytest },
|
|
21
|
+
{ name: '5. javascript (vue)', value: :vue },
|
|
22
|
+
{ name: '6. javascript (plain)', value: :js },
|
|
23
|
+
{ name: '7. vue template', value: :vue_template },
|
|
24
|
+
{ name: '8. haml', value: :haml },
|
|
25
|
+
{ name: '9. Manual testing in GDK', value: :gdk },
|
|
26
|
+
{ name: '10. Data verification in Tableau', value: :tableau },
|
|
27
|
+
{ name: '11. View examples for a different event', value: :other_event },
|
|
28
|
+
{ name: '12. Exit', value: :exit }
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
15
31
|
DEFAULT_PROPERTY_VALUE = "'custom_value'"
|
|
16
32
|
|
|
17
33
|
attr_reader :cli, :event
|
|
@@ -43,25 +59,12 @@ module GitlabInternalEventsCli
|
|
|
43
59
|
end
|
|
44
60
|
|
|
45
61
|
def prompt_for_usage_location(default = '1. ruby/rails')
|
|
46
|
-
choices = [
|
|
47
|
-
{ name: '1. ruby/rails', value: :rails },
|
|
48
|
-
{ name: '2. rspec', value: :rspec },
|
|
49
|
-
{ name: '3. javascript (vue)', value: :vue },
|
|
50
|
-
{ name: '4. javascript (plain)', value: :js },
|
|
51
|
-
{ name: '5. vue template', value: :vue_template },
|
|
52
|
-
{ name: '6. haml', value: :haml },
|
|
53
|
-
{ name: '7. Manual testing in GDK', value: :gdk },
|
|
54
|
-
{ name: '8. Data verification in Tableau', value: :tableau },
|
|
55
|
-
{ name: '9. View examples for a different event', value: :other_event },
|
|
56
|
-
{ name: '10. Exit', value: :exit }
|
|
57
|
-
]
|
|
58
|
-
|
|
59
62
|
usage_location = cli.select(
|
|
60
63
|
'Select a use-case to view examples for:',
|
|
61
|
-
|
|
64
|
+
CHOICES,
|
|
62
65
|
**select_opts,
|
|
63
66
|
**filter_opts,
|
|
64
|
-
per_page:
|
|
67
|
+
per_page: CHOICES.count
|
|
65
68
|
) do |menu|
|
|
66
69
|
menu.default default
|
|
67
70
|
end
|
|
@@ -73,24 +76,30 @@ module GitlabInternalEventsCli
|
|
|
73
76
|
when :rspec
|
|
74
77
|
rspec_examples
|
|
75
78
|
prompt_for_usage_location('2. rspec')
|
|
79
|
+
when :python
|
|
80
|
+
python_examples
|
|
81
|
+
prompt_for_usage_location('3. python')
|
|
82
|
+
when :pytest
|
|
83
|
+
pytest_examples
|
|
84
|
+
prompt_for_usage_location('4. pytest')
|
|
76
85
|
when :haml
|
|
77
86
|
haml_examples
|
|
78
|
-
prompt_for_usage_location('
|
|
87
|
+
prompt_for_usage_location('8. haml')
|
|
79
88
|
when :js
|
|
80
89
|
js_examples
|
|
81
|
-
prompt_for_usage_location('
|
|
90
|
+
prompt_for_usage_location('6. javascript (plain)')
|
|
82
91
|
when :vue
|
|
83
92
|
vue_examples
|
|
84
|
-
prompt_for_usage_location('
|
|
93
|
+
prompt_for_usage_location('5. javascript (vue)')
|
|
85
94
|
when :vue_template
|
|
86
95
|
vue_template_examples
|
|
87
|
-
prompt_for_usage_location('
|
|
96
|
+
prompt_for_usage_location('7. vue template')
|
|
88
97
|
when :gdk
|
|
89
98
|
gdk_examples
|
|
90
|
-
prompt_for_usage_location('
|
|
99
|
+
prompt_for_usage_location('9. Manual testing in GDK')
|
|
91
100
|
when :tableau
|
|
92
101
|
service_ping_dashboard_examples
|
|
93
|
-
prompt_for_usage_location('
|
|
102
|
+
prompt_for_usage_location('10. Data verification in Tableau')
|
|
94
103
|
when :other_event
|
|
95
104
|
self.class.new(cli).run
|
|
96
105
|
when :exit
|
|
@@ -269,6 +278,97 @@ module GitlabInternalEventsCli
|
|
|
269
278
|
cli.say("Want to see the implementation details? See app/assets/javascripts/tracking/internal_events.js\n\n")
|
|
270
279
|
end
|
|
271
280
|
|
|
281
|
+
def python_examples
|
|
282
|
+
identifier_params = identifiers.map do |identifier|
|
|
283
|
+
" #{identifier}_id: int,"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
identifier_args = identifiers.map do |identifier|
|
|
287
|
+
" #{identifier}_id=#{identifier}_id,"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
property_args = format_additional_properties do |property, value, description|
|
|
291
|
+
" #{property}=#{value}, # #{description}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
additional_properties_arg = if property_args.any?
|
|
295
|
+
" additional_properties=InternalEventAdditionalProperties(\n" \
|
|
296
|
+
"#{property_args.join("\n")}\n ),"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
track_args = ["'#{action}',", *identifier_args, additional_properties_arg].compact.join("\n ")
|
|
300
|
+
|
|
301
|
+
function_params = [
|
|
302
|
+
*identifier_params,
|
|
303
|
+
' internal_event_client: InternalEventsClient = Provide[',
|
|
304
|
+
' ContainerApplication.internal_event.client',
|
|
305
|
+
' ],'
|
|
306
|
+
].join("\n")
|
|
307
|
+
|
|
308
|
+
imports = if property_args.any?
|
|
309
|
+
'from lib.internal_events import InternalEventsClient, InternalEventAdditionalProperties'
|
|
310
|
+
else
|
|
311
|
+
'from lib.internal_events import InternalEventsClient'
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
cli.say format_warning <<~TEXT
|
|
315
|
+
#{divider}
|
|
316
|
+
#{format_help('# PYTHON')}
|
|
317
|
+
|
|
318
|
+
#{imports}
|
|
319
|
+
from dependency_injector.wiring import Provide, inject
|
|
320
|
+
from ai_gateway.container import ContainerApplication
|
|
321
|
+
|
|
322
|
+
@inject
|
|
323
|
+
async def my_method(
|
|
324
|
+
#{function_params}
|
|
325
|
+
):
|
|
326
|
+
internal_event_client.track_event(
|
|
327
|
+
#{track_args}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
#{divider}
|
|
331
|
+
TEXT
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def pytest_examples
|
|
335
|
+
property_args = format_additional_properties do |property, value, description|
|
|
336
|
+
" #{property}=#{value}, # #{description}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
additional_properties_arg = if property_args.any?
|
|
340
|
+
" additional_properties = InternalEventAdditionalProperties(\n" \
|
|
341
|
+
"#{property_args.join("\n")}\n )"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
assert_args = ["event_name='#{action}',"]
|
|
345
|
+
assert_args << ' additional_properties=additional_properties,' if property_args.any?
|
|
346
|
+
identifiers.each { |id| assert_args << " #{id}_id=#{id}_id," }
|
|
347
|
+
assert_args_str = assert_args.join("\n ")
|
|
348
|
+
|
|
349
|
+
imports = ('from lib.internal_events import InternalEventAdditionalProperties' if property_args.any?)
|
|
350
|
+
|
|
351
|
+
cli.say format_warning <<~TEXT
|
|
352
|
+
#{divider}
|
|
353
|
+
#{format_help('# PYTEST')}
|
|
354
|
+
|
|
355
|
+
from unittest.mock import Mock
|
|
356
|
+
#{imports}
|
|
357
|
+
|
|
358
|
+
def test_#{action}(internal_event_client: Mock):
|
|
359
|
+
#{"#{additional_properties_arg}\n\n" if additional_properties_arg} instance = YourClass()
|
|
360
|
+
instance._internal_event_client = internal_event_client
|
|
361
|
+
|
|
362
|
+
instance.trigger_action()
|
|
363
|
+
|
|
364
|
+
internal_event_client.track_event.assert_called_once_with(
|
|
365
|
+
#{assert_args_str}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
#{divider}
|
|
369
|
+
TEXT
|
|
370
|
+
end
|
|
371
|
+
|
|
272
372
|
private
|
|
273
373
|
|
|
274
374
|
def action
|
|
@@ -32,6 +32,42 @@ module GitlabInternalEventsCli
|
|
|
32
32
|
events.slice(*event_paths)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def generate_ai_event_suggestion(action)
|
|
36
|
+
ai_tracking_url = format_info('https://docs.gitlab.com/development/ai_features/usage_tracking/#adding-new-event-for-tracking')
|
|
37
|
+
ai_tracking_module = format_info('Gitlab::Tracking::AiTracking')
|
|
38
|
+
|
|
39
|
+
<<~TEXT
|
|
40
|
+
#{divider}
|
|
41
|
+
#{format_info('AI Tracking')}
|
|
42
|
+
|
|
43
|
+
Events with #{format_warning('classification: duo')} are required to be registered in #{ai_tracking_module}, you can do this in the following steps:
|
|
44
|
+
|
|
45
|
+
- #{format_info('Add the event name and the unique AI feature ID:')}
|
|
46
|
+
|
|
47
|
+
#{format_info('# Tracking without metadata')}
|
|
48
|
+
events(#{action}: AI_FEATURE_ID)
|
|
49
|
+
|
|
50
|
+
or, if your event has additional metadata:
|
|
51
|
+
|
|
52
|
+
#{format_info('# Tracking with metadata')}
|
|
53
|
+
events(#{event.action}: AI_FEATURE_ID) do |context|
|
|
54
|
+
{ job_id: context['job'].id }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Learn more: #{ai_tracking_url}
|
|
58
|
+
TEXT
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def generate_python_advice
|
|
62
|
+
<<~TEXT
|
|
63
|
+
\n
|
|
64
|
+
#{divider}
|
|
65
|
+
#{format_info('Python codebase')}
|
|
66
|
+
|
|
67
|
+
It seems like you are using the CLI in a python codebase. Make sure to check the python examples in the usage section.
|
|
68
|
+
TEXT
|
|
69
|
+
end
|
|
70
|
+
|
|
35
71
|
private
|
|
36
72
|
|
|
37
73
|
def trim_description(description)
|
|
@@ -31,13 +31,20 @@ module GitlabInternalEventsCli
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def file_saved_message(verb, filepath)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
# Context/validation messages only apply to YAML definition files.
|
|
35
|
+
# Other artifacts (e.g. generated Ruby class & spec files for database
|
|
36
|
+
# metrics) are reported with just the success line.
|
|
37
|
+
if filepath.end_with?('.yml', '.yaml')
|
|
38
|
+
attributes = YAML.safe_load_file(absolute_path(filepath))
|
|
39
|
+
|
|
40
|
+
format_prefix ' ', [
|
|
41
|
+
file_saved_success_message(verb, filepath),
|
|
42
|
+
file_saved_context_message(attributes),
|
|
43
|
+
file_saved_validations_message(attributes)
|
|
44
|
+
].compact.join("\n")
|
|
45
|
+
else
|
|
46
|
+
format_prefix ' ', file_saved_success_message(verb, filepath)
|
|
47
|
+
end
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
def write_to_file(filepath, content, verb)
|
|
@@ -79,7 +79,15 @@ module GitlabInternalEventsCli
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
# NOTE: :instrumentation_operation is kept on the struct but intentionally
|
|
83
|
+
# excluded from NEW_METRIC_FIELDS so it does NOT leak into the YAML output.
|
|
84
|
+
# It is only used to generate the Ruby instrumentation class file for
|
|
85
|
+
# database metrics.
|
|
86
|
+
NewMetric = Struct.new(
|
|
87
|
+
*NEW_METRIC_FIELDS,
|
|
88
|
+
:identifier, :actions, :key, :filters, :operator, :instrumentation_operation,
|
|
89
|
+
keyword_init: true
|
|
90
|
+
) do
|
|
83
91
|
def formatted_output
|
|
84
92
|
extra_keys = event_metric? ? { events: events } : {}
|
|
85
93
|
|
|
@@ -107,6 +115,88 @@ module GitlabInternalEventsCli
|
|
|
107
115
|
)
|
|
108
116
|
end
|
|
109
117
|
|
|
118
|
+
# Path to the Ruby instrumentation class file generated for
|
|
119
|
+
# database metrics. Mirrors the layout used by the upstream
|
|
120
|
+
# `rails generate gitlab:usage_metric` generator.
|
|
121
|
+
def instrumentation_class_file_path
|
|
122
|
+
File.join(
|
|
123
|
+
*[
|
|
124
|
+
distribution_path,
|
|
125
|
+
'lib', 'gitlab', 'usage', 'metrics', 'instrumentations',
|
|
126
|
+
"#{instrumentation_class_underscored}.rb"
|
|
127
|
+
].compact
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Path to the RSpec file for the generated instrumentation class.
|
|
132
|
+
def instrumentation_class_spec_file_path
|
|
133
|
+
File.join(
|
|
134
|
+
*[
|
|
135
|
+
distribution_path,
|
|
136
|
+
'spec', 'lib', 'gitlab', 'usage', 'metrics', 'instrumentations',
|
|
137
|
+
"#{instrumentation_class_underscored}_spec.rb"
|
|
138
|
+
].compact
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Renders the body of the database instrumentation class.
|
|
143
|
+
#
|
|
144
|
+
# Based on upstream
|
|
145
|
+
# lib/generators/gitlab/usage_metric/templates/database_instrumentation_class.rb.template
|
|
146
|
+
# with one intentional deviation: the upstream Rails generator appends
|
|
147
|
+
# "Metric" to the class name from a separate argument, whereas the CLI
|
|
148
|
+
# asks the user to provide the full class name (e.g. "CountIssuesMetric")
|
|
149
|
+
# and interpolates it verbatim.
|
|
150
|
+
def instrumentation_class_content
|
|
151
|
+
<<~RUBY
|
|
152
|
+
# frozen_string_literal: true
|
|
153
|
+
|
|
154
|
+
module Gitlab
|
|
155
|
+
module Usage
|
|
156
|
+
module Metrics
|
|
157
|
+
module Instrumentations
|
|
158
|
+
class #{instrumentation_class} < DatabaseMetric
|
|
159
|
+
operation :#{instrumentation_operation}
|
|
160
|
+
|
|
161
|
+
relation do
|
|
162
|
+
# Insert ActiveRecord relation here
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
RUBY
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Renders the spec body for the generated instrumentation class.
|
|
173
|
+
#
|
|
174
|
+
# Differs from the upstream Rails template in two ways:
|
|
175
|
+
# 1. Uses the combined `a correct instrumented metric value and query`
|
|
176
|
+
# shared example (issue gitlab-org/gitlab#569191), where upstream uses
|
|
177
|
+
# `a correct instrumented metric value`.
|
|
178
|
+
# 2. Adds `data_source: 'database'` to the shared example params, matching
|
|
179
|
+
# the convention used by recent database metric specs in
|
|
180
|
+
# gitlab-org/gitlab (e.g. `count_admins_metric_spec.rb`).
|
|
181
|
+
def instrumentation_class_spec_content
|
|
182
|
+
<<~RUBY
|
|
183
|
+
# frozen_string_literal: true
|
|
184
|
+
|
|
185
|
+
require 'spec_helper'
|
|
186
|
+
|
|
187
|
+
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::#{instrumentation_class}, feature_category: :service_ping do
|
|
188
|
+
let(:expected_value) { 0 }
|
|
189
|
+
let(:expected_query) { '' }
|
|
190
|
+
|
|
191
|
+
it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all', data_source: 'database' }
|
|
192
|
+
end
|
|
193
|
+
RUBY
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def database_metric?
|
|
197
|
+
data_source == 'database'
|
|
198
|
+
end
|
|
199
|
+
|
|
110
200
|
def distribution_path
|
|
111
201
|
'ee' unless tiers.include?('free')
|
|
112
202
|
end
|
|
@@ -235,6 +325,37 @@ module GitlabInternalEventsCli
|
|
|
235
325
|
def event_metric?
|
|
236
326
|
data_source == 'internal_events'
|
|
237
327
|
end
|
|
328
|
+
|
|
329
|
+
# Converts a CamelCase class name like "CountIssuesMetric" to
|
|
330
|
+
# the snake_case file name stem "count_issues_metric".
|
|
331
|
+
# Implemented without ActiveSupport to keep the gem dependency-free.
|
|
332
|
+
def instrumentation_class_underscored
|
|
333
|
+
return '' unless instrumentation_class
|
|
334
|
+
|
|
335
|
+
instrumentation_class
|
|
336
|
+
.gsub('::', '/')
|
|
337
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
338
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
339
|
+
.downcase
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Derives a snake_case metric-name suggestion from the
|
|
343
|
+
# instrumentation_class so database metrics can show a sensible
|
|
344
|
+
# default in the "How should we reference this metric?" prompt.
|
|
345
|
+
#
|
|
346
|
+
# Trailing `_metric` is stripped because the metric's key_path /
|
|
347
|
+
# filename should not end in `_metric` (the class name does, but
|
|
348
|
+
# the key path describes the value, not the Ruby class).
|
|
349
|
+
#
|
|
350
|
+
# Examples:
|
|
351
|
+
# "CountIssuesMetric" -> "count_issues"
|
|
352
|
+
# "CountStuffINTheDBMetric" -> "count_stuff_in_the_db"
|
|
353
|
+
# nil -> nil
|
|
354
|
+
def default_name_from_instrumentation_class
|
|
355
|
+
return unless instrumentation_class && !instrumentation_class.empty?
|
|
356
|
+
|
|
357
|
+
instrumentation_class_underscored.sub(/_metric\z/, '')
|
|
358
|
+
end
|
|
238
359
|
end
|
|
239
360
|
|
|
240
361
|
class Metric
|
|
@@ -8,6 +8,20 @@ module GitlabInternalEventsCli
|
|
|
8
8
|
|
|
9
9
|
CLASS_NAME_REGEX = /\A[a-zA-Z]+\z/
|
|
10
10
|
|
|
11
|
+
# Maps each allowed database operation to its menu description.
|
|
12
|
+
# Mirrors the operations accepted by upstream
|
|
13
|
+
# `rails generate gitlab:usage_metric --type database`.
|
|
14
|
+
DATABASE_OPERATIONS = {
|
|
15
|
+
'count' => 'count rows in the relation',
|
|
16
|
+
'distinct_count' => 'count distinct values in a column',
|
|
17
|
+
'estimate_batch_distinct_count' => 'approximate distinct count using HyperLogLog',
|
|
18
|
+
'sum' => 'sum a numeric column',
|
|
19
|
+
'average' => 'average a numeric column'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Width used to align operation names with their descriptions in the menu.
|
|
23
|
+
OPERATION_NAME_WIDTH = DATABASE_OPERATIONS.keys.map(&:length).max
|
|
24
|
+
|
|
11
25
|
attr_reader :metric
|
|
12
26
|
|
|
13
27
|
def initialize(cli)
|
|
@@ -20,6 +34,7 @@ module GitlabInternalEventsCli
|
|
|
20
34
|
metric.data_source = 'database'
|
|
21
35
|
|
|
22
36
|
prompt_for_instrumentation_class
|
|
37
|
+
prompt_for_operation
|
|
23
38
|
prompt_for_time_frame
|
|
24
39
|
end
|
|
25
40
|
|
|
@@ -35,7 +50,10 @@ module GitlabInternalEventsCli
|
|
|
35
50
|
|
|
36
51
|
TEXT
|
|
37
52
|
|
|
38
|
-
|
|
53
|
+
# multiline: true suppresses the duplicate `Input text:` prefix that
|
|
54
|
+
# TTY::Prompt would otherwise emit on the input line below — the
|
|
55
|
+
# question text already includes the prefix via the heredoc above.
|
|
56
|
+
metric.instrumentation_class = prompt_for_text(' Instrumentation class: ', multiline: true) do |q|
|
|
39
57
|
q.required true
|
|
40
58
|
q.messages[:required?] = INSTRUMENTATION_CLASS_HELP
|
|
41
59
|
q.messages[:valid?] = INSTRUMENTATION_CLASS_ERROR
|
|
@@ -43,6 +61,21 @@ module GitlabInternalEventsCli
|
|
|
43
61
|
end
|
|
44
62
|
end
|
|
45
63
|
|
|
64
|
+
def prompt_for_operation
|
|
65
|
+
cli.say OPERATION_INTRO
|
|
66
|
+
|
|
67
|
+
metric.instrumentation_operation = cli.select(
|
|
68
|
+
'Which operation should the metric perform?',
|
|
69
|
+
**select_opts
|
|
70
|
+
) do |menu|
|
|
71
|
+
menu.enum '.'
|
|
72
|
+
|
|
73
|
+
DATABASE_OPERATIONS.each do |op, description|
|
|
74
|
+
menu.choice "#{op.ljust(OPERATION_NAME_WIDTH)} -- #{description}", op
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
46
79
|
def prompt_for_time_frame
|
|
47
80
|
metric.time_frame = cli.multi_select(
|
|
48
81
|
'For which time frames do you want the metric to be calculated? (Space to select)',
|
|
@@ -44,6 +44,19 @@ module GitlabInternalEventsCli
|
|
|
44
44
|
|
|
45
45
|
TEXT
|
|
46
46
|
|
|
47
|
+
DATABASE_METRIC_EXAMPLES = <<~TEXT
|
|
48
|
+
Database metrics derive their value from an ActiveRecord relation.
|
|
49
|
+
|
|
50
|
+
Common shapes:
|
|
51
|
+
count -- ex) total number of merge requests
|
|
52
|
+
distinct_count -- ex) count of distinct users who created a project
|
|
53
|
+
sum -- ex) total storage used across all projects
|
|
54
|
+
average -- ex) average issue weight
|
|
55
|
+
|
|
56
|
+
See https://docs.gitlab.com/development/internal_analytics/metrics/metrics_instrumentation/#database-metrics
|
|
57
|
+
|
|
58
|
+
TEXT
|
|
59
|
+
|
|
47
60
|
EVENT_EXISTENCE_CHECK_INSTRUCTIONS = <<~TEXT.freeze
|
|
48
61
|
To determine what to do next, let's figure out if the event is already tracked & usable.
|
|
49
62
|
|
|
@@ -5,13 +5,6 @@ module GitlabInternalEventsCli
|
|
|
5
5
|
module MetricDefiner
|
|
6
6
|
extend Helpers::Formatting
|
|
7
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
8
|
ALL_METRICS_EXIST_NOTICE = <<~TEXT
|
|
16
9
|
|
|
17
10
|
Looks like the potential metrics for this event either already exist or are unsupported.
|
|
@@ -81,9 +74,16 @@ module GitlabInternalEventsCli
|
|
|
81
74
|
TEXT
|
|
82
75
|
|
|
83
76
|
DATABASE_METRIC_NAME_HELP = <<~TEXT.freeze
|
|
84
|
-
#{format_warning('Required. Max %<count>s characters. Only lowercase
|
|
77
|
+
#{format_warning('Required. Max %<count>s characters. Only lowercase letters, numbers, and underscores are allowed.')}
|
|
78
|
+
|
|
79
|
+
This value becomes the metric's key path (ID) and YAML filename, so it must be snake_case.
|
|
80
|
+
It is intentionally different from the instrumentation class name you entered earlier:
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
Instrumentation class (Ruby, CamelCase): %<class_name>s
|
|
83
|
+
Metric name (key/filename): %<suggested_name>s
|
|
84
|
+
|
|
85
|
+
Tip: a good metric name describes the value being measured, not the Ruby class.
|
|
86
|
+
Press Enter to accept the suggested default, or type your own snake_case name.
|
|
87
87
|
TEXT
|
|
88
88
|
|
|
89
89
|
NAME_REQUIREMENT_REASONS = {
|
|
@@ -106,12 +106,13 @@ module GitlabInternalEventsCli
|
|
|
106
106
|
}.freeze
|
|
107
107
|
|
|
108
108
|
NAME_ERROR = <<~TEXT.freeze
|
|
109
|
-
#{format_warning('Input is invalid. Max %<count>s characters. Only lowercase
|
|
109
|
+
#{format_warning('Input is invalid. Max %<count>s characters. Only lowercase letters, numbers, and underscores are allowed (snake_case). Ensure this key path (ID) is not already in use.')}
|
|
110
110
|
TEXT
|
|
111
111
|
|
|
112
112
|
INSTRUMENTATION_CLASS_INTRO = <<~TEXT.freeze
|
|
113
|
-
#{format_info('METRIC
|
|
113
|
+
#{format_info('METRIC INSTRUMENTATION CLASS')}
|
|
114
114
|
Choose a name for the Ruby class that will be used to calculate the metric.
|
|
115
|
+
The CLI will scaffold the class file and a matching spec file for you.
|
|
115
116
|
|
|
116
117
|
#{format_info('GOOD EXAMPLES:')}
|
|
117
118
|
- CountSnippetsMetric
|
|
@@ -133,6 +134,20 @@ module GitlabInternalEventsCli
|
|
|
133
134
|
INSTRUMENTATION_CLASS_ERROR = <<~TEXT.freeze
|
|
134
135
|
#{format_warning('Input is invalid. Only lowercase/uppercase letters are allowed.')}
|
|
135
136
|
TEXT
|
|
137
|
+
|
|
138
|
+
OPERATION_INTRO = <<~TEXT.freeze
|
|
139
|
+
#{format_info('METRIC OPERATION')}
|
|
140
|
+
Choose how the metric should derive its value from the database relation.
|
|
141
|
+
|
|
142
|
+
See the database metrics documentation for details:
|
|
143
|
+
https://docs.gitlab.com/development/internal_analytics/metrics/metrics_instrumentation/#database-metrics
|
|
144
|
+
TEXT
|
|
145
|
+
|
|
146
|
+
DATABASE_INSTRUMENTATION_FILES_NEXT_STEPS = <<~TEXT.freeze
|
|
147
|
+
- Fill in the ActiveRecord relation inside the generated instrumentation class.
|
|
148
|
+
- Adjust `expected_value` and `expected_query` in the generated spec to match your relation.
|
|
149
|
+
- Reference: #{format_info('https://docs.gitlab.com/development/internal_analytics/metrics/metrics_instrumentation/#database-metrics')}
|
|
150
|
+
TEXT
|
|
136
151
|
end
|
|
137
152
|
end
|
|
138
153
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabInternalEventsCli
|
|
4
|
+
class VersionChecker
|
|
5
|
+
RUBYGEMS_API_URL = 'https://rubygems.org/api/v1/gems/gitlab_internal_events_cli.json'
|
|
6
|
+
GEM_NAME = 'gitlab_internal_events_cli'
|
|
7
|
+
|
|
8
|
+
def initialize(timeout: 3)
|
|
9
|
+
@timeout = timeout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def latest_version
|
|
13
|
+
@latest_version ||= fetch_latest_version
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update_available?
|
|
17
|
+
return false unless latest_version
|
|
18
|
+
|
|
19
|
+
Gem::Version.new(latest_version) > Gem::Version.new(VERSION)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self_update!
|
|
23
|
+
system('gem', 'install', GEM_NAME, '--no-document')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def fetch_latest_version
|
|
29
|
+
uri = URI(RUBYGEMS_API_URL)
|
|
30
|
+
response = Timeout.timeout(@timeout) { Net::HTTP.get(uri) }
|
|
31
|
+
gem_info = JSON.parse(response)
|
|
32
|
+
|
|
33
|
+
return unless gem_info.is_a?(Hash) && gem_info['version']
|
|
34
|
+
|
|
35
|
+
gem_info['version']
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -20,6 +20,7 @@ require_relative 'gitlab_internal_events_cli/metric'
|
|
|
20
20
|
require_relative 'gitlab_internal_events_cli/global_state'
|
|
21
21
|
require_relative 'gitlab_internal_events_cli/helpers'
|
|
22
22
|
require_relative 'gitlab_internal_events_cli/gitlab_prompt'
|
|
23
|
+
require_relative 'gitlab_internal_events_cli/version_checker'
|
|
23
24
|
require_relative 'gitlab_internal_events_cli/cli'
|
|
24
25
|
require_relative 'gitlab_internal_events_cli/text/event_definer'
|
|
25
26
|
require_relative 'gitlab_internal_events_cli/text/metric_definer'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab_internal_events_cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json_schemer
|
|
@@ -141,6 +141,7 @@ files:
|
|
|
141
141
|
- lib/gitlab_internal_events_cli/text/metric_definer.rb
|
|
142
142
|
- lib/gitlab_internal_events_cli/time_framed_key_path.rb
|
|
143
143
|
- lib/gitlab_internal_events_cli/version.rb
|
|
144
|
+
- lib/gitlab_internal_events_cli/version_checker.rb
|
|
144
145
|
homepage: https://gitlab.com/gitlab-org/analytics-section/product-analytics/analytics-cli
|
|
145
146
|
licenses:
|
|
146
147
|
- MIT
|