plan_my_stuff 0.1.0 → 1.0.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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +595 -0
  3. data/CONFIGURATION.md +487 -0
  4. data/README.md +612 -88
  5. data/app/controllers/plan_my_stuff/application_controller.rb +27 -5
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +50 -19
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +53 -0
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +129 -0
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +161 -0
  11. data/app/controllers/plan_my_stuff/issues/testings_controller.rb +82 -0
  12. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +62 -0
  13. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +55 -0
  14. data/app/controllers/plan_my_stuff/issues_controller.rb +53 -70
  15. data/app/controllers/plan_my_stuff/labels_controller.rb +32 -10
  16. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +88 -0
  17. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +44 -0
  18. data/app/controllers/plan_my_stuff/project_items_controller.rb +32 -69
  19. data/app/controllers/plan_my_stuff/projects_controller.rb +81 -3
  20. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +67 -0
  21. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +49 -0
  22. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +121 -0
  23. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +202 -0
  24. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +371 -0
  25. data/app/jobs/plan_my_stuff/application_job.rb +8 -0
  26. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +75 -0
  27. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  28. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +8 -0
  29. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/index.html.erb +5 -5
  31. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  32. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +108 -0
  33. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +11 -6
  34. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +4 -3
  35. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +113 -0
  36. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +4 -3
  37. data/app/views/plan_my_stuff/issues/show.html.erb +67 -6
  38. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  39. data/app/views/plan_my_stuff/projects/edit.html.erb +5 -0
  40. data/app/views/plan_my_stuff/projects/index.html.erb +18 -2
  41. data/app/views/plan_my_stuff/projects/new.html.erb +5 -0
  42. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +30 -0
  43. data/app/views/plan_my_stuff/projects/show.html.erb +30 -11
  44. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +10 -0
  45. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +20 -0
  46. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +5 -0
  47. data/app/views/plan_my_stuff/testing_projects/new.html.erb +5 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +40 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +52 -0
  50. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +36 -0
  51. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  52. data/config/routes.rb +43 -15
  53. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +302 -20
  54. data/lib/plan_my_stuff/application_record.rb +158 -1
  55. data/lib/plan_my_stuff/approval.rb +88 -0
  56. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  57. data/lib/plan_my_stuff/archive.rb +12 -0
  58. data/lib/plan_my_stuff/attachment.rb +83 -0
  59. data/lib/plan_my_stuff/attachment_uploader.rb +245 -0
  60. data/lib/plan_my_stuff/aws_sns_simulator.rb +116 -0
  61. data/lib/plan_my_stuff/base_metadata.rb +25 -28
  62. data/lib/plan_my_stuff/base_project.rb +502 -0
  63. data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +186 -0
  64. data/lib/plan_my_stuff/base_project_item.rb +588 -0
  65. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  66. data/lib/plan_my_stuff/cache.rb +197 -0
  67. data/lib/plan_my_stuff/client.rb +139 -64
  68. data/lib/plan_my_stuff/comment.rb +225 -100
  69. data/lib/plan_my_stuff/comment_metadata.rb +68 -5
  70. data/lib/plan_my_stuff/configuration.rb +459 -28
  71. data/lib/plan_my_stuff/custom_fields.rb +96 -12
  72. data/lib/plan_my_stuff/engine.rb +14 -2
  73. data/lib/plan_my_stuff/errors.rb +65 -5
  74. data/lib/plan_my_stuff/graphql/queries.rb +454 -0
  75. data/lib/plan_my_stuff/issue.rb +1097 -166
  76. data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
  77. data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
  78. data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
  79. data/lib/plan_my_stuff/issue_extractions/waiting.rb +171 -0
  80. data/lib/plan_my_stuff/issue_field.rb +126 -0
  81. data/lib/plan_my_stuff/issue_field_translation.rb +67 -0
  82. data/lib/plan_my_stuff/issue_field_value_set.rb +68 -0
  83. data/lib/plan_my_stuff/issue_metadata.rb +132 -21
  84. data/lib/plan_my_stuff/label.rb +100 -13
  85. data/lib/plan_my_stuff/link.rb +144 -0
  86. data/lib/plan_my_stuff/markdown.rb +13 -7
  87. data/lib/plan_my_stuff/metadata_parser.rb +51 -12
  88. data/lib/plan_my_stuff/notifications.rb +148 -0
  89. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +46 -0
  90. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  91. data/lib/plan_my_stuff/pipeline/status.rb +40 -0
  92. data/lib/plan_my_stuff/pipeline/testing.rb +23 -0
  93. data/lib/plan_my_stuff/pipeline.rb +310 -0
  94. data/lib/plan_my_stuff/project.rb +63 -465
  95. data/lib/plan_my_stuff/project_item.rb +3 -409
  96. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  97. data/lib/plan_my_stuff/project_metadata.rb +47 -0
  98. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  99. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  100. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  101. data/lib/plan_my_stuff/reminders.rb +12 -0
  102. data/lib/plan_my_stuff/repo.rb +145 -0
  103. data/lib/plan_my_stuff/test_helpers.rb +265 -25
  104. data/lib/plan_my_stuff/testing_project.rb +292 -0
  105. data/lib/plan_my_stuff/testing_project_item.rb +218 -0
  106. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  107. data/lib/plan_my_stuff/user_resolver.rb +24 -3
  108. data/lib/plan_my_stuff/verifier.rb +10 -0
  109. data/lib/plan_my_stuff/version.rb +2 -2
  110. data/lib/plan_my_stuff/webhook_replayer.rb +292 -0
  111. data/lib/plan_my_stuff.rb +55 -20
  112. data/lib/tasks/plan_my_stuff.rake +331 -0
  113. metadata +99 -4
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ # ETag-backed HTTP cache for GitHub REST reads.
5
+ #
6
+ # Stores `{ etag:, body: }` pairs in `Rails.cache` keyed by resource.
7
+ # Readers look up the entry, pass its ETag in `If-None-Match`, and
8
+ # either (a) get a cheap 304 and serve the cached body, or (b) get a
9
+ # fresh 200, write the new ETag + body, and return that. Writes from
10
+ # +.create!+/+.update!+ front-load the cache with the fresh ETag
11
+ # returned on the mutating response so the next read is cheap too.
12
+ #
13
+ # Covers issues, comments, and their list endpoints.
14
+ #
15
+ module Cache
16
+ # Gem-internal cache version. Bump when the cache layout or the
17
+ # payload shape changes in a backwards-incompatible way so existing
18
+ # entries are orphaned rather than mis-read.
19
+ CACHE_VERSION = 'v1'
20
+
21
+ class << self
22
+ # @return [Boolean] whether `Rails.cache` is available and caching is enabled
23
+ def enabled?
24
+ return false unless PlanMyStuff.configuration.cache_enabled
25
+ return false if !defined?(::Rails) || !::Rails.respond_to?(:cache) || ::Rails.cache.nil?
26
+
27
+ true
28
+ end
29
+
30
+ # Reads the cached `{etag:, body:}` entry for an issue.
31
+ #
32
+ # @param repo [String] resolved full repo name (e.g. "Org/Name")
33
+ # @param number [Integer]
34
+ #
35
+ # @return [Hash{Symbol => Object}, nil]
36
+ #
37
+ def read_issue(repo, number)
38
+ return unless enabled?
39
+
40
+ ::Rails.cache.read(issue_key(repo, number))
41
+ end
42
+
43
+ # Writes a cache entry for an issue.
44
+ #
45
+ # @param repo [String] resolved full repo name
46
+ # @param number [Integer]
47
+ # @param etag [String, nil] value of the `ETag` response header
48
+ # @param body [Object] parsed GitHub issue response
49
+ #
50
+ # @return [void]
51
+ #
52
+ def write_issue(repo, number, etag:, body:)
53
+ return unless enabled?
54
+ return if etag.blank?
55
+
56
+ ::Rails.cache.write(
57
+ issue_key(repo, number),
58
+ { etag: etag, body: normalize(body) },
59
+ )
60
+ end
61
+
62
+ # Removes the cache entry for an issue.
63
+ #
64
+ # @param repo [String]
65
+ # @param number [Integer]
66
+ #
67
+ # @return [void]
68
+ #
69
+ def delete_issue(repo, number)
70
+ return unless enabled?
71
+
72
+ ::Rails.cache.delete(issue_key(repo, number))
73
+ end
74
+
75
+ # Reads the cached `{etag:, body:}` entry for a comment.
76
+ #
77
+ # @param repo [String] resolved full repo name (e.g. "Org/Name")
78
+ # @param id [Integer]
79
+ #
80
+ # @return [Hash{Symbol => Object}, nil]
81
+ #
82
+ def read_comment(repo, id)
83
+ return unless enabled?
84
+
85
+ ::Rails.cache.read(comment_key(repo, id))
86
+ end
87
+
88
+ # Writes a cache entry for a comment.
89
+ #
90
+ # @param repo [String] resolved full repo name
91
+ # @param id [Integer]
92
+ # @param etag [String, nil] value of the `ETag` response header
93
+ # @param body [Object] parsed GitHub comment response
94
+ #
95
+ # @return [void]
96
+ #
97
+ def write_comment(repo, id, etag:, body:)
98
+ return unless enabled?
99
+ return if etag.blank?
100
+
101
+ ::Rails.cache.write(
102
+ comment_key(repo, id),
103
+ { etag: etag, body: normalize(body) },
104
+ )
105
+ end
106
+
107
+ # Removes the cache entry for a comment.
108
+ #
109
+ # @param repo [String]
110
+ # @param id [Integer]
111
+ #
112
+ # @return [void]
113
+ #
114
+ def delete_comment(repo, id)
115
+ return unless enabled?
116
+
117
+ ::Rails.cache.delete(comment_key(repo, id))
118
+ end
119
+
120
+ # Reads the cached `{etag:, body:}` entry for a list endpoint.
121
+ #
122
+ # @param resource [Symbol] :issue or :comment
123
+ # @param repo [String] resolved full repo name
124
+ # @param params [Hash] query params that make this list unique
125
+ #
126
+ # @return [Hash{Symbol => Object}, nil]
127
+ #
128
+ def read_list(resource, repo, params)
129
+ return unless enabled?
130
+
131
+ ::Rails.cache.read(list_key(resource, repo, params))
132
+ end
133
+
134
+ # Writes a cache entry for a list endpoint.
135
+ #
136
+ # @param resource [Symbol] :issue or :comment
137
+ # @param repo [String] resolved full repo name
138
+ # @param params [Hash] query params that make this list unique
139
+ # @param etag [String, nil] value of the `ETag` response header
140
+ # @param body [Object] parsed GitHub list response (array of resources)
141
+ #
142
+ # @return [void]
143
+ #
144
+ def write_list(resource, repo, params, etag:, body:)
145
+ return unless enabled?
146
+ return if etag.blank?
147
+
148
+ ::Rails.cache.write(
149
+ list_key(resource, repo, params),
150
+ { etag: etag, body: normalize(body) },
151
+ )
152
+ end
153
+
154
+ private
155
+
156
+ # @return [String]
157
+ def issue_key(repo, number)
158
+ [key_prefix, 'issue', repo, number].join('/')
159
+ end
160
+
161
+ # @return [String]
162
+ def comment_key(repo, id)
163
+ [key_prefix, 'comment', repo, id].join('/')
164
+ end
165
+
166
+ # @return [String]
167
+ def list_key(resource, repo, params)
168
+ serialized = params.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}:#{v}" }.join(',')
169
+ [key_prefix, 'list', resource, repo, serialized].join('/')
170
+ end
171
+
172
+ # Builds the cache key prefix from gem + app cache versions so
173
+ # that bumping either side orphans all existing entries.
174
+ #
175
+ # @return [String]
176
+ #
177
+ def key_prefix
178
+ app_version = PlanMyStuff.configuration.cache_version || '0'
179
+ ['pms', CACHE_VERSION, app_version].join('/')
180
+ end
181
+
182
+ # Coerces a Sawyer::Resource, Struct, Array, or similar to a
183
+ # plain Hash (or Array of Hashes) so the cache payload is
184
+ # portable across stores. Anything else is passed through.
185
+ #
186
+ # @return [Object]
187
+ #
188
+ def normalize(body)
189
+ return body.map { |item| normalize(item) } if body.is_a?(Array)
190
+ return body.to_hash if body.respond_to?(:to_hash)
191
+ return body.to_h if body.respond_to?(:to_h)
192
+
193
+ body
194
+ end
195
+ end
196
+ end
197
+ end
@@ -4,18 +4,63 @@ require 'active_support/core_ext/hash/keys'
4
4
  require 'octokit'
5
5
 
6
6
  module PlanMyStuff
7
- # Infrastructure wrapper around Octokit. Handles auth, error normalization,
8
- # and repo resolution. Domain modules (Issues, Projects, etc.) use this
9
- # internally via PlanMyStuff.client.
7
+ # Infrastructure wrapper around Octokit. Handles auth, error normalization, and repo resolution. Domain modules
8
+ # (Issues, Projects, etc.) use this internally via PlanMyStuff.client.
10
9
  class Client
11
10
  # @return [Octokit::Client]
12
11
  attr_reader :octokit
13
12
 
13
+ # Returns the Faraday response from the most recent Octokit call. Useful for reading headers (e.g. `ETag`,
14
+ # `X-RateLimit-Remaining`) that aren't included in the parsed response body.
15
+ #
16
+ # @return [Faraday::Response, nil]
17
+ delegate :last_response, to: :octokit
18
+
19
+ class << self
20
+ # Activates trace mode: every +rest+ and +graphql+ call is recorded into +traced_requests+ until
21
+ # +exit_trace_mode!+ is called. Useful for debugging which GitHub calls a code path makes.
22
+ #
23
+ # @return [void]
24
+ #
25
+ def trace_mode!
26
+ @trace_mode = true
27
+ @traced_requests = []
28
+ end
29
+
30
+ # Disables trace mode and clears any recorded requests.
31
+ #
32
+ # @return [void]
33
+ #
34
+ def exit_trace_mode!
35
+ @trace_mode = false
36
+ @traced_requests = []
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def trace_mode?
41
+ @trace_mode == true
42
+ end
43
+
44
+ # @return [Array<Hash>]
45
+ def traced_requests
46
+ @traced_requests ||= []
47
+ end
48
+ end
49
+
50
+ # @raise [PlanMyStuff::ConfigurationError] if +import_access_token+ is blank when +importing: true+
51
+ #
14
52
  # @return [Client]
15
- def initialize
53
+ #
54
+ def initialize(importing: false)
16
55
  PlanMyStuff.configuration.validate!
17
56
 
18
- @octokit = Octokit::Client.new(access_token: PlanMyStuff.configuration.access_token)
57
+ access_token = importing ? PlanMyStuff.configuration.import_access_token : PlanMyStuff.configuration.access_token
58
+
59
+ if importing && access_token.blank?
60
+ raise(PlanMyStuff::ConfigurationError, 'Import access token is required for import client but not configured')
61
+ end
62
+
63
+ @octokit = Octokit::Client.new(access_token: access_token)
19
64
  end
20
65
 
21
66
  # Delegates a REST API call to Octokit, normalizing errors.
@@ -26,16 +71,10 @@ module PlanMyStuff
26
71
  #
27
72
  # @return [Object] Octokit response
28
73
  #
29
- def rest(method, *, **kwargs, &)
30
- if kwargs.empty?
31
- octokit.public_send(method, *, &)
32
- else
33
- octokit.public_send(method, *, **kwargs, &)
74
+ def rest(method, *args, **kwargs, &block)
75
+ trace!(:rest, method: method, args: args, kwargs: kwargs) do
76
+ execute_rest!(method, *args, **kwargs, &block)
34
77
  end
35
- rescue Octokit::TooManyRequests => e
36
- raise_rate_limit_error(e)
37
- rescue Octokit::ClientError, Octokit::ServerError => e
38
- raise(APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
39
78
  end
40
79
 
41
80
  # Executes a GraphQL query against GitHub's /graphql endpoint.
@@ -46,65 +85,101 @@ module PlanMyStuff
46
85
  # @return [Hash] parsed response data
47
86
  #
48
87
  def graphql(query, variables: {})
49
- payload = { query: query }
50
- payload[:variables] = variables unless variables.empty?
51
-
52
- response = octokit.post('/graphql', payload.to_json)
53
- data =
54
- if response.is_a?(Hash)
55
- response
56
- else
57
- (response.respond_to?(:to_h) ? response.to_h : response)
58
- end
59
-
60
- data = data.deep_symbolize_keys if data.respond_to?(:deep_symbolize_keys)
61
-
62
- check_graphql_errors!(data)
63
-
64
- data[:data]
65
- rescue Octokit::TooManyRequests => e
66
- raise_rate_limit_error(e)
67
- rescue Octokit::ClientError, Octokit::ServerError => e
68
- raise(APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
88
+ trace!(:graphql, query: query, variables: variables) do
89
+ execute_graphql!(query, variables)
90
+ end
69
91
  end
70
92
 
71
93
  # Resolves a repo param to a full "Org/Repo" string.
72
94
  #
73
- # @param repo [Symbol, String, nil] repo key, full string, or nil for default
95
+ # @param repo [Symbol, String, PlanMyStuff::Repo, nil] repo key, full string, Repo instance, or nil for default
74
96
  #
75
97
  # @return [String] full repo path (e.g. "BrandsInsurance/Element")
76
98
  #
77
- # @raise [ArgumentError] if repo cannot be resolved
78
- #
79
- def resolve_repo(repo = nil)
80
- repo ||= PlanMyStuff.configuration.default_repo
81
-
82
- if repo.nil?
83
- raise(
84
- PlanMyStuff::ConfigurationError,
85
- 'No repo provided and config.default_repo is not set. ' \
86
- 'Either pass repo: explicitly or set config.default_repo in your initializer.',
87
- )
99
+ def resolve_repo!(repo = nil)
100
+ PlanMyStuff::Repo.resolve!(repo).full_name
101
+ end
102
+
103
+ private
104
+
105
+ # @raise [PlanMyStuff::APIError] if Octokit call fails with client or server error
106
+ #
107
+ # @return [Object]
108
+ #
109
+ def execute_rest!(method, *, **kwargs, &)
110
+ if kwargs.empty?
111
+ octokit.public_send(method, *, &)
112
+ else
113
+ octokit.public_send(method, *, **kwargs, &)
114
+ end
115
+ rescue Octokit::TooManyRequests => e
116
+ raise_rate_limit_error!(e)
117
+ rescue Octokit::ClientError, Octokit::ServerError => e
118
+ raise(PlanMyStuff::APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
88
119
  end
89
120
 
90
- case repo
91
- when Symbol
92
- resolved = PlanMyStuff.configuration.repos[repo]
93
- raise(ArgumentError, "Unknown repo key: #{repo.inspect}") if resolved.nil?
121
+ # @raise [PlanMyStuff::APIError] if Octokit call fails with client or server error
122
+ #
123
+ # @return [Hash]
124
+ #
125
+ def execute_graphql!(query, variables)
126
+ payload = { query: query }
127
+ payload[:variables] = variables unless variables.empty?
128
+
129
+ response = octokit.post('/graphql', payload.to_json)
130
+ data =
131
+ if response.is_a?(Hash)
132
+ response
133
+ else
134
+ (response.respond_to?(:to_h) ? response.to_h : response)
135
+ end
136
+
137
+ data = data.deep_symbolize_keys if data.respond_to?(:deep_symbolize_keys)
138
+
139
+ check_graphql_errors!(data)
140
+
141
+ data[:data]
142
+ rescue Octokit::TooManyRequests => e
143
+ raise_rate_limit_error!(e)
144
+ rescue Octokit::ClientError, Octokit::ServerError => e
145
+ raise(PlanMyStuff::APIError.new(e.message, status: e.respond_to?(:response_status) ? e.response_status : nil))
146
+ end
94
147
 
95
- resolved
96
- when String
97
- repo
98
- else
99
- raise(ArgumentError, "Cannot resolve repo: #{repo.inspect}")
148
+ # Records the wrapped call into +Client.traced_requests+ when trace mode is on; otherwise just yields.
149
+ #
150
+ # @param kind [Symbol] :rest or :graphql
151
+ # @param details [Hash] call-specific details (method/args/query/variables)
152
+ #
153
+ # @return [Object]
154
+ #
155
+ def trace!(kind, **details)
156
+ return yield unless self.class.trace_mode?
157
+
158
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
159
+ begin
160
+ result = yield
161
+ record_trace(kind, start, details)
162
+ result
163
+ rescue => e
164
+ record_trace(kind, start, details, error: e)
165
+ raise
166
+ end
100
167
  end
101
- end
102
168
 
103
- private
169
+ # @return [void]
170
+ def record_trace(kind, start, details, error: nil)
171
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1_000).round(2)
172
+ entry = { kind: kind, duration_ms: duration_ms, status: octokit.last_response&.status, **details }
173
+ if error
174
+ entry[:error] = error.class.name
175
+ entry[:error_message] = error.message
176
+ end
177
+ self.class.traced_requests << entry
178
+ end
104
179
 
105
- # @param data [Hash] parsed GraphQL response
180
+ # @raise [PlanMyStuff::GraphQLError] if response contains errors
106
181
  #
107
- # @raise [GraphQLError] if response contains errors
182
+ # @param data [Hash] parsed GraphQL response
108
183
  #
109
184
  # @return [void]
110
185
  #
@@ -113,16 +188,16 @@ module PlanMyStuff
113
188
  return if errors.blank?
114
189
 
115
190
  messages = errors.filter_map { |e| e[:message] }
116
- raise(GraphQLError.new(messages.join('; '), errors: errors))
191
+ raise(PlanMyStuff::GraphQLError.new(messages.join('; '), errors: errors))
117
192
  end
118
193
 
119
- # @param exception [Octokit::TooManyRequests]
194
+ # @raise [PlanMyStuff::RateLimitError] if GitHub has been rate limited
120
195
  #
121
- # @raise [RateLimitError]
196
+ # @param exception [Octokit::TooManyRequests]
122
197
  #
123
- def raise_rate_limit_error(exception)
198
+ def raise_rate_limit_error!(exception)
124
199
  retry_after = parse_retry_after(exception)
125
- raise(RateLimitError.new(exception.message, retry_after: retry_after))
200
+ raise(PlanMyStuff::RateLimitError.new(exception.message, retry_after: retry_after))
126
201
  end
127
202
 
128
203
  # @param exception [Octokit::TooManyRequests]