gitlab-triage 1.51.1 → 1.52.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0852608feca48b98d92771b30203ae3f2a83220fdcce24f08d435b9af12c7f20'
4
- data.tar.gz: 224da2c7b14a0513d2d06c43ae1c13bb3ac5459114df3daa32da6785354b2247
3
+ metadata.gz: c0188aec696ec4f1821f6e9d4244a2323527d464e9c51da0b7e2b54053003a2c
4
+ data.tar.gz: d432a7b181c290420afdd883c01a55f686a1e30d1797f736fbbf3171459f36ee
5
5
  SHA512:
6
- metadata.gz: 435c98010e80f7bf81e31a0976c36d948e56e2b94da2e08ad9eefef5f6680967ab95c4e91f9667233111d564de27c5380890f546d047e6b1cf55a7c6e6915881
7
- data.tar.gz: 9e87b898dab83aee3b7f0585e805a6200478e27fc7cefa441b99422666d7ee51b85819b5e04812542b3e0d6a136384409a0261f7971d97571606d3a369fabc26
6
+ metadata.gz: 797365b9ad72539cf3027e7e00130cbf88172e11ec46162c7c38e25d7eebbaca39cb6d5518a033ac7b820e127c15b0ef8a3d64c8011a0dcd45f4d9ef985a13c6
7
+ data.tar.gz: 46c14ed3a8916402aefe35aaa6b5f80c8b6b647ee4f6e1ab7bbf138ac4c0c9fd0326a672be08abc1043bfdcc5280d74456554a71f2f390439f1b9858828401f7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab-triage (1.51.1)
4
+ gitlab-triage (1.52.0)
5
5
  activesupport (>= 5.1)
6
6
  globalid (~> 1.0, >= 1.0.1)
7
7
  graphql (< 2.1.0)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module APIObservability
8
+ class Tracker
9
+ PAGINATION_PARAMS = %w[page per_page].freeze
10
+
11
+ attr_reader :calls, :resources_fetched
12
+
13
+ def initialize
14
+ @calls = Hash.new(0)
15
+ @resources_fetched = 0
16
+ @rate_limit_waits = 0
17
+ @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
18
+ end
19
+
20
+ def record(method, url, resource_count: 0)
21
+ @calls["#{method} #{normalize_endpoint(url)}"] += 1
22
+ @resources_fetched += resource_count
23
+ end
24
+
25
+ def record_rate_limit_wait
26
+ @rate_limit_waits += 1
27
+ end
28
+
29
+ def total
30
+ @calls.values.sum
31
+ end
32
+
33
+ def print_summary
34
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
35
+
36
+ puts "\n"
37
+ puts '=' * 70
38
+ puts ' API Call Summary'
39
+ puts '=' * 70
40
+ puts " Total HTTP requests: #{total}"
41
+ puts " Resources fetched: #{@resources_fetched}"
42
+ puts " Rate limit waits: #{@rate_limit_waits}"
43
+ puts " Elapsed time: #{elapsed.round(1)}s"
44
+ puts '-' * 70
45
+
46
+ @calls.sort_by { |_, count| -count }.each do |endpoint, count|
47
+ puts " #{count.to_s.rjust(5)}x #{endpoint}"
48
+ end
49
+
50
+ puts '=' * 70
51
+ end
52
+
53
+ private
54
+
55
+ def normalize_endpoint(url)
56
+ uri = URI.parse(url)
57
+ path = uri.path
58
+ .sub(%r{/api/v\d+}, '')
59
+ .sub(%r{/projects/[^/]+}, '/projects/:id')
60
+ .sub(%r{/groups/[^/]+}, '/groups/:id')
61
+ .gsub(%r{/\d+(/|\z)}, '/:id\1')
62
+
63
+ query_keys = (URI.decode_www_form(uri.query || '').map(&:first) - PAGINATION_PARAMS).sort
64
+
65
+ path += "?#{query_keys.join('&')}" if query_keys.any?
66
+ path
67
+ rescue URI::InvalidURIError
68
+ url
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -32,6 +32,7 @@ require_relative 'network_adapters/httparty_adapter'
32
32
  require_relative 'network_adapters/graphql_adapter'
33
33
  require_relative 'graphql_queries/query_builder'
34
34
  require_relative 'ui'
35
+ require_relative 'api_observability'
35
36
 
36
37
  module Gitlab
37
38
  module Triage
@@ -90,20 +91,26 @@ module Gitlab
90
91
  def perform
91
92
  puts "Performing a dry run.\n\n" if options.dry_run
92
93
 
93
- puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.singularize}", char: '=')
94
- puts
94
+ unless options.quiet
95
+ puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.singularize}", char: '=')
96
+ puts
97
+ end
95
98
 
96
99
  resource_rules.each do |resource_type, policy_definition|
97
100
  next unless right_resource_type_for_resource_option?(resource_type)
98
101
 
99
102
  assert_epic_rule!(resource_type)
100
103
 
101
- puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
102
- puts
104
+ unless options.quiet
105
+ puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
106
+ puts
107
+ end
103
108
 
104
109
  process_summaries(resource_type, policy_definition[:summaries])
105
110
  process_rules(resource_type, policy_definition[:rules])
106
111
  end
112
+
113
+ tracker.print_summary if options.summary
107
114
  end
108
115
 
109
116
  def network
@@ -204,12 +211,16 @@ module Gitlab
204
211
  @resource_rules ||= policies.delete(:resource_rules) { {} }
205
212
  end
206
213
 
214
+ def tracker
215
+ @tracker ||= options.summary ? APIObservability::Tracker.new : nil
216
+ end
217
+
207
218
  def network_adapter
208
- @network_adapter ||= @network_adapter_class.new(options)
219
+ @network_adapter ||= @network_adapter_class.new(options, tracker: tracker)
209
220
  end
210
221
 
211
222
  def graphql_network_adapter
212
- @graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
223
+ @graphql_network_adapter ||= @graphql_network_adapter_class.new(options, tracker: tracker)
213
224
  end
214
225
 
215
226
  def rule_conditions(rule)
@@ -310,8 +321,10 @@ module Gitlab
310
321
  #
311
322
  # @return [nil]
312
323
  def process_summary(resource_type, summary_definition)
313
- puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
314
- puts
324
+ unless options.quiet
325
+ puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
326
+ puts
327
+ end
315
328
 
316
329
  summary_parts_for_rules(resource_type, summary_definition[:rules]) do |summary_resources|
317
330
  policy = Policies::SummaryPolicy.new(
@@ -365,7 +378,7 @@ module Gitlab
365
378
  #
366
379
  # @return [nil]
367
380
  def resources_for_rule(resource_type, rule_definition, in_summary: false)
368
- puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-')
381
+ puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-') unless options.quiet
369
382
 
370
383
  # Skip resource fetching for custom type rules
371
384
  if rule_definition[:type] == 'custom'
@@ -373,7 +386,7 @@ module Gitlab
373
386
  raise ArgumentError, "type: custom can only be used in summary rules, not in top-level rules (rule: #{rule_definition[:name]})"
374
387
  end
375
388
 
376
- puts "\n* Skipping resource fetch for custom type rule"
389
+ puts "\n* Skipping resource fetch for custom type rule" unless options.quiet
377
390
  yield(PoliciesResources::RuleResources.new([]), {})
378
391
  return
379
392
  end
@@ -391,14 +404,23 @@ module Gitlab
391
404
  # In some filters/actions we want to know which resource type it is
392
405
  attach_resource_type(resources, resource_type)
393
406
 
394
- puts "\n\n* Found #{resources.count} resources..."
395
- print "* Filtering resources..."
407
+ unless options.quiet
408
+ puts "\n\n* Found #{resources.count} resources..."
409
+ print "* Filtering resources..."
410
+ end
411
+
396
412
  resources = filter_resources(resources, expanded_conditions)
397
- puts "\n* Total after filtering: #{resources.count} resources"
398
- print "* Limiting resources..."
413
+
414
+ unless options.quiet
415
+ puts "\n* Total after filtering: #{resources.count} resources"
416
+ print "* Limiting resources..."
417
+ end
418
+
399
419
  resources = limit_resources(resources, rule_limits(rule_definition))
400
- puts "\n* Total after limiting: #{resources.count} resources"
401
- puts
420
+ unless options.quiet
421
+ puts "\n* Total after limiting: #{resources.count} resources"
422
+ puts
423
+ end
402
424
 
403
425
  resources = sanitize_resources(resources)
404
426
 
@@ -470,7 +492,7 @@ module Gitlab
470
492
  policy: policy,
471
493
  network: network,
472
494
  dry: options.dry_run)
473
- puts
495
+ puts unless options.quiet
474
496
  end
475
497
 
476
498
  def filter_resources(resources, conditions)
@@ -29,7 +29,7 @@ module Gitlab
29
29
  parsed_graphql_query = adapter.parse(graphql_query.query)
30
30
 
31
31
  begin
32
- print '.'
32
+ print '.' unless options.quiet
33
33
 
34
34
  response = adapter.query(
35
35
  parsed_graphql_query,
@@ -52,7 +52,7 @@ module Gitlab
52
52
  def mutate(graphql_mutation, variables = {})
53
53
  return if graphql_mutation.blank?
54
54
 
55
- print '.'
55
+ print '.' unless options.quiet
56
56
 
57
57
  parsed_mutation = adapter.parse(graphql_mutation)
58
58
 
@@ -99,6 +99,7 @@ module Gitlab
99
99
  return unless reset_at
100
100
 
101
101
  puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{reset_at - Time.now} seconds" if options.debug
102
+ adapter.tracker&.record_rate_limit_wait
102
103
  sleep(1) until Time.now >= reset_at
103
104
  end
104
105
  end
@@ -8,10 +8,11 @@ module Gitlab
8
8
  class BaseAdapter
9
9
  USER_AGENT = "GitLab Triage #{Gitlab::Triage::VERSION}".freeze
10
10
 
11
- attr_reader :options
11
+ attr_reader :options, :tracker
12
12
 
13
- def initialize(options)
13
+ def initialize(options, tracker: nil)
14
14
  @options = options
15
+ @tracker = tracker
15
16
  end
16
17
  end
17
18
  end
@@ -138,8 +138,11 @@ module Gitlab
138
138
  end
139
139
 
140
140
  def http_client
141
- Client::HTTP.new("#{options.host_url}/api/graphql") do
142
- def execute(document:, operation_name: nil, variables: {}, context: {}) # rubocop:disable Lint/NestedMethodDefinition
141
+ graphql_tracker = tracker
142
+ graphql_url = "#{options.host_url}/api/graphql"
143
+
144
+ Client::HTTP.new(graphql_url) do
145
+ define_method(:execute) do |document:, operation_name: nil, variables: {}, context: {}|
143
146
  body = {}
144
147
  body['query'] = document.to_query_string
145
148
  body['variables'] = variables if variables.any?
@@ -157,6 +160,7 @@ module Gitlab
157
160
 
158
161
  case response.code
159
162
  when 200, 400
163
+ graphql_tracker&.record('POST', graphql_url)
160
164
  JSON.parse(response.body).merge('extensions' => { 'headers' => response.headers })
161
165
  else
162
166
  { 'errors' => [{ 'message' => "#{response.code} #{response.message}" }] }
@@ -24,13 +24,18 @@ module Gitlab
24
24
  raise_on_internal_server_error!(response)
25
25
  raise_on_too_many_requests!(response)
26
26
 
27
- {
27
+ result = {
28
28
  more_pages: (response.headers["x-next-page"].to_s != ""),
29
29
  next_page_url: next_page_url(url, response),
30
30
  results: response.parsed_response,
31
31
  ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
32
32
  ratelimit_reset_at: Time.at(response.headers["ratelimit-reset"].to_i)
33
33
  }
34
+
35
+ resource_count = result[:results].is_a?(Array) ? result[:results].size : 0
36
+ tracker&.record('GET', url, resource_count: resource_count)
37
+
38
+ result
34
39
  end
35
40
 
36
41
  def post(token, url, body)
@@ -48,6 +53,8 @@ module Gitlab
48
53
  raise_on_internal_server_error!(response)
49
54
  raise_on_too_many_requests!(response)
50
55
 
56
+ tracker&.record('POST', url)
57
+
51
58
  {
52
59
  results: response.parsed_response,
53
60
  ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
@@ -68,6 +75,8 @@ module Gitlab
68
75
  raise_on_internal_server_error!(response)
69
76
  raise_on_too_many_requests!(response)
70
77
 
78
+ tracker&.record('DELETE', url)
79
+
71
80
  {
72
81
  results: response.parsed_response,
73
82
  ratelimit_remaining: response.headers["ratelimit-remaining"].to_i,
@@ -65,6 +65,14 @@ module Gitlab
65
65
  options.debug = value
66
66
  end
67
67
 
68
+ opts.on('--summary', 'Print API call summary at end of execution') do |value|
69
+ options.summary = value
70
+ end
71
+
72
+ opts.on('--quiet', 'Suppress pagination dots and section headers') do |value|
73
+ options.quiet = value
74
+ end
75
+
68
76
  opts.on('--cache-dir [string]', String,
69
77
  'Directory containing pre-fetched API responses. ' \
70
78
  'Files must follow the naming convention: <source>_<id>_<resource>.json ' \
@@ -15,7 +15,9 @@ module Gitlab
15
15
  :host_url,
16
16
  :require_files,
17
17
  :api_version,
18
- :cache_dir
18
+ :cache_dir,
19
+ :summary,
20
+ :quiet
19
21
  ) do
20
22
  def initialize(*args)
21
23
  super
@@ -35,7 +35,7 @@ module Gitlab
35
35
  resources = []
36
36
 
37
37
  begin
38
- print '.'
38
+ print '.' unless options.quiet
39
39
  url = response.fetch(:next_page_url) { url }
40
40
 
41
41
  response = execute_with_retry(
@@ -165,6 +165,7 @@ module Gitlab
165
165
  return unless response.delete(:ratelimit_remaining) < MINIMUM_RATE_LIMIT
166
166
 
167
167
  puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{response[:ratelimit_reset_at] - Time.now} seconds" if options.debug
168
+ @adapter.tracker&.record_rate_limit_wait
168
169
  sleep(1) until Time.now >= response[:ratelimit_reset_at]
169
170
  end
170
171
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.51.1'
5
+ VERSION = '1.52.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-triage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.51.1
4
+ version: 1.52.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-08 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -240,6 +240,7 @@ files:
240
240
  - lib/gitlab/triage/action/issue.rb
241
241
  - lib/gitlab/triage/action/summarize.rb
242
242
  - lib/gitlab/triage/action/work_item_status.rb
243
+ - lib/gitlab/triage/api_observability.rb
243
244
  - lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
244
245
  - lib/gitlab/triage/api_query_builders/date_query_param_builder.rb
245
246
  - lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb