appsignal 4.0.4 → 4.0.5

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: 91e5aad04da2524d3d2f4bd983ecd76cf1a33380a1bc2a6bf8aab1bd4d91db5b
4
- data.tar.gz: c7d0582debd5c6d9ed29f2239a4baeed92ebdad46606c423e7537495b4dea3ab
3
+ metadata.gz: 51a7aea13c765d90524be570df11734a859538a18e2934fbf24e2631d9668817
4
+ data.tar.gz: 702e3e82530b79db9d8b26519d14cb9664d728a12f3d6c5999b63bfbda022fd1
5
5
  SHA512:
6
- metadata.gz: a482b32ffa5d9ddc805a65507ab4d526f9c299969c86123743e8f0c4830af6f8abd3eecd805cb7c4eec6abcd474a2f97704ac9f5129825b7958245cc973ff6db
7
- data.tar.gz: c0bf6a6ba6454fee105c6890db3154e03c0ad64c6b5ab325fb0186edf99066e923d7b6b4043ea4d77428155fbc58371ece1309517109437edc8cc265ffda554a
6
+ metadata.gz: 0a290f8c380748ff3ddaca671191d75277f592a296299fc2a21035ebab6afcd58dc2e8796ec197e155e5118b07bccaf2ae1d13c94253f1919388428e83fecd46
7
+ data.tar.gz: bf73768bfbbc2db0c042cb84ddc185362707f7b0b622d0565ce7a5142ddd0479daea6e76d7d87d9b7542ff01929b7a24cf566ca9036e3bd66a5fb0f2de1e622a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # AppSignal for Ruby gem Changelog
2
2
 
3
+ ## 4.0.5
4
+
5
+ _Published on 2024-09-02._
6
+
7
+ ### Added
8
+
9
+ - Report Puma low-level errors using the `lowlevel_error` reporter. This will report errors previously not caught by our instrumentation middleware. (patch [70cc21f4](https://github.com/appsignal/appsignal-ruby/commit/70cc21f49e19faa9fd2d12a051620cd48e036dcb))
10
+
11
+ ### Changed
12
+
13
+ - Log a warning when loader defaults are added after AppSignal has already been configured.
14
+
15
+ ```ruby
16
+ # Bad
17
+ Appsignal.configure # or Appsignal.start
18
+ Appsignal.load(:sinatra)
19
+
20
+ # Good
21
+ Appsignal.load(:sinatra)
22
+ Appsignal.configure # or Appsignal.start
23
+ ```
24
+
25
+ (patch [0997dd9c](https://github.com/appsignal/appsignal-ruby/commit/0997dd9c0430123a697b8100785f5676163e20ef))
26
+ - Rename the `path` and `method` transaction metadata to `request_path` and `request_method` to make it more clear what context this metadata is from. The `path` and `method` metadata will continue to be reported until the next major/minor version. (patch [fa314b5f](https://github.com/appsignal/appsignal-ruby/commit/fa314b5fb6fdfbf3e9746df377b0145cde0cfa36))
27
+ - Internal change to how OpenTelemetry metrics are sent. (patch [e66d1d70](https://github.com/appsignal/appsignal-ruby/commit/e66d1d702d5010cb5b8084ba790b24d9e70a9e08))
28
+
29
+ ### Removed
30
+
31
+ - Drop support for Puma version 2 and lower. (patch [4fab861c](https://github.com/appsignal/appsignal-ruby/commit/4fab861cae74b08aa71bf64e1b134ae4b1df1dff))
32
+
33
+ ### Fixed
34
+
35
+ - Fix the error log message about our `at_exit` hook reporting no error on process exit when the process exits without an error. (patch [b71f3966](https://github.com/appsignal/appsignal-ruby/commit/b71f39661e9b05c10fa78b821ba0e45bde0c941b))
36
+
3
37
  ## 4.0.4
4
38
 
5
39
  _Published on 2024-08-29._
data/ext/agent.rb CHANGED
@@ -6,7 +6,7 @@
6
6
  # Modifications to this file will be overwritten with the next agent release.
7
7
 
8
8
  APPSIGNAL_AGENT_CONFIG = {
9
- "version" => "0.35.21",
9
+ "version" => "0.35.22",
10
10
  "mirrors" => [
11
11
  "https://appsignal-agent-releases.global.ssl.fastly.net",
12
12
  "https://d135dj0rjqvssy.cloudfront.net"
@@ -14,131 +14,131 @@ APPSIGNAL_AGENT_CONFIG = {
14
14
  "triples" => {
15
15
  "x86_64-darwin" => {
16
16
  "static" => {
17
- "checksum" => "0e7b5bc5bf1e9903276b9fbe105af1128011613f9dc7ac7cedd4d230d92011bd",
17
+ "checksum" => "024cef88b24032d7187488c00682f92095da406d80ae642ac858e6318b840ad6",
18
18
  "filename" => "appsignal-x86_64-darwin-all-static.tar.gz"
19
19
  },
20
20
  "dynamic" => {
21
- "checksum" => "56cc9aed70014b4fda2b30ff34819370cab2eb2bda588f59d7d86816d60a458f",
21
+ "checksum" => "ddddf86a951fe57f50e351a449df65d2c6428d703581b8ef5922fad23a688f1d",
22
22
  "filename" => "appsignal-x86_64-darwin-all-dynamic.tar.gz"
23
23
  }
24
24
  },
25
25
  "universal-darwin" => {
26
26
  "static" => {
27
- "checksum" => "0e7b5bc5bf1e9903276b9fbe105af1128011613f9dc7ac7cedd4d230d92011bd",
27
+ "checksum" => "024cef88b24032d7187488c00682f92095da406d80ae642ac858e6318b840ad6",
28
28
  "filename" => "appsignal-x86_64-darwin-all-static.tar.gz"
29
29
  },
30
30
  "dynamic" => {
31
- "checksum" => "56cc9aed70014b4fda2b30ff34819370cab2eb2bda588f59d7d86816d60a458f",
31
+ "checksum" => "ddddf86a951fe57f50e351a449df65d2c6428d703581b8ef5922fad23a688f1d",
32
32
  "filename" => "appsignal-x86_64-darwin-all-dynamic.tar.gz"
33
33
  }
34
34
  },
35
35
  "aarch64-darwin" => {
36
36
  "static" => {
37
- "checksum" => "423178f0178f15d7f663451e45a318ca92d8f7cca136b5e985e753520b4ad3e4",
37
+ "checksum" => "b8dfebd77322a1f66e0f0acf1717c2583189016e9749286f61956a3ebbc1a16a",
38
38
  "filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
39
39
  },
40
40
  "dynamic" => {
41
- "checksum" => "c2c0aeba5ddb38584c8b1cc8b2ad2407431c71b944cd4dee47d45828b93619ae",
41
+ "checksum" => "d94ae45a9fa69566a30aaacf9f65bdf21b5d0d2a5990e4ba8f98dedc5956f62f",
42
42
  "filename" => "appsignal-aarch64-darwin-all-dynamic.tar.gz"
43
43
  }
44
44
  },
45
45
  "arm64-darwin" => {
46
46
  "static" => {
47
- "checksum" => "423178f0178f15d7f663451e45a318ca92d8f7cca136b5e985e753520b4ad3e4",
47
+ "checksum" => "b8dfebd77322a1f66e0f0acf1717c2583189016e9749286f61956a3ebbc1a16a",
48
48
  "filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
49
49
  },
50
50
  "dynamic" => {
51
- "checksum" => "c2c0aeba5ddb38584c8b1cc8b2ad2407431c71b944cd4dee47d45828b93619ae",
51
+ "checksum" => "d94ae45a9fa69566a30aaacf9f65bdf21b5d0d2a5990e4ba8f98dedc5956f62f",
52
52
  "filename" => "appsignal-aarch64-darwin-all-dynamic.tar.gz"
53
53
  }
54
54
  },
55
55
  "arm-darwin" => {
56
56
  "static" => {
57
- "checksum" => "423178f0178f15d7f663451e45a318ca92d8f7cca136b5e985e753520b4ad3e4",
57
+ "checksum" => "b8dfebd77322a1f66e0f0acf1717c2583189016e9749286f61956a3ebbc1a16a",
58
58
  "filename" => "appsignal-aarch64-darwin-all-static.tar.gz"
59
59
  },
60
60
  "dynamic" => {
61
- "checksum" => "c2c0aeba5ddb38584c8b1cc8b2ad2407431c71b944cd4dee47d45828b93619ae",
61
+ "checksum" => "d94ae45a9fa69566a30aaacf9f65bdf21b5d0d2a5990e4ba8f98dedc5956f62f",
62
62
  "filename" => "appsignal-aarch64-darwin-all-dynamic.tar.gz"
63
63
  }
64
64
  },
65
65
  "aarch64-linux" => {
66
66
  "static" => {
67
- "checksum" => "bf94592c9640f89d0321acd5a0a9bd6951154c37f97f4778ddc2fc536203dc4f",
67
+ "checksum" => "1f21e14f31b9b91f5ce3da0dcb91224bc9f4a0d834ada75e87f396ca9838cf59",
68
68
  "filename" => "appsignal-aarch64-linux-all-static.tar.gz"
69
69
  },
70
70
  "dynamic" => {
71
- "checksum" => "8f8e60daae9ac10bae66fcf9ea33984046b5c92d911eae71ac1a8bdbaa7fa3eb",
71
+ "checksum" => "cf81cabcd216cf41d55ad2ad50f5eccfc1b3edc0dc78159973f3c0687f5ad86d",
72
72
  "filename" => "appsignal-aarch64-linux-all-dynamic.tar.gz"
73
73
  }
74
74
  },
75
75
  "i686-linux" => {
76
76
  "static" => {
77
- "checksum" => "1553674c377a3e6fa56e31754d2af175e734531a44a76c680ff14cc49d4f0031",
77
+ "checksum" => "6111532d1958f9bbbdbea9104c509636d7b6d311090c609a0c134b68e6ef0dd6",
78
78
  "filename" => "appsignal-i686-linux-all-static.tar.gz"
79
79
  },
80
80
  "dynamic" => {
81
- "checksum" => "93e53b3cb2e2fb89cc3dff690da910fccb46503c3d58e4d3fb1a525fb222eea7",
81
+ "checksum" => "705fd88f0e2a4370766f64c93d123476d37f169190550cc3a2c85d34ce1b1893",
82
82
  "filename" => "appsignal-i686-linux-all-dynamic.tar.gz"
83
83
  }
84
84
  },
85
85
  "x86-linux" => {
86
86
  "static" => {
87
- "checksum" => "1553674c377a3e6fa56e31754d2af175e734531a44a76c680ff14cc49d4f0031",
87
+ "checksum" => "6111532d1958f9bbbdbea9104c509636d7b6d311090c609a0c134b68e6ef0dd6",
88
88
  "filename" => "appsignal-i686-linux-all-static.tar.gz"
89
89
  },
90
90
  "dynamic" => {
91
- "checksum" => "93e53b3cb2e2fb89cc3dff690da910fccb46503c3d58e4d3fb1a525fb222eea7",
91
+ "checksum" => "705fd88f0e2a4370766f64c93d123476d37f169190550cc3a2c85d34ce1b1893",
92
92
  "filename" => "appsignal-i686-linux-all-dynamic.tar.gz"
93
93
  }
94
94
  },
95
95
  "x86_64-linux" => {
96
96
  "static" => {
97
- "checksum" => "497d280dff46f7e72875af69f7b3756f3925bcad9e1922f030f818c8c5fa5d67",
97
+ "checksum" => "9e325faf63b7fe9b9804b4e8ce4bac7deefa6c47aec47faf47d90133d44dbaa5",
98
98
  "filename" => "appsignal-x86_64-linux-all-static.tar.gz"
99
99
  },
100
100
  "dynamic" => {
101
- "checksum" => "1b230cc3e80ef4013defc9d6e80713d1d318ed7f673121433d864b47e239ff73",
101
+ "checksum" => "11bac70bee7e620b31a26a01093a0c9ef01f7eb7508ac0c0a0eae27973cc643f",
102
102
  "filename" => "appsignal-x86_64-linux-all-dynamic.tar.gz"
103
103
  }
104
104
  },
105
105
  "x86_64-linux-musl" => {
106
106
  "static" => {
107
- "checksum" => "2bcbc6b57a0298a97e6549e025ec092f8b3bd2b4e8f430fe7b040f7a2e274527",
107
+ "checksum" => "a76de00904eb40a61a7e68082cb06379aa13f61ec0d25ead828fd2ba022f9be9",
108
108
  "filename" => "appsignal-x86_64-linux-musl-all-static.tar.gz"
109
109
  },
110
110
  "dynamic" => {
111
- "checksum" => "7e89c78e9375d55b035f0e75e13d8dd278c2ddf4618fbe005c26f13d249b303f",
111
+ "checksum" => "b7b34ed9243059259a5fa10daa65adb6c4748e8b9715f64e6e559f88aa7aec03",
112
112
  "filename" => "appsignal-x86_64-linux-musl-all-dynamic.tar.gz"
113
113
  }
114
114
  },
115
115
  "aarch64-linux-musl" => {
116
116
  "static" => {
117
- "checksum" => "f9bfd06e0107f3a491ff6009a1e8c0d6d2598c128e8ee84bd852a2f8d3e8e16b",
117
+ "checksum" => "2e197b99b7052357e466a7d93450d87cbdc07d0e5749630e98adcf7c7fad8936",
118
118
  "filename" => "appsignal-aarch64-linux-musl-all-static.tar.gz"
119
119
  },
120
120
  "dynamic" => {
121
- "checksum" => "187a9a8e58617a0f78077b412dee18351cc975af41500443d7450fc44f2f65d3",
121
+ "checksum" => "ace9ab040e4c412f9d6c0b7d2209388e81f40882f8719a67cc30c60e0bdc50ed",
122
122
  "filename" => "appsignal-aarch64-linux-musl-all-dynamic.tar.gz"
123
123
  }
124
124
  },
125
125
  "x86_64-freebsd" => {
126
126
  "static" => {
127
- "checksum" => "cf8043c78e0e628cf0747dd57327d6c0ffd4147d3bff7b2426110fd2fada0960",
127
+ "checksum" => "0c263f693ba5c06373b9fb4062f14d2b6bf1ff33bc0d737abcccc77bab66a634",
128
128
  "filename" => "appsignal-x86_64-freebsd-all-static.tar.gz"
129
129
  },
130
130
  "dynamic" => {
131
- "checksum" => "072c2312aadf961c3f851552ee255db77f0c4050e47d438dd9f82e536b68bcd6",
131
+ "checksum" => "7545a50f4d127da466e87d9504df4911ed9f8b0ec67b86023e08f8a58ea2b8c2",
132
132
  "filename" => "appsignal-x86_64-freebsd-all-dynamic.tar.gz"
133
133
  }
134
134
  },
135
135
  "amd64-freebsd" => {
136
136
  "static" => {
137
- "checksum" => "cf8043c78e0e628cf0747dd57327d6c0ffd4147d3bff7b2426110fd2fada0960",
137
+ "checksum" => "0c263f693ba5c06373b9fb4062f14d2b6bf1ff33bc0d737abcccc77bab66a634",
138
138
  "filename" => "appsignal-x86_64-freebsd-all-static.tar.gz"
139
139
  },
140
140
  "dynamic" => {
141
- "checksum" => "072c2312aadf961c3f851552ee255db77f0c4050e47d438dd9f82e536b68bcd6",
141
+ "checksum" => "7545a50f4d127da466e87d9504df4911ed9f8b0ec67b86023e08f8a58ea2b8c2",
142
142
  "filename" => "appsignal-x86_64-freebsd-all-dynamic.tar.gz"
143
143
  }
144
144
  }
@@ -15,6 +15,13 @@ module Appsignal
15
15
 
16
16
  # @api private
17
17
  def self.add_loader_defaults(name, env: nil, root_path: nil, **options)
18
+ if Appsignal.config
19
+ Appsignal.internal_logger.warn(
20
+ "The config defaults from the '#{name}' loader are ignored since " \
21
+ "the AppSignal config has already been initialized."
22
+ )
23
+ end
24
+
18
25
  loader_defaults << {
19
26
  :name => name,
20
27
  :env => env,
@@ -24,6 +24,7 @@ module Appsignal
24
24
  class AtExitCallback
25
25
  def self.call
26
26
  error = $! # rubocop:disable Style/SpecialGlobalVars
27
+ return unless error
27
28
  return if ignored_error?(error)
28
29
  return if Appsignal::Transaction.last_errors.include?(error)
29
30
 
@@ -7,10 +7,14 @@ module Appsignal
7
7
  register :puma
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Puma)
10
+ defined?(::Puma) &&
11
+ Gem::Version.new(Puma::Const::VERSION) >= Gem::Version.new("3.0.0")
11
12
  end
12
13
 
13
14
  def install
15
+ require "appsignal/integrations/puma"
16
+ ::Puma::Server.prepend(Appsignal::Integrations::PumaServer)
17
+
14
18
  return unless defined?(::Puma::Cluster)
15
19
 
16
20
  # For clustered mode with multiple workers
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ # @api private
6
+ module PumaServer
7
+ def lowlevel_error(error, env, response_status = 500)
8
+ response =
9
+ if method(:lowlevel_error).super_method.arity.abs == 3 # Puma >= 5
10
+ super
11
+ else # Puma <= 4
12
+ super(error, env)
13
+ end
14
+
15
+ unless PumaServerHelper.ignored_error?(error)
16
+ Appsignal.report_error(error) do |transaction|
17
+ Appsignal::Rack::ApplyRackRequest
18
+ .new(::Rack::Request.new(env))
19
+ .apply_to(transaction)
20
+ transaction.add_tags(
21
+ :reported_by => :puma_lowlevel_error,
22
+ :response_status => response_status
23
+ )
24
+ end
25
+ end
26
+
27
+ response
28
+ end
29
+ end
30
+
31
+ module PumaServerHelper
32
+ IGNORED_ERRORS = [
33
+ # Ignore internal Puma Client IO errors
34
+ # https://github.com/puma/puma/blob/9ee922d28e1fffd02c1d5480a9e13376f92f46a3/lib/puma/server.rb#L536-L544
35
+ "Puma::MiniSSL::SSLError",
36
+ "Puma::HttpParserError",
37
+ "Puma::HttpParserError501"
38
+ ].freeze
39
+
40
+ def self.ignored_error?(error)
41
+ IGNORED_ERRORS.include?(error.class.to_s)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -19,7 +19,6 @@ module Appsignal
19
19
  @app = app
20
20
  @options = options
21
21
  @request_class = options.fetch(:request_class, ::Rack::Request)
22
- @params_method = options.fetch(:params_method, :params)
23
22
  @instrument_event_name = options.fetch(:instrument_event_name, nil)
24
23
  @report_errors = options.fetch(:report_errors, DEFAULT_ERROR_REPORTING)
25
24
  end
@@ -136,52 +135,9 @@ module Appsignal
136
135
  # Override this method to set metadata after the app is called.
137
136
  # Call `super` to also include the default set metadata.
138
137
  def add_transaction_metadata_after(transaction, request)
139
- transaction.set_metadata("path", request.path)
140
-
141
- request_method = request_method_for(request)
142
- transaction.set_metadata("method", request_method) if request_method
143
-
144
- transaction.add_params { params_for(request) }
145
- transaction.add_session_data { session_data_for(request) }
146
- transaction.add_headers do
147
- request.env if request.respond_to?(:env)
148
- end
149
-
150
- queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
151
- transaction.set_queue_start(queue_start) if queue_start
152
- end
153
-
154
- def params_for(request)
155
- return unless request.respond_to?(@params_method)
156
-
157
- request.send(@params_method)
158
- rescue => error
159
- Appsignal.internal_logger.error(
160
- "Exception while fetching params from '#{@request_class}##{@params_method}': " \
161
- "#{error.class} #{error}"
162
- )
163
- nil
164
- end
165
-
166
- def request_method_for(request)
167
- request.request_method
168
- rescue => error
169
- Appsignal.internal_logger.error(
170
- "Exception while fetching the HTTP request method: #{error.class}: #{error}"
171
- )
172
- nil
173
- end
174
-
175
- def session_data_for(request)
176
- return unless request.respond_to?(:session)
177
-
178
- request.session.to_h
179
- rescue => error
180
- Appsignal.internal_logger.error(
181
- "Exception while fetching session data from '#{@request_class}': " \
182
- "#{error.class} #{error}"
183
- )
184
- nil
138
+ Appsignal::Rack::ApplyRackRequest
139
+ .new(request, @options)
140
+ .apply_to(transaction)
185
141
  end
186
142
 
187
143
  def request_for(env)
@@ -79,6 +79,8 @@ module Appsignal
79
79
  #
80
80
  # The EventHandler.on_finish callback should be called first, this is
81
81
  # just a fallback if that doesn't get called.
82
+ #
83
+ # One such scenario is when a Puma "lowlevel_error" occurs.
82
84
  Appsignal::Transaction.complete_current!
83
85
  end
84
86
  transaction.start_event
@@ -5,13 +5,17 @@ module Appsignal
5
5
  # @api private
6
6
  class HanamiMiddleware < AbstractMiddleware
7
7
  def initialize(app, options = {})
8
- options[:params_method] ||= :params
8
+ options[:params_method] = nil
9
9
  options[:instrument_event_name] ||= "process_action.hanami"
10
10
  super
11
11
  end
12
12
 
13
13
  private
14
14
 
15
+ def add_transaction_metadata_after(transaction, request)
16
+ transaction.add_params { params_for(request) }
17
+ end
18
+
15
19
  def params_for(request)
16
20
  ::Hanami::Action.params_class.new(request.env).to_h
17
21
  end
@@ -37,5 +37,73 @@ module Appsignal
37
37
  end
38
38
  end
39
39
  end
40
+
41
+ class ApplyRackRequest
42
+ attr_reader :request, :options
43
+
44
+ def initialize(request, options = {})
45
+ @request = request
46
+ @options = options
47
+ @params_method = options.fetch(:params_method, :params)
48
+ end
49
+
50
+ def apply_to(transaction)
51
+ request_path = request.path
52
+ transaction.set_metadata("request_path", request_path)
53
+ # TODO: Remove in next major/minor version
54
+ transaction.set_metadata("path", request_path)
55
+
56
+ request_method = request_method_for(request)
57
+ if request_method
58
+ transaction.set_metadata("request_method", request_method)
59
+ # TODO: Remove in next major/minor version
60
+ transaction.set_metadata("method", request_method)
61
+ end
62
+
63
+ transaction.add_params { params_for(request) }
64
+ transaction.add_session_data { session_data_for(request) }
65
+ transaction.add_headers do
66
+ request.env if request.respond_to?(:env)
67
+ end
68
+
69
+ queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
70
+ transaction.set_queue_start(queue_start) if queue_start
71
+ end
72
+
73
+ private
74
+
75
+ def params_for(request)
76
+ return if !@params_method || !request.respond_to?(@params_method)
77
+
78
+ request.send(@params_method)
79
+ rescue => error
80
+ Appsignal.internal_logger.error(
81
+ "Exception while fetching params from '#{request.class}##{@params_method}': " \
82
+ "#{error.class} #{error}"
83
+ )
84
+ nil
85
+ end
86
+
87
+ def request_method_for(request)
88
+ request.request_method
89
+ rescue => error
90
+ Appsignal.internal_logger.error(
91
+ "Exception while fetching the HTTP request method: #{error.class}: #{error}"
92
+ )
93
+ nil
94
+ end
95
+
96
+ def session_data_for(request)
97
+ return unless request.respond_to?(:session)
98
+
99
+ request.session.to_h
100
+ rescue => error
101
+ Appsignal.internal_logger.error(
102
+ "Exception while fetching session data from '#{request.class}': " \
103
+ "#{error.class} #{error}"
104
+ )
105
+ nil
106
+ end
107
+ end
40
108
  end
41
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "4.0.4"
4
+ VERSION = "4.0.5"
5
5
  end