deployangel 0.1.7 → 0.1.9

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: 5250331c4f5a2c2715d94bbbfc62f61daae1f58a744e1430a231a23077a7c540
4
- data.tar.gz: 8db08acb92e1b8923038e2f726481197eb082e308d1f59a6e18911a04d6c3301
3
+ metadata.gz: e2bc6936b563039a1c2caa91f3a8125f0c32582366505ab35dfabf2ec90af3c5
4
+ data.tar.gz: 22c805b0adef58922b3513afc19492433421358be2e75fd12513e650a7e57c9c
5
5
  SHA512:
6
- metadata.gz: '0548b47a265acf82aa9cb67e566142ec4e4e648af0e828217066f8104bc24bf305fd397a1bdb2741de7bf919e10e0d8e4d76fcdd2d9e865e0aaf46ebca226acd'
7
- data.tar.gz: e622c67f73c8475ff52dd8628e296de2950dce36b7a83e447d23e3a523fdf950a13801ee654972e2c60d176f8c6ddfd7230bec31159cc604b77782bb4a4757a6
6
+ metadata.gz: 361b042f1d885fc63442dff6d4f4c406b36fd14ea5c226cf04a5bea2f7537770c6b4f4ad169331b95e6f3bdacf37169f90ac70fc6567006203f4884fe021236a
7
+ data.tar.gz: 328b709591c7c5441390fb74dc535d0a515061fd64161bbe555ffe9f4f8602bf32f10676b6119458e44837ee539e96e5e2f4d72b97cafc05e203d3ef2eb77e7b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9 (2026-10-04)
4
+
5
+ - A 4xx response to a request no route matched, such as a bot probing
6
+ `/wp-admin` or `.env`, or middleware turning a request away before
7
+ routing, is still recorded under `GET unmatched` but no longer counts in
8
+ the app's request count, status counts, or latency. It made apps look
9
+ busier than their real pages, and fast 404s diluted their latency. An
10
+ unrouted 5xx still counts.
11
+
12
+ ## 0.1.8 (2026-10-04)
13
+
14
+ - Health checks served by a lambda or a mounted Rack app at a conventional
15
+ path (`/up`, `/health`, `/healthz`, `/healthcheck`, `/health_check`,
16
+ `/livez`, `/readyz`, `/statusz`, `/ping`) are left out, like the health-check
17
+ gems'. A controller at one of these paths is still recorded, since it may be
18
+ a real page.
19
+ - `config.sidekiq_cron_schedule_file` reads sidekiq-cron jobs from the file an
20
+ app loads them from itself, such as `config/sidekiq_schedule.yml.erb` passed
21
+ to `Sidekiq::Cron::Job.load_from_hash!`. ERB is rendered, as in the default
22
+ file.
23
+
3
24
  ## 0.1.7 (2026-10-04)
4
25
 
5
26
  - Health checks from OkComputer, health_check, and rails-healthcheck are left
data/README.md CHANGED
@@ -45,8 +45,10 @@ ENV DEPLOYANGEL_REVISION=$GIT_SHA
45
45
 
46
46
  and build with `docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .`. On
47
47
  DigitalOcean App Platform, set `DEPLOYANGEL_REVISION: ${_self.COMMIT_HASH}` in
48
- the app spec. If the agent finds none of these, its telemetry can't be tied to a
49
- deploy, and the dashboard says so.
48
+ the app spec. On Elastic Beanstalk, see
49
+ [Deploying to Elastic Beanstalk](#deploying-to-elastic-beanstalk). If the agent
50
+ finds none of these, its telemetry can't be tied to a deploy, and the dashboard
51
+ says so.
50
52
 
51
53
  ## What it sends
52
54
 
@@ -73,6 +75,17 @@ deploy, and the dashboard says so.
73
75
  It does not send request bodies, parameters, headers, cookies, SQL, logs, or
74
76
  user data.
75
77
 
78
+ ## Data and pricing
79
+
80
+ - Everything goes to DeployAngel's hosted service, which runs on DigitalOcean
81
+ in the United States. There's no self-hosted version, and no choice of
82
+ region.
83
+ - Telemetry is kept for 21 days, and release history for 7, 30, or 90 days
84
+ depending on the plan. The [privacy policy](https://deployangel.com/privacy)
85
+ has the details and the services DeployAngel uses.
86
+ - DeployAngel is free during the beta, with notice before paid plans start.
87
+ See [pricing](https://deployangel.com/pricing).
88
+
76
89
  ## Safety
77
90
 
78
91
  - Nothing runs on the network during a request. Requests only update
@@ -115,8 +128,11 @@ Health checks aren't recorded: load balancers and uptime monitors call them
115
128
  all the time and they always answer fast, so they would make your app look
116
129
  busier and healthier than its real pages. The agent recognizes Rails' own
117
130
  (`/up`), OkComputer, health_check, and rails-healthcheck, wherever they're
118
- mounted. If your health check is your own controller, list its route the way
119
- the dashboard shows it. HEAD requests to it are left out too:
131
+ mounted, and a lambda or Rack app at a conventional path (`/up`, `/health`,
132
+ `/healthz`, `/healthcheck`, `/health_check`, `/livez`, `/readyz`, `/statusz`,
133
+ `/ping`), such as `get "healthz", to: ->(_) { [200, {}, ["ok"]] }`. If your
134
+ health check is your own controller, list its route the way the dashboard
135
+ shows it. HEAD requests to it are left out too:
120
136
 
121
137
  ```ruby
122
138
  DeployAngel.configure do |config|
@@ -124,6 +140,22 @@ DeployAngel.configure do |config|
124
140
  end
125
141
  ```
126
142
 
143
+ DeployAngel expects declared recurring jobs on schedule. It reads Solid Queue's
144
+ `config/recurring.yml`, sidekiq-scheduler's section of Sidekiq's config, and
145
+ sidekiq-cron's schedule file (`config/schedule.yml` unless sidekiq-cron is
146
+ configured otherwise), rendering ERB in each. If your app loads sidekiq-cron
147
+ jobs itself from another file, for example with `Sidekiq::Cron::Job.load_from_hash!`,
148
+ point the agent at that file:
149
+
150
+ ```ruby
151
+ DeployAngel.configure do |config|
152
+ config.sidekiq_cron_schedule_file = "config/sidekiq_schedule.yml.erb"
153
+ end
154
+ ```
155
+
156
+ Jobs that exist only in Redis, such as ones created in code or in the
157
+ Sidekiq web UI, aren't read.
158
+
127
159
  `DEPLOYANGEL_ENABLED=true|false` forces reporting on or off in any environment.
128
160
  `DEPLOYANGEL_URL` overrides the API endpoint (default `https://api.deployangel.com`).
129
161
 
@@ -182,6 +214,28 @@ to the run. Explicit options still win.
182
214
  DEPLOYANGEL_API_TOKEN: ${{ secrets.DEPLOYANGEL_API_TOKEN }}
183
215
  ```
184
216
 
217
+ ### When a release never reports
218
+
219
+ A registered release must start reporting within 15 minutes of being
220
+ registered. If no instance reports it by then, the verdict is
221
+ **inconclusive** with reason `no_release_telemetry`, and the dashboard says
222
+ "Not cleared: this release never reported". The usual causes are a release
223
+ that never booted (a failed migration or a crash on start), an agent with no
224
+ token, or a release identity the agent can't find. If the app kept reporting
225
+ an earlier release instead, the verdict names it ("while v41 kept running").
226
+ `deployangel verify --wait` exits 2, so the CI step above fails the job.
227
+
228
+ If the release does start reporting within 24 hours, as after a slow rolling
229
+ deploy, DeployAngel withdraws that verdict and verifies the release from then,
230
+ unless a newer release has rolled out first. A CI job that already failed stays
231
+ failed; run `deployangel verify --wait` again to wait for the new verdict.
232
+
233
+ Only registered releases are caught this way. DeployAngel learns about an
234
+ unregistered release when the agent first reports it, so a release that never
235
+ boots is never seen. If a release that dies on boot is a risk for you, register
236
+ deploys from CI. The 15 minutes count from registration, so register when the
237
+ deploy finishes, not when it starts.
238
+
185
239
  ### Deploying with Kamal
186
240
 
187
241
  The agent reads `KAMAL_VERSION`, so it knows its release with no setup. Pass
@@ -234,6 +288,35 @@ set :deployangel_register, false # turn it off, e.g. for a stage without De
234
288
  With `:deployangel_wait`, `cap` exits with an error if the release fails
235
289
  verification, which CI can act on. It never rolls anything back.
236
290
 
291
+ ### Deploying to Elastic Beanstalk
292
+
293
+ Elastic Beanstalk doesn't tell the app which commit it's running. Set
294
+ `DEPLOYANGEL_REVISION` in the same `update-environment` call that deploys the
295
+ new version, so the two always change together. This works on every platform,
296
+ Docker included, with no build args. Then wait for the environment and register
297
+ the release:
298
+
299
+ ```bash
300
+ aws elasticbeanstalk update-environment \
301
+ --environment-name "$EB_ENV" \
302
+ --version-label "$VERSION_LABEL" \
303
+ --option-settings "Namespace=aws:elasticbeanstalk:application:environment,OptionName=DEPLOYANGEL_REVISION,Value=$GITHUB_SHA"
304
+ aws elasticbeanstalk wait environment-updated --environment-names "$EB_ENV"
305
+
306
+ bundle exec deployangel release --commit="$GITHUB_SHA" --version="$VERSION_LABEL"
307
+ bundle exec deployangel verify --wait --until=initial
308
+ ```
309
+
310
+ `update-environment` returns as soon as the deploy starts, so don't skip the
311
+ wait: registering early starts the
312
+ [15-minute clock](#when-a-release-never-reports) before the new version is
313
+ running. If the deploy fails and Beanstalk rolls back, the environment still
314
+ returns to Ready, but the new commit never reports, so `verify` exits 2.
315
+
316
+ Leave `DEPLOYANGEL_RELEASE_VERSION` unset here. When the agent reports a
317
+ version, the release is matched by it, so it would have to equal `--version`
318
+ exactly. Matching by commit avoids that.
319
+
237
320
  ## CLI and coding agents
238
321
 
239
322
  The gem ships a `deployangel` command. It doesn't boot Rails. Give it a token
@@ -42,12 +42,12 @@ module DeployAngel
42
42
  @active
43
43
  end
44
44
 
45
- def record_request(route_key:, status:, duration_ms:, unhandled: false)
45
+ def record_request(route_key:, status:, duration_ms:, unhandled: false, in_totals: true)
46
46
  return unless @active
47
47
 
48
48
  after_fork! if Process.pid != @pid
49
49
  start_reporter
50
- @aggregator.record(route_key: route_key, status: status, duration_ms: duration_ms, unhandled: unhandled)
50
+ @aggregator.record(route_key: route_key, status: status, duration_ms: duration_ms, unhandled: unhandled, in_totals: in_totals)
51
51
  rescue StandardError => e
52
52
  warn_once(:record, "DeployAngel failed to record a request: #{e.class}: #{e.message}")
53
53
  end
@@ -11,7 +11,7 @@ module DeployAngel
11
11
 
12
12
  attr_accessor :token, :endpoint, :enabled, :environments, :release_version, :revision,
13
13
  :flush_interval, :open_timeout, :read_timeout, :max_queued_payloads, :max_routes, :logger,
14
- :file_digests, :critical_flows, :ignored_routes
14
+ :file_digests, :critical_flows, :ignored_routes, :sidekiq_cron_schedule_file
15
15
 
16
16
  def initialize(env = ENV)
17
17
  @token = env["DEPLOYANGEL_TOKEN"]
@@ -30,6 +30,9 @@ module DeployAngel
30
30
  @critical_flows = {}
31
31
  # Route keys as the dashboard shows them, such as "GET /healthz".
32
32
  @ignored_routes = []
33
+ # For an app that loads sidekiq-cron's jobs itself (load_from_hash!)
34
+ # from its own file, such as "config/sidekiq_schedule.yml.erb".
35
+ @sidekiq_cron_schedule_file = nil
33
36
  end
34
37
 
35
38
  # Reports only with a token. By default only in the listed
@@ -89,11 +89,13 @@ module DeployAngel
89
89
  (@job_classes[key] ||= JobStats.empty).record_discard
90
90
  end
91
91
 
92
- def record(route_key, status, duration_ms, unhandled, max_routes)
93
- @requests += 1
94
- @status_counts[status.to_s] += 1 if status >= 400
95
- @unhandled_exceptions += 1 if unhandled
96
- @histogram.record(duration_ms)
92
+ def record(route_key, status, duration_ms, unhandled, max_routes, in_totals)
93
+ if in_totals
94
+ @requests += 1
95
+ @status_counts[status.to_s] += 1 if status >= 400
96
+ @unhandled_exceptions += 1 if unhandled
97
+ @histogram.record(duration_ms)
98
+ end
97
99
 
98
100
  key = @routes.key?(route_key) || @routes.size < max_routes - 1 ? route_key : OTHER_ROUTE
99
101
  route = (@routes[key] ||= RouteStats.empty)
@@ -112,11 +114,13 @@ module DeployAngel
112
114
  @mutex = Mutex.new
113
115
  end
114
116
 
115
- def record(route_key:, status:, duration_ms:, unhandled: false)
117
+ # in_totals: false records the request under its route only, leaving it
118
+ # out of the app-wide counts and latency.
119
+ def record(route_key:, status:, duration_ms:, unhandled: false, in_totals: true)
116
120
  started_at = period_start(@clock.call)
117
121
  @mutex.synchronize do
118
122
  (@periods[started_at] ||= Period.new(started_at))
119
- .record(route_key, status.to_i, duration_ms, unhandled, @max_routes)
123
+ .record(route_key, status.to_i, duration_ms, unhandled, @max_routes, in_totals)
120
124
  end
121
125
  end
122
126
 
@@ -13,6 +13,9 @@ module DeployAngel
13
13
  # counting them would make any app look busy and healthy. Others can
14
14
  # be left out with config.ignored_routes.
15
15
  HEALTH_CHECK_CONTROLLERS = %w[rails/health ok_computer/ok_computer health_check/health_check healthcheck/healthchecks].freeze
16
+ # Health checks served by a lambda or a mounted Rack app rather than a
17
+ # controller, at a conventional path such as /healthz.
18
+ HEALTH_CHECK_PATHS = %w[up health healthz healthcheck health_check livez readyz statusz ping].freeze
16
19
 
17
20
  def initialize(app)
18
21
  @app = app
@@ -37,12 +40,12 @@ module DeployAngel
37
40
 
38
41
  private
39
42
  def record(env, status, started, unhandled:, exception: nil)
40
- return if HEALTH_CHECK_CONTROLLERS.include?(env["action_dispatch.request.path_parameters"]&.dig(:controller))
43
+ return if health_check?(env)
41
44
 
42
45
  route = route_key(env)
43
46
  return if route && DeployAngel.configuration.ignored_route?(route)
44
47
  # Unrouted successes are static files and similar; unrouted
45
- # errors (such as routing 404s) still count.
48
+ # errors (such as routing 404s) are still recorded.
46
49
  return if route.nil? && status.to_i < 400
47
50
 
48
51
  key = route || "#{env["REQUEST_METHOD"]} unmatched"
@@ -51,12 +54,28 @@ module DeployAngel
51
54
  route_key: key,
52
55
  status: status.to_i,
53
56
  duration_ms: (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0,
54
- unhandled: unhandled
57
+ unhandled: unhandled,
58
+ # A 4xx no route matched is mostly bots probing paths like
59
+ # /wp-admin, or middleware turning requests away. It stays
60
+ # visible under "unmatched", but out of the app's totals, so it
61
+ # doesn't add to the evidence or dilute real pages' latency. An
62
+ # unrouted 5xx still counts: something broke.
63
+ in_totals: !route.nil? || status.to_i >= 500
55
64
  )
56
65
  rescue StandardError
57
66
  nil
58
67
  end
59
68
 
69
+ # A controller at a health-check path may be a real page, so only
70
+ # controllerless routes are judged by their path.
71
+ def health_check?(env)
72
+ controller = env["action_dispatch.request.path_parameters"]&.dig(:controller)
73
+ return HEALTH_CHECK_CONTROLLERS.include?(controller) if controller
74
+
75
+ pattern = route_pattern(env) or return false
76
+ HEALTH_CHECK_PATHS.include?(pattern.delete_suffix(FORMAT_SUFFIX).split("/").last)
77
+ end
78
+
60
79
  def route_key(env)
61
80
  method = env["REQUEST_METHOD"]
62
81
  if (pattern = route_pattern(env))
@@ -86,8 +86,10 @@ module DeployAngel
86
86
  end
87
87
 
88
88
  # sidekiq-cron jobs from its schedule file (config/schedule.yml unless
89
- # configured otherwise), which it loads when Sidekiq starts. Jobs
90
- # created in code live only in Redis, which the agent doesn't read.
89
+ # configured otherwise), which it loads when Sidekiq starts, or from
90
+ # the file in config.sidekiq_cron_schedule_file for an app that loads
91
+ # them itself. Jobs created in code live only in Redis, which the agent
92
+ # doesn't read.
91
93
  def sidekiq_cron_schedules
92
94
  return [] unless defined?(::Sidekiq::Cron)
93
95
 
@@ -105,7 +107,8 @@ module DeployAngel
105
107
  end
106
108
 
107
109
  def sidekiq_cron_schedule_file
108
- configured = ::Sidekiq::Cron.configuration.cron_schedule_file if ::Sidekiq::Cron.respond_to?(:configuration)
110
+ configured = @config.sidekiq_cron_schedule_file
111
+ configured ||= ::Sidekiq::Cron.configuration.cron_schedule_file if ::Sidekiq::Cron.respond_to?(:configuration)
109
112
  File.expand_path(configured || "config/schedule.yml", @root)
110
113
  rescue StandardError
111
114
  File.join(@root, "config", "schedule.yml")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deployangel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens