diffend-monitor 0.2.27

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.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module Config
5
+ # Class used to figure out the file from which we should load the settings
6
+ module FileFinder
7
+ # Names of the files or paths where we will look for the settings
8
+ #
9
+ # @note We do the double dot trick, to look outside of the current dir because when
10
+ # executed from a docker container, we copy the local uncommitted settings into the
11
+ # dir above the app location not to pollute the reset state of the git repo
12
+ #
13
+ # @note Order is important, as for local env we should load from
14
+ # local file (if present first)
15
+ FILE_NAMES = %w[
16
+ .diffend.yml
17
+ ].map { |name| ["../#{name}", name] }.tap(&:flatten!).freeze
18
+
19
+ private_constant :FILE_NAMES
20
+
21
+ class << self
22
+ # Looks for Diffend settings file for a given env
23
+ #
24
+ # @param build_path [String] path of the current build
25
+ #
26
+ # @return [String] path to the file from which we should load all the settings
27
+ def call(build_path)
28
+ FILE_NAMES
29
+ .map { |name| File.join(build_path, name) }
30
+ .map { |name| Dir[name] }
31
+ .find { |selection| !selection.empty? }
32
+ .tap { |path| path || raise(Errors::MissingConfigurationFile) }
33
+ .first
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Module for all the components related to setting up the config
5
+ module Config
6
+ # Class responsible for validating the config from .diffend.yml
7
+ module Validator
8
+ class << self
9
+ # @param config [OpenStruct] path of the current build
10
+ def call(config)
11
+ raise Errors::ProjectIdMissingInConfigurationFile if missing?(config, 'project_id')
12
+ raise Errors::ShareableIdMissingInConfigurationFile if missing?(config, 'shareable_id')
13
+ raise Errors::ShareableKeyMissingInConfigurationFile if missing?(config, 'shareable_key')
14
+ raise Errors::BuildPathMissingInConfigurationFile if missing?(config, 'build_path')
15
+ end
16
+
17
+ private
18
+
19
+ def missing?(config, key)
20
+ config.public_send(key).nil? || config.public_send(key).empty?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Build runner app errors
5
+ module Errors
6
+ # Base error class from which all the errors should inherit
7
+ BaseError = Class.new(StandardError)
8
+ # Raised when we couldn't find a valid configuration file
9
+ MissingConfigurationFile = Class.new(BaseError)
10
+ # Raised when configuration file is empty
11
+ EmptyConfigurationFile = Class.new(BaseError)
12
+ # Raised when configuration file is malformed
13
+ MalformedConfigurationFile = Class.new(BaseError)
14
+ # Raised when project_id is missing in configuration file
15
+ ProjectIdMissingInConfigurationFile = Class.new(BaseError)
16
+ # Raised when shareable_id is missing in configuration file
17
+ ShareableIdMissingInConfigurationFile = Class.new(BaseError)
18
+ # Raised when shareable_key is missing in configuration file
19
+ ShareableKeyMissingInConfigurationFile = Class.new(BaseError)
20
+ # Raised when build_path is missing in configuration file
21
+ BuildPathMissingInConfigurationFile = Class.new(BaseError)
22
+ # Raised when server-side error occurs
23
+ RequestServerError = Class.new(BaseError)
24
+ # Raised when we had an exception that we know how to handle
25
+ HandledException = Class.new(BaseError)
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Diffend
6
+ module HandleErrors
7
+ # Module responsible for building exception payload
8
+ module BuildExceptionPayload
9
+ class << self
10
+ # Build exception payload
11
+ #
12
+ # @param exception [Exception, NilClass] expection that was raised
13
+ # @param payload [Hash] with versions to check
14
+ #
15
+ # @return [Hash]
16
+ def call(exception, payload)
17
+ {
18
+ request_id: SecureRandom.uuid,
19
+ payload: payload,
20
+ exception: {
21
+ class: exception&.class,
22
+ message: exception&.message,
23
+ backtrace: exception&.backtrace
24
+ }
25
+ }.freeze
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for displaying exception payload to stdout
6
+ module DisplayToStdout
7
+ class << self
8
+ # Display exception payload to stdout
9
+ #
10
+ # @param exception_payload [Hash]
11
+ def call(exception_payload)
12
+ puts exception_payload.to_json
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ module Messages
6
+ PAYLOAD_DUMP = '^^^ Above is the dump of your request ^^^'
7
+ UNHANDLED_EXCEPTION = <<~MSG
8
+ \nSomething went really wrong. We recorded this incident in our system and will review it.\n
9
+ This is a bug, don't hesitate.\n
10
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
11
+ MSG
12
+ UNSUPPORTED_RESPONSE = <<~MSG
13
+ \nAPI returned an unsupported response. We recorded this incident in our system and will review it.\n
14
+ This is a bug, don't hesitate.\n
15
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
16
+ MSG
17
+ UNSUPPORTED_VERDICT = <<~MSG
18
+ \nAPI returned an unsupported verdict. We recorded this incident in our system and will review it.\n
19
+ This is a bug, don't hesitate.\n
20
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
21
+ MSG
22
+ REQUEST_ERROR = <<~MSG
23
+ \nWe were unable to process your request at this time. We recorded this incident in our system and will review it.\n
24
+ If you think that this is a bug, don't hesitate.\n
25
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
26
+ MSG
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for reporting errors to diffend
6
+ module Report
7
+ class << self
8
+ # Execute request to Diffend
9
+ #
10
+ # @param exception [Exception] expection that was raised
11
+ # @param payload [Hash] with versions to check
12
+ # @param config [OpenStruct] Diffend config
13
+ # @param message [Symbol] message that we want to display
14
+ # @param report [Boolean] if true we will report the issue to diffend
15
+ # @param raise_exception [Boolean] if true we will raise an exception
16
+ #
17
+ # @return [Net::HTTPResponse] response from Diffend
18
+ def call(config:, message:, exception: nil, payload: {}, report: false, raise_exception: true)
19
+ exception_payload = prepare_exception_payload(exception, payload)
20
+
21
+ Bundler.ui.error(Diffend::HandleErrors::Messages::PAYLOAD_DUMP)
22
+ Bundler.ui.error(Diffend::HandleErrors::Messages.const_get(message.to_s.upcase))
23
+
24
+ if report
25
+ Diffend::Request.call(
26
+ build_request_object(config, exception_payload)
27
+ )
28
+ end
29
+
30
+ raise Diffend::Errors::HandledException if raise_exception
31
+ end
32
+
33
+ # @param config [OpenStruct] diffend config
34
+ # @param payload [Hash]
35
+ #
36
+ # @return [Diffend::RequestObject]
37
+ def build_request_object(config, payload)
38
+ Diffend::RequestObject.new(
39
+ config: config,
40
+ url: errors_url(config.project_id),
41
+ payload: payload,
42
+ request_method: :post
43
+ )
44
+ end
45
+
46
+ # Prepare exception payload and display it to stdout
47
+ #
48
+ # @param exception [Exception] expection that was raised
49
+ # @param payload [Hash] with versions to check
50
+ #
51
+ # @return [Hash]
52
+ def prepare_exception_payload(exception, payload)
53
+ Diffend::HandleErrors::BuildExceptionPayload
54
+ .call(exception, payload)
55
+ .tap(&Diffend::HandleErrors::DisplayToStdout.method(:call))
56
+ end
57
+
58
+ # Provides diffend errors endpoint url
59
+ #
60
+ # @param project_id [String] diffend project_id
61
+ #
62
+ # @return [String] diffend endpoint
63
+ def errors_url(project_id)
64
+ return ENV['DIFFEND_ERROR_URL'] if ENV.key?('DIFFEND_ERROR_URL')
65
+
66
+ "https://my.diffend.io/api/projects/#{project_id}/errors"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[
4
+ build_bundler_definition
5
+ errors
6
+ config/fetcher
7
+ config/file_finder
8
+ config/validator
9
+ commands
10
+ handle_errors/messages
11
+ handle_errors/build_exception_payload
12
+ handle_errors/display_to_stdout
13
+ handle_errors/report
14
+ request_object
15
+ request
16
+ voting
17
+ track
18
+ ].each { |file| require "diffend/#{file}" }
19
+
20
+ %w[
21
+ versions/local
22
+ versions/remote
23
+ ].each { |file| require "diffend/voting/#{file}" }
24
+
25
+ Thread.new do
26
+ track = Diffend::Track.new
27
+ track.start
28
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+
7
+ module Diffend
8
+ # Module responsible for doing request to Diffend
9
+ module Request
10
+ # Message displayed when connection issue occured and we will retry
11
+ CONNECTION_MESSAGE = 'We experienced a connection issue, retrying...'
12
+ # List of connection exceptions
13
+ CONNECTION_EXCEPTIONS = [
14
+ Errno::ECONNRESET,
15
+ Errno::ENETUNREACH,
16
+ Errno::EHOSTUNREACH,
17
+ Errno::ECONNREFUSED
18
+ ].freeze
19
+ # Message displayed when timeout occured and we will retry
20
+ TIMEOUT_MESSAGE = 'We experienced a connection issue, retrying...'
21
+ # List of timeout exceptions
22
+ TIMEOUT_EXCEPTIONS = [
23
+ Net::OpenTimeout,
24
+ Net::ReadTimeout
25
+ ].freeze
26
+ # Message displayed when server issue occured and we will retry
27
+ SERVER_ERROR_MESSAGE = 'We experienced a server-side issue, retrying...'
28
+ # List of server issues
29
+ #
30
+ # 500 - Internal Server Error
31
+ # 502 - Bad Gateway
32
+ # 503 - Service Unavailable
33
+ # 504 - Gateway Timeout
34
+ SERVER_ERRORS = [500, 502, 503, 504].freeze
35
+ # Number of retries
36
+ RETRIES = 3
37
+ # Request headers
38
+ HEADERS = { 'Content-Type': 'application/json' }.freeze
39
+
40
+ private_constant :HEADERS
41
+
42
+ class << self
43
+ # Execute request
44
+ #
45
+ # @param request_object [Diffend::RequestObject]
46
+ #
47
+ # @return [Net::HTTPResponse] response from Diffend
48
+ def call(request_object)
49
+ retry_count ||= -1
50
+
51
+ build_http(request_object.url) do |http, uri|
52
+ response = http.request(
53
+ build_request(
54
+ uri,
55
+ request_object.request_method,
56
+ request_object.config,
57
+ request_object.payload
58
+ )
59
+ )
60
+
61
+ if SERVER_ERRORS.include?(response.code.to_i)
62
+ raise Diffend::Errors::RequestServerError, response.code.to_i
63
+ end
64
+
65
+ response
66
+ end
67
+ rescue Diffend::Errors::RequestServerError => e
68
+ retry_count += 1
69
+
70
+ retry if handle_retry(SERVER_ERROR_MESSAGE, retry_count)
71
+
72
+ Diffend::HandleErrors::Report.call(
73
+ exception: e,
74
+ payload: request_object.payload,
75
+ config: request_object.config,
76
+ message: :request_error
77
+ )
78
+ rescue *CONNECTION_EXCEPTIONS => e
79
+ retry_count += 1
80
+
81
+ retry if handle_retry(CONNECTION_MESSAGE, retry_count)
82
+
83
+ Diffend::HandleErrors::Report.call(
84
+ exception: e,
85
+ payload: request_object.payload,
86
+ config: request_object.config,
87
+ message: :request_error
88
+ )
89
+ rescue *TIMEOUT_EXCEPTIONS => e
90
+ retry_count += 1
91
+
92
+ retry if handle_retry(TIMEOUT_MESSAGE, retry_count)
93
+
94
+ Diffend::HandleErrors::Report.call(
95
+ exception: e,
96
+ payload: request_object.payload,
97
+ config: request_object.config,
98
+ message: :request_error
99
+ )
100
+ end
101
+
102
+ # Handle retry
103
+ #
104
+ # @param message [String] message we want to display
105
+ # @param retry_count [Integer]
106
+ def handle_retry(message, retry_count)
107
+ return false if retry_count == RETRIES
108
+
109
+ Bundler.ui.error(message)
110
+ sleep(exponential_backoff(retry_count))
111
+
112
+ retry_count < RETRIES
113
+ end
114
+
115
+ # Builds http connection object
116
+ #
117
+ # @param url [String] command endpoint url
118
+ def build_http(url)
119
+ uri = URI(url)
120
+
121
+ Net::HTTP.start(
122
+ uri.host,
123
+ uri.port,
124
+ use_ssl: uri.scheme == 'https',
125
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
126
+ open_timeout: 5,
127
+ read_timeout: 5
128
+ ) { |http| yield(http, uri) }
129
+ end
130
+
131
+ # Build http post request and assigns headers and payload
132
+ #
133
+ # @param uri [URI::HTTPS]
134
+ # @param request_method [Symbol]
135
+ # @param config [OpenStruct] Diffend config
136
+ # @param payload [Hash] with versions to check
137
+ #
138
+ # @return [Net::HTTP::Post, Net::HTTP::Put]
139
+ def build_request(uri, request_method, config, payload)
140
+ pick_request_method(request_method)
141
+ .new(uri.request_uri, HEADERS)
142
+ .tap { |request| assign_auth(request, config) }
143
+ .tap { |request| assign_payload(request, payload) }
144
+ end
145
+
146
+ # Pick request method
147
+ #
148
+ # @param request_method [Symbol]
149
+ #
150
+ # @return [Net::HTTP::Post, Net::HTTP::Put]
151
+ def pick_request_method(request_method)
152
+ case request_method
153
+ when :post
154
+ Net::HTTP::Post
155
+ when :put
156
+ Net::HTTP::Put
157
+ end
158
+ end
159
+
160
+ # Assigns basic authorization if provided in the config
161
+ #
162
+ # @param request [Net::HTTP::Post] prepared http post
163
+ # @param config [OpenStruct] Diffend config
164
+ def assign_auth(request, config)
165
+ return unless config
166
+ return unless config.shareable_id
167
+ return unless config.shareable_key
168
+
169
+ request.basic_auth(config.shareable_id, config.shareable_key)
170
+ end
171
+
172
+ # Assigns payload as json
173
+ #
174
+ # @param request [Net::HTTP::Post] prepared http post
175
+ # @param payload [Hash] with versions to check
176
+ def assign_payload(request, payload)
177
+ request.body = JSON.dump(payload: payload)
178
+ end
179
+
180
+ def exponential_backoff(retry_count)
181
+ 2**(retry_count + 1)
182
+ end
183
+ end
184
+ end
185
+ end