chronos-ruby 0.9.0.pre.1 → 0.9.0.pre.2

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: 34b5d9d1193017931d129dc366922b276d9b85ced0321e78d3f4a5b1a8c13e40
4
- data.tar.gz: 72277b74f51825118ce6cb2dda83652f035bbfef3cb56c18b7ee98cc3e6d1d54
3
+ metadata.gz: ba6053cbe8155fba7fa99c0adf09db9e0aa38d33314b2355c49f1760233183fe
4
+ data.tar.gz: 138ee6958622e5e03d24863c553066c6368459fe423731950bcfcc63bfb12e02
5
5
  SHA512:
6
- metadata.gz: 2c6b44a856bbb8975c6a4dc9aba5d9bcd2cf9a9d44456c19fe373860872491ff310f11a2a1b4c7588306ae2795caf4af8cf008e38c10bcdbe0322fca9a803ece
7
- data.tar.gz: c4ff3f4da09810013c0e4d352bb18cd04c0a652c160816465d4b088490d5eea5f722c326ab8a6dcc9806f7b74c050ee0c77e8c6a4c604ddf650cb08fc66a3fd4
6
+ metadata.gz: 58b5255dacdb15e3df4147cceb563e247989fa8cc2e72c23e79166e51c772ffc9ba0e87120c025de5d20cff04be21a7625cafb39d9f47793f82d156bdcb5cf09
7
+ data.tar.gz: 981f49d6f3d12314278456338e975797c27df3f7d93217eb2a8c9df630faa6081a683159b7694e0963cd7f9fa4ed6fae5f095b697a1ea9ce1c3159f388a5b36d
data/CHANGELOG.md CHANGED
@@ -12,6 +12,39 @@ All notable changes are documented here. The project follows Semantic Versioning
12
12
  - legacy CI now resolves Bundler 1.17.3 through `Gem.bin_path` on RubyGems versions that do not support the `_version_` executable selector.
13
13
  - documentation verification now reads source and Markdown files explicitly as UTF-8 on legacy container locales.
14
14
 
15
+ ## [0.9.0.pre.2] - 2026-07-21
16
+
17
+ ### Added
18
+
19
+ - bounded startup and runtime exception ignore rules through `config.ignore_rules` and `Chronos.ignore_if`;
20
+ - Active Job trace/request propagation without modifying public arguments, plus adapter, job/provider IDs, attempts, status, and failure telemetry;
21
+ - real-gem Sidekiq 4.2/5.2 Docker compatibility jobs;
22
+ - repeatable Rack comparison and fake-endpoint load gates;
23
+ - corrected `script/test_all` so `pipefail` no longer turns installed Rubies into false skips and an empty matrix fails;
24
+ - Airbrake migration, SemVer, deprecation, protocol stability, security review, and 1.0 readiness documentation.
25
+
26
+ ### Changed
27
+
28
+ - version advanced to `0.9.0.pre.2` instead of `1.0.0` because the full external legacy matrix has not yet supplied zero-failure evidence;
29
+ - Active Job identifiers are now documented collected fields and propagated context is bounded to trace/request IDs.
30
+
31
+ ### Fixed
32
+
33
+ - the Sidekiq 5 compatibility smoke now resolves the shared Sidekiq 4/5 fixture inside `examples/` instead of the nonexistent `/app/sidekiq-4` path.
34
+
35
+ ### Security
36
+
37
+ - local ignore rules are bounded to 100, contain callback failures, and cannot be installed remotely;
38
+ - fake-endpoint load validation rejects payloads containing the secret authentication key;
39
+ - package signing remains deferred until a trusted key lifecycle exists; protected publishing credentials remain required.
40
+
41
+ ### Known limitations
42
+
43
+ - compatibility stays `Best effort` until all new and existing GitHub Actions matrix jobs pass without skips;
44
+ - end-of-life runtimes and framework dependencies retain their upstream security risk;
45
+ - Active Job adapters that bypass standard serialization/execution hooks need dedicated evidence;
46
+ - comparative results are not a performance claim and must be published with controlled environment metadata.
47
+
15
48
  ## [0.9.0.pre.1] - 2026-07-21
16
49
 
17
50
  ### Added
data/README.md CHANGED
@@ -33,7 +33,7 @@ See [Compatibility](docs/compatibility.md).
33
33
  The current public build is a pre-release. Add its exact version to the application's `Gemfile`:
34
34
 
35
35
  ```ruby
36
- gem "chronos-ruby", "0.9.0.pre.1"
36
+ gem "chronos-ruby", "0.9.0.pre.2"
37
37
  ```
38
38
 
39
39
  Install with a Bundler version compatible with the application. For the oldest supported runtime:
@@ -54,7 +54,7 @@ gem install chronos-ruby --pre
54
54
  Version 0.5 exposes Rails support explicitly, keeping Rails and ActiveSupport out of plain Ruby applications:
55
55
 
56
56
  ```ruby
57
- gem "chronos-ruby", "0.9.0.pre.1", :require => "chronos/rails"
57
+ gem "chronos-ruby", "0.9.0.pre.2", :require => "chronos/rails"
58
58
  ```
59
59
 
60
60
  Generate the initializer with:
@@ -166,7 +166,15 @@ Chronos.configure do |config|
166
166
  end
167
167
  ```
168
168
 
169
- Local exception-specific ignore callbacks are not available in version 0.4. The server may provide a bounded list of exact fingerprints to ignore; it cannot provide regular expressions or executable rules.
169
+ Version `0.9.0.pre.2` adds bounded local rules after configuration:
170
+
171
+ ```ruby
172
+ Chronos.ignore_if do |notice|
173
+ notice.exception_class == "SomeExpectedError"
174
+ end
175
+ ```
176
+
177
+ Rules receive an immutable normalized notice, run before serialization/queueing, and must return exactly `true` to discard. The default limit is 20 and the hard configurable maximum is 100. A failing rule is contained. See [Bounded local ignore rules](docs/modules/ignore-rules.md).
170
178
 
171
179
  ## Performance monitoring
172
180
 
@@ -193,12 +201,12 @@ Version `0.6.0.pre.1` adds optional Sidekiq 4/5 middleware:
193
201
 
194
202
  ```ruby
195
203
  gem "sidekiq", "~> 5.0"
196
- gem "chronos-ruby", "0.9.0.pre.1", :require => "chronos/sidekiq"
204
+ gem "chronos-ruby", "0.9.0.pre.2", :require => "chronos/sidekiq"
197
205
  ```
198
206
 
199
207
  The client middleware propagates only trace/request identifiers in a versioned Sidekiq-envelope field and never changes worker arguments. The server records class, queue, JID, retry count, duration, calculable queue latency, bounded arguments/tags, status, and error class. Values pass through the shared sanitizer before delivery. Failed jobs are notified once and the original exception is re-raised. See [Sidekiq 4/5 legacy integration](docs/modules/sidekiq-legacy.md).
200
208
 
201
- The Rails subscriber continues to record basic Active Job class, queue, and duration. Full Active Job context propagation plus optional Resque and Delayed Job adapters are subsequent version 0.6 increments.
209
+ When Active Job is available, the Rails integration propagates only bounded trace/request identifiers in a namespaced serialized field without changing job arguments. It records adapter, job/provider IDs, class, queue, attempts, duration, status, and error class, and captures a supplied failure once. See [Active Job legacy integration](docs/modules/active-job.md).
202
210
 
203
211
  ## External HTTP, cache, and dependencies
204
212
 
data/SECURITY.md CHANGED
@@ -7,3 +7,5 @@ Never include live project keys, customer payloads, personal data, or production
7
7
  The Chronos 0.x line provides technical compatibility with end-of-life Ruby versions. It cannot provide security maintenance for the Ruby runtime, OpenSSL, operating system, Rails, or other dependencies used by the host application.
8
8
 
9
9
  Security fixes in the agent will be assessed for backporting to maintained Chronos version lines. No response-time commitment is made before a formal security process is published.
10
+
11
+ The current prerelease review and residual risks are recorded in [docs/security-review.md](docs/security-review.md). Release publishing uses a protected GitHub environment and a scoped RubyGems key. Package signing is deferred until the project has a trusted certificate issuance, rotation, revocation, and offline-key process; do not add a private signing key to the repository or workflow secrets without that process.
@@ -0,0 +1,9 @@
1
+ # ADR-018: Pre-1.0 hardening gates
2
+
3
+ ## Decision
4
+
5
+ Keep the release at `0.9.0.pre.2` while any mandatory 1.0 evidence remains external or incomplete. Add bounded local ignore rules, Active Job envelope propagation, real Sidekiq compatibility jobs, repeatable comparative/load benchmarks, and explicit release/security policies.
6
+
7
+ ## Consequences
8
+
9
+ The stable API is not promised before the full legacy matrix passes. Active Job gains one namespaced serialized field without changing arguments. Application callbacks remain bounded in count but their execution cost belongs to the application. Package checksums are used until a trusted signing lifecycle is available.
@@ -13,11 +13,14 @@ Chronos Ruby 0.x is the legacy line. Technical compatibility does not make an en
13
13
 
14
14
  | Ruby | Sidekiq integration | Status | Evidence |
15
15
  |---|---|---|---|
16
- | 2.2.10–2.6 | Sidekiq 4 / 5 | Best effort | Middleware unit/integration contracts; dedicated real-gem matrix pending |
16
+ | 2.2.10 | Sidekiq 4.2.10 | Best effort | Unit/integration contracts and dedicated real-gem Docker job |
17
+ | 2.5.9 | Sidekiq 5.2.10 | Best effort | Unit/integration contracts and dedicated real-gem Docker job |
17
18
 
18
19
  Version 0.5 includes Rails 4.2 and 5.2 applications plus a dedicated matrix, but this document conservatively keeps the combinations at `Best effort` until all release-gate evidence, including fake-server payload validation, is green. Rails 5.0 uses the same feature-detected public APIs but does not yet have its own example application.
19
20
 
20
- Version `0.6.0.pre.1` uses the public Sidekiq 4/5 middleware signatures and remains `Best effort` until dedicated jobs exercise both real gem versions across their valid Ruby combinations.
21
+ Version `0.9.0.pre.2` adds dedicated Sidekiq 4.2.10 and 5.2.10 real-gem jobs. Status remains `Best effort` until both external jobs pass on the release candidate.
22
+
23
+ Active Job propagation uses the standard `serialize`, `deserialize`, and `perform_now` extension points with a namespaced bounded field. Rails 4.2/5.2 example jobs and unit contracts provide evidence; the complete external matrix must still pass before stable support is declared.
21
24
 
22
25
  Version `0.7.0.pre.1` keeps the same Ruby/Rails matrix and implements APM aggregation without modern concurrency or SQL-parser dependencies. Its compatibility remains `Best effort` until request/SQL/job aggregate payloads pass the dedicated fake-server gates for every listed runtime.
23
26
 
@@ -32,6 +32,8 @@
32
32
  | `allowlist_keys` | Optional | `[]` | Explicit key-name exceptions; content detection still runs |
33
33
  | `hash_keys` | Optional | `[]` | Scalar identifier keys replaced by scoped SHA-256 values |
34
34
  | `filters` | Optional | `[]` | Callable application-specific privacy filters |
35
+ | `ignore_rules` | Optional | `[]` | Callable local exception rules evaluated before serialization |
36
+ | `max_ignore_rules` | Optional | `20` | Maximum retained local rules; range 1–100 |
35
37
  | `anonymize_ip` | Recommended | `true` | Replaces the final octet of supplied IPv4 addresses |
36
38
  | `max_retries` | Optional | `3` | Maximum retry attempts after the first delivery failure |
37
39
  | `retry_base_interval` | Optional | `0.5` | Initial exponential-backoff delay in seconds |
@@ -83,6 +85,8 @@ Chronos.configure do |config|
83
85
  config.workers = 1
84
86
  config.blocklist_keys += [:medical_record, /bank_account/i]
85
87
  config.hash_keys += [:customer_id]
88
+ config.ignore_rules << proc { |notice| notice.exception_class == "SomeExpectedError" }
89
+ config.max_ignore_rules = 20
86
90
  config.max_retries = 3
87
91
  config.backlog_size = 100
88
92
  config.circuit_failure_threshold = 5
@@ -106,7 +110,7 @@ Chronos.configure do |config|
106
110
  end
107
111
  ```
108
112
 
109
- Privacy matcher collections and filters are copied into the immutable runtime snapshot. Matchers must be String, Symbol, or Regexp values, and every custom filter must respond to `call`.
113
+ Privacy matcher collections, filters, and startup ignore rules are copied into the immutable runtime snapshot. Matchers must be String, Symbol, or Regexp values, and every filter/rule must respond to `call`. Runtime rules added through `Chronos.ignore_if` cannot exceed `max_ignore_rules`.
110
114
 
111
115
  The gem never reads all environment variables automatically. The host application decides which values to pass. See [Privacy and LGPD](privacy-lgpd.md) before adding application context.
112
116
 
@@ -26,7 +26,8 @@ Version 0.9 emits exceptions, cache telemetry, dependency/deploy events, and agg
26
26
  | View template basename and duration | Collected with Rails integration | `render_template.action_view` |
27
27
  | SQL operation name, cached flag, duration | Collected; SQL text omitted | `sql.active_record` |
28
28
  | Mailer/action and duration | Collected; message content omitted | `deliver.action_mailer` |
29
- | Active Job class, queue, duration | Collected when available; arguments omitted | `perform.active_job` |
29
+ | Active Job adapter, job/provider IDs, class, queue, attempts, duration, status, error class | Collected when available; arguments omitted | serialization and `perform.active_job` |
30
+ | Active Job trace/request IDs | Propagated in a bounded namespaced field | Current Chronos context |
30
31
  | Sidekiq class, queue, JID, retry count, duration, status, error class | Collected with optional middleware | Sidekiq job envelope and local clock |
31
32
  | Sidekiq queue latency | Collected when enqueue time is available | Sidekiq or Chronos envelope timestamp |
32
33
  | Sidekiq arguments | Collected, sanitized, and bounded | First 20 job arguments; collections/depth/strings limited |
@@ -48,7 +49,7 @@ Version 0.9 emits exceptions, cache telemetry, dependency/deploy events, and agg
48
49
  | Event release, revision, deploy ID, environment, service, region, instance | Present as bounded correlation; values optional | Explicit immutable configuration or normalized deploy payload |
49
50
  | Deploy environment, revision, version, repository, actor, deploy ID, service, region, instance | Explicit synchronous deploy API | Application/deployment integration arguments |
50
51
 
51
- The gem never collects request bodies, response bodies, raw query strings, cookies, HTTP authorization headers, environment variables in bulk, Git state, source code, raw SQL, SQL bind values, database rows, raw cache keys/values, mail bodies/recipients, gem paths, or lockfile contents. Sidekiq JIDs/arguments, loaded gem names/versions, and deploy fields are documented integration data.
52
+ The gem never collects request bodies, response bodies, raw query strings, cookies, HTTP authorization headers, environment variables in bulk, Git state, source code, raw SQL, SQL bind values, database rows, raw cache keys/values, mail bodies/recipients, Active Job arguments, gem paths, or lockfile contents. Active Job IDs, Sidekiq JIDs/arguments, loaded gem names/versions, and deploy fields are documented integration data.
52
53
 
53
54
  APM dimensions never include user ID, job ID, raw URL, exception message, bind value, or cache key. Normalized routes replace common numeric/UUID segments. Normalized SQL can retain schema, table, and column identifiers; review those identifiers as part of the privacy audit.
54
55
 
@@ -0,0 +1,7 @@
1
+ # Deprecation policy
2
+
3
+ After `1.0.0`, a public API scheduled for removal remains available for at least one minor release and normally six months. The warning, replacement, first deprecated version, and earliest removal version must be documented in the changelog and migration material.
4
+
5
+ Warnings are emitted at most once per process through the configured safe logger and must not include application payloads. Security fixes, behavior that can expose secrets, and upstream runtime incompatibilities may require faster action; the security advisory and changelog must explain the exception. Ruby/Rails support changes are recorded in `docs/compatibility.md` before removal.
6
+
7
+ Prereleases may revise APIs without the full stable window, but every incompatible revision must remain explicit. Version `0.9.0.pre.2` introduces no removal.
@@ -0,0 +1,18 @@
1
+ # Migration from Airbrake
2
+
3
+ Version `0.9.0.pre.2` provides a staged migration path. Run both agents only long enough to compare delivery, then remove Airbrake to avoid duplicate reports and overhead.
4
+
5
+ | Airbrake concept | Chronos equivalent |
6
+ |---|---|
7
+ | `Airbrake.configure` credentials/host | `Chronos.configure` with `project_id`, `project_key`, and `host` |
8
+ | `Airbrake.notify(error, params)` | `Chronos.notify(error, :parameters => params)` |
9
+ | `Airbrake.notify_sync` | `Chronos.notify_sync` |
10
+ | ignore/filter callbacks | `Chronos.ignore_if` and `config.filters` |
11
+ | deploy notification | `Chronos.notify_deploy` |
12
+ | Rack/Rails integration | `require "chronos/rails"` or Chronos Rack middleware |
13
+
14
+ Start with `error_notifications`, Rails capture, and APM enabled in one non-production environment. Configure the same application version and environment used by the old notifier. Compare exception count, class, sanitized context, deploy correlation, request/job metrics, and retry diagnostics. Never copy an Airbrake project key into Chronos.
15
+
16
+ Chronos callbacks receive a normalized immutable `Chronos::Core::Notice`, not an Airbrake notice. Filters operate on each sanitized key/value pair. Translate callback logic explicitly and add tests; do not assume callback argument compatibility. Chronos intentionally omits request/response bodies, raw SQL, binds, cookies, authorization headers, mail content, and raw cache keys.
17
+
18
+ Before removing Airbrake, exercise a synchronous notification, an asynchronous notification followed by `Chronos.flush`, a Rails/Rack failure, a background job failure, and an unavailable endpoint. Confirm that the bounded queue/backlog behavior is acceptable. Roll back by disabling Chronos or restoring the previous dependency; no application exception semantics are changed by the agent.
@@ -0,0 +1,7 @@
1
+ # Active Job legacy integration
2
+
3
+ When `ActiveJob::Base` is available, the Rails installer prepends public serialization/execution hooks. Enqueue serialization adds a namespaced `chronos_context` field with schema version plus at most the trace and request identifiers; each identifier is limited to 128 bytes. Public job arguments are unchanged. Deserialization restores this context only around `perform_now`.
4
+
5
+ The `perform.active_job` subscriber records adapter, job ID, provider job ID, class, queue, execution count, duration, status, and error class. Arguments are omitted. A supplied exception is reported once with bounded job context. The integration never changes retry, acknowledgement, adapter, or original exception behavior.
6
+
7
+ This metadata can identify a workflow or tenant in some applications. Review retention and identifier generation under the project's privacy policy.
@@ -0,0 +1,11 @@
1
+ # Bounded local ignore rules
2
+
3
+ Configure startup rules with `config.ignore_rules` or add a rule to the active agent with `Chronos.ignore_if`. A rule receives an immutable normalized notice and must return exactly `true` to discard it before serialization, queueing, retry, or network work.
4
+
5
+ ```ruby
6
+ Chronos.ignore_if do |notice|
7
+ notice.exception_class == "SomeExpectedError"
8
+ end
9
+ ```
10
+
11
+ At most `max_ignore_rules` rules are retained (default 20, hard maximum 100). Registration returns `false` when Chronos is not configured, the object is not callable, or the limit is full. A rule failure is contained and treated as no match. Rules run in the capture caller and therefore must be fast, thread-safe, and free of I/O. Remote configuration cannot add executable rules.
@@ -28,10 +28,10 @@ The public `ActiveSupport::Notifications.subscribe` API is used for:
28
28
  | `render_template.action_view` | `request` with `kind=view` | template basename and duration |
29
29
  | `sql.active_record` | `query` | operation name, cached flag, and duration |
30
30
  | `deliver.action_mailer` | `job` with `kind=mailer` | mailer, action, and duration |
31
- | `perform.active_job` | `job` | job class, queue, and duration, when Active Job is available |
31
+ | `perform.active_job` | `job` | adapter, job/provider IDs, class, queue, attempts, duration, status, and error class |
32
32
  | cache read/write/hit notifications | `cache` | operation, backend, namespace, hit/miss, duration, and optional scoped key hash |
33
33
 
34
- Raw SQL, binds, cache keys, mail recipients and bodies, job IDs and arguments, request/response bodies, cookies, and authorization headers are not copied. Template identifiers are reduced to their basename. Every event passes through `Sanitizer`, `SafeSerializer`, the bounded queue, retry policy, circuit breaker, and memory backlog.
34
+ Raw SQL, binds, cache keys, mail recipients and bodies, job arguments, request/response bodies, cookies, and authorization headers are not copied. Documented Active Job IDs are collected, while public job arguments remain unchanged and omitted. Template identifiers are reduced to their basename. Every event passes through `Sanitizer`, `SafeSerializer`, the bounded queue, retry policy, circuit breaker, and memory backlog.
35
35
 
36
36
  ## Controller exceptions and deduplication
37
37
 
@@ -57,4 +57,4 @@ The repository contains independent applications under `examples/rails-4.2` and
57
57
 
58
58
  ## Limits
59
59
 
60
- Version 0.7 aggregates controller, SQL, and job timings into bounded APM batches. SQL literals and binds remain excluded while a bounded normalized query and fingerprint are produced. Sidekiq support remains a separate optional integration; full Active Job propagation remains a later legacy increment.
60
+ Version 0.7 aggregates controller, SQL, and job timings into bounded APM batches. SQL literals and binds remain excluded while a bounded normalized query and fingerprint are produced. Sidekiq support remains a separate optional integration. Version `0.9.0.pre.2` provides bounded Active Job trace/request propagation through a namespaced serialized field; adapters that replace the standard Active Job serialization/execution hooks require separate compatibility evidence.
@@ -4,7 +4,7 @@ Version `0.6.0.pre.1` starts the legacy jobs line with optional Sidekiq 4 and 5
4
4
 
5
5
  ```ruby
6
6
  gem "sidekiq", "~> 5.0"
7
- gem "chronos-ruby", "0.9.0.pre.1", :require => "chronos/sidekiq"
7
+ gem "chronos-ruby", "0.9.0.pre.2", :require => "chronos/sidekiq"
8
8
  ```
9
9
 
10
10
  `chronos/sidekiq` installs middleware through the public `configure_client` and `configure_server` APIs. It does nothing when Sidekiq is unavailable, and the core gem never requires Sidekiq. Installation adds no Chronos thread or Redis/database connection per job; delivery continues through the agent's existing fixed worker pool.
data/docs/performance.md CHANGED
@@ -29,6 +29,17 @@ Current controls:
29
29
 
30
30
  Run the scripts under `benchmarks/` and record Ruby version, operating system, CPU, warmup, iteration count, median, and dispersion before publishing results. `benchmarks/filtering.rb` measures privacy filtering, `benchmarks/retry_backlog.rb` measures fixed-memory outage behavior, `benchmarks/request_overhead.rb` compares Rack-protocol calls, and `benchmarks/rails_notifications.rb` isolates subscriber normalization overhead.
31
31
 
32
+ ## Version 0.9.0.pre.2 release gates
33
+
34
+ `benchmarks/comparative.rb` compares the same successful Rack fixture without and with Chronos instrumentation. It performs configurable warmup, at least three samples, and reports median plus median absolute deviation. `benchmarks/fake_endpoint_load.rb` sends asynchronous exception events to a local TCP endpoint, verifies the v1 schema marker, ensures the secret key is absent from every payload, and fails on loss, rejection, invalid payload, or timeout.
35
+
36
+ ```bash
37
+ ITERATIONS=50000 WARMUP=5000 SAMPLES=7 bundle exec ruby benchmarks/comparative.rb
38
+ ITERATIONS=500 bundle exec ruby benchmarks/fake_endpoint_load.rb
39
+ ```
40
+
41
+ Results are environment-specific evidence, not a general speed claim. Record CPU, OS, Ruby, gem commit, and environment variables with any published result. Airbrake comparison remains optional and must use a legally compatible, equivalent sanitized payload on the same supported runtime.
42
+
32
43
  ## Version 0.5 Rails subscriber benchmark
33
44
 
34
45
  Run:
@@ -0,0 +1,5 @@
1
+ # Protocol v1 stability
2
+
3
+ The schemas under `contracts/` are the source of truth for protocol v1. Version `0.9.0.pre.2` keeps `schema_version: "1.0"` and treats every required field, enum value, privacy exclusion, and maximum as a compatibility contract.
4
+
5
+ Compatible changes may add optional bounded fields or new event types accepted by the server. Removing or renaming a field, changing its type/meaning, weakening a bound, or making an optional field required needs a new protocol major schema. Authentication remains outside the JSON payload. Contract tests and `script/verify_docs` must pass before a release.
@@ -0,0 +1,18 @@
1
+ # Version 1.0 readiness
2
+
3
+ Version `0.9.0.pre.2` is a hardening release, not the stable release. The local suite, lint, documentation verifier, build, load test, and comparative benchmark are required evidence. The release can advance to `1.0.0` only after the GitHub Actions matrix is green for every declared legacy Ruby/framework/job combination.
4
+
5
+ | Gate | State in pre.2 |
6
+ |---|---|
7
+ | Ruby pure, Rack, Rails 4.2/5.2 | Implemented; external legacy matrix must pass |
8
+ | Sidekiq 4/5 real gems | Dedicated Docker matrix added; must pass |
9
+ | Active Job metadata/error/context | Implemented and contract-tested |
10
+ | filters, bounded ignore rules, context, breadcrumbs | Implemented and documented |
11
+ | request/SQL/job APM, deploy, retry/backlog, remote configuration | Implemented and contract-tested |
12
+ | payload fixture privacy | Contract-tested |
13
+ | public API/options/protocol review | Documented; final review required |
14
+ | Airbrake migration, SemVer, deprecation, security review | Added in pre.2 |
15
+ | fake endpoint load and repeatable comparison | Executable gates added |
16
+ | package signing | Not currently feasible; protected secret plus checksum is the interim control |
17
+
18
+ Do not change compatibility status from `Best effort` or create a `v1.0.0` tag until all external jobs and the dependency audit pass without skips.
@@ -0,0 +1,16 @@
1
+ # Security review for 0.9.0.pre.2
2
+
3
+ Review date: 2026-07-21. Scope: capture, serialization, transport, remote configuration, framework/job integrations, release workflow, examples, and fixtures.
4
+
5
+ Verified by contracts and implementation review:
6
+
7
+ - the secret project key is sent only in an authentication header and never in the event body;
8
+ - sanitization and payload limits run before queueing, retry, backlog, or transport;
9
+ - queue, workers, retries, delays, circuit state, backlog, breadcrumbs, APM groups, dependencies, ignore rules, and propagated identifiers are bounded;
10
+ - TLS verification is default and plain HTTP requires explicit `ssl_verify = false`;
11
+ - remote configuration is size-limited, allowlisted, and cannot replace credentials/host or install executable rules;
12
+ - integrations contain agent failures and do not collect bodies, authorization, raw SQL/binds, mail content, or raw cache keys;
13
+ - Active Job propagation uses a namespaced v1 field containing only bounded trace/request identifiers and does not alter job arguments;
14
+ - fixture privacy is enforced by contract tests and dependency advisories are checked by the security workflow.
15
+
16
+ Residual risks: supported Ruby/Rails versions are end-of-life; in-memory backlog is lost at exit; application filters/ignore rules execute application code; project identifiers and documented job IDs may be personal data in some deployments; package signing is not enabled because no trusted certificate/key lifecycle exists. Release artifacts should use protected environments and published SHA-256 checksums until signing can be operated safely.
data/docs/semver.md ADDED
@@ -0,0 +1,11 @@
1
+ # Semantic Versioning policy
2
+
3
+ Chronos follows Semantic Versioning for the gem API and the independently versioned event protocol.
4
+
5
+ - Before `1.0.0`, incompatible changes may occur only in a documented prerelease and must appear in the changelog and migration notes.
6
+ - From `1.0.0`, patch releases contain compatible fixes, minor releases add compatible capability, and major releases may remove or change public behavior.
7
+ - Public API includes documented `Chronos` methods, configuration options, supported require paths, integration entry points, and documented return/error behavior.
8
+ - Internal constants and files explicitly described as internal are not public API.
9
+ - The JSON `schema_version` is independent of the gem version. Additive optional v1 fields are compatible; removing, renaming, or changing required v1 fields requires a new protocol major version.
10
+
11
+ A release is not promoted to stable while its supported matrix or mandatory release gates are incomplete.
data/lib/chronos/agent.rb CHANGED
@@ -50,6 +50,10 @@ module Chronos
50
50
  @capture.call_sync(exception, context_for_capture(context))
51
51
  end
52
52
 
53
+ def ignore_if(&block)
54
+ @ignore_policy.add(&block)
55
+ end
56
+
53
57
  def with_context(context = {}, &block)
54
58
  @context_store.with_context(context, &block)
55
59
  end
@@ -186,7 +190,12 @@ module Chronos
186
190
  private
187
191
 
188
192
  def initialize_capture(options)
189
- @capture = options[:capture] || Application::CaptureException.new(@config, @delivery_pipeline, @logger)
193
+ @ignore_policy = options[:ignore_policy] || Application::IgnorePolicy.new(
194
+ @config.ignore_rules, @config.max_ignore_rules, @logger
195
+ )
196
+ @capture = options[:capture] || Application::CaptureException.new(
197
+ @config, @delivery_pipeline, @logger, :ignore_policy => @ignore_policy
198
+ )
190
199
  @telemetry = options[:telemetry] || Application::CaptureTelemetry.new(@config, @delivery_pipeline, @logger)
191
200
  end
192
201
 
@@ -18,6 +18,7 @@ module Chronos
18
18
  @delivery_pipeline = delivery_pipeline
19
19
  @logger = logger || Internal::SafeLogger.new(config.logger)
20
20
  @notice_builder = options[:notice_builder] || Core::NoticeBuilder.new(config)
21
+ @ignore_policy = options[:ignore_policy]
21
22
  @serializer = options[:serializer] || Core::PayloadSerializer.new(
22
23
  config,
23
24
  nil,
@@ -29,6 +30,7 @@ module Chronos
29
30
  return false unless capture_enabled?
30
31
 
31
32
  notice = build_notice(exception, context)
33
+ return false if ignored?(notice)
32
34
  return false unless @delivery_pipeline.capture_allowed?("exception", notice.fingerprint)
33
35
 
34
36
  @delivery_pipeline.enqueue(@serializer.call(notice))
@@ -41,6 +43,7 @@ module Chronos
41
43
  return false unless capture_enabled?
42
44
 
43
45
  notice = build_notice(exception, context)
46
+ return false if ignored?(notice)
44
47
  return false unless @delivery_pipeline.capture_allowed?("exception", notice.fingerprint)
45
48
 
46
49
  @delivery_pipeline.deliver_sync(@serializer.call(notice))
@@ -59,6 +62,10 @@ module Chronos
59
62
  @notice_builder.call(exception, context)
60
63
  end
61
64
 
65
+ def ignored?(notice)
66
+ @ignore_policy && @ignore_policy.ignored?(notice)
67
+ end
68
+
62
69
  def diagnose(error)
63
70
  @logger.warn("Chronos capture failed: #{error.class}")
64
71
  end
@@ -0,0 +1,54 @@
1
+ module Chronos
2
+ module Application
3
+ # Stores and evaluates bounded application-owned exception ignore rules.
4
+ #
5
+ # @responsibility Decide whether a normalized Notice should be discarded locally.
6
+ # @motivation Expected application failures should not consume queue or network capacity.
7
+ # @limits Rules inspect only the bounded Notice API and cannot be installed remotely.
8
+ # @collaborators CaptureException and application-provided callables.
9
+ # @thread_safety Rule registration and snapshots are protected by a mutex.
10
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
11
+ # @example
12
+ # policy.add { |notice| notice.exception_class == "ExpectedError" }
13
+ # @errors A failing rule is contained, logged, and treated as not matching.
14
+ # @performance The number of evaluated rules is capped by configuration.
15
+ class IgnorePolicy
16
+ def initialize(rules, limit, logger)
17
+ @rules = Array(rules).dup
18
+ @limit = limit
19
+ @logger = logger
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ def add(rule = nil, &block)
24
+ candidate = rule || block
25
+ return false unless candidate.respond_to?(:call)
26
+
27
+ @mutex.synchronize do
28
+ return false if @rules.length >= @limit
29
+
30
+ @rules << candidate
31
+ end
32
+ true
33
+ rescue StandardError
34
+ false
35
+ end
36
+
37
+ def ignored?(notice)
38
+ rules = @mutex.synchronize { @rules.dup }
39
+ rules.any? { |rule| safely_matches?(rule, notice) }
40
+ rescue StandardError
41
+ false
42
+ end
43
+
44
+ private
45
+
46
+ def safely_matches?(rule, notice)
47
+ rule.call(notice) == true
48
+ rescue StandardError => error
49
+ @logger.warn("Chronos ignore rule failed: #{error.class}")
50
+ false
51
+ end
52
+ end
53
+ end
54
+ end
@@ -9,7 +9,7 @@ module Chronos
9
9
  # @thread_safety Validation reads one configuration instance without shared state.
10
10
  # @compatibility Ruby 2.2.10 through Ruby 2.6.
11
11
  # @errors Invalid values become messages; malformed URLs are contained.
12
- module ConfigurationValidation
12
+ module ConfigurationValidation # rubocop:disable Metrics/ModuleLength
13
13
  private
14
14
 
15
15
  def resilience_errors
@@ -60,6 +60,7 @@ module Chronos
60
60
  errors.concat(matcher_errors("allowlist_keys", allowlist_keys))
61
61
  errors.concat(matcher_errors("hash_keys", hash_keys))
62
62
  errors.concat(filter_errors)
63
+ errors.concat(ignore_rule_errors)
63
64
  errors.concat(anonymization_errors)
64
65
  errors
65
66
  end
@@ -99,6 +100,20 @@ module Chronos
99
100
  [true, false].include?(anonymize_ip) ? [] : ["anonymize_ip must be true or false"]
100
101
  end
101
102
 
103
+ def ignore_rule_errors
104
+ errors = []
105
+ unless ignore_rules.is_a?(Array) && ignore_rules.all? { |rule| rule.respond_to?(:call) }
106
+ errors << "ignore_rules must contain only callable objects"
107
+ end
108
+ unless positive_integer?(max_ignore_rules) && max_ignore_rules <= 100
109
+ errors << "max_ignore_rules must be an integer between 1 and 100"
110
+ end
111
+ if ignore_rules.is_a?(Array) && max_ignore_rules.is_a?(Integer) && ignore_rules.length > max_ignore_rules
112
+ errors << "ignore_rules cannot exceed max_ignore_rules"
113
+ end
114
+ errors
115
+ end
116
+
102
117
  def matcher_errors(name, values)
103
118
  return [] unless values.is_a?(Array)
104
119
  return [] if values.all? { |value| value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Regexp) }
@@ -37,7 +37,7 @@ module Chronos
37
37
  :queue_size, :workers, :enabled, :error_notifications,
38
38
  :ignored_environments, :proxy, :ssl_verify, :user_agent,
39
39
  :max_payload_size, :gzip, :blocklist_keys, :allowlist_keys,
40
- :filters, :hash_keys, :anonymize_ip, :max_retries,
40
+ :filters, :hash_keys, :anonymize_ip, :ignore_rules, :max_ignore_rules, :max_retries,
41
41
  :retry_base_interval, :retry_max_interval, :retry_jitter,
42
42
  :backlog_size, :circuit_failure_threshold, :circuit_reset_timeout,
43
43
  :remote_configuration, :remote_config_max_bytes, :sampling_rate,
@@ -162,6 +162,8 @@ module Chronos
162
162
  @filters = []
163
163
  @hash_keys = []
164
164
  @anonymize_ip = true
165
+ @ignore_rules = []
166
+ @max_ignore_rules = 20
165
167
  end
166
168
 
167
169
  def initialize_resilience_defaults
@@ -0,0 +1,119 @@
1
+ require "securerandom"
2
+
3
+ module Chronos
4
+ module Integrations
5
+ # Propagates bounded Chronos context through Active Job serialization.
6
+ #
7
+ # @responsibility Add trace/request identifiers to the job envelope and restore them while performing.
8
+ # @motivation Active Job adapters commonly cross process boundaries where thread-local context is lost.
9
+ # @limits It changes only a namespaced serialization field, never public job arguments or adapter behavior.
10
+ # @collaborators ActiveJob::Base serialization hooks and the Chronos facade.
11
+ # @thread_safety Installation is mutex-protected and each job owns its restored context.
12
+ # @compatibility Active Job shipped with Rails 4.2 through Rails 5.2.
13
+ # @example
14
+ # Chronos::Integrations::ActiveJob.install(ActiveJob::Base)
15
+ # @errors Context failures are contained so enqueue and perform semantics remain unchanged.
16
+ # @performance Adds at most two strings of 128 bytes to a serialized job.
17
+ module ActiveJob
18
+ CONTEXT_KEY = "chronos_context".freeze
19
+ SCHEMA_VERSION = "1.0".freeze
20
+ MAX_IDENTIFIER_BYTES = 128
21
+
22
+ @mutex = Mutex.new
23
+
24
+ class << self
25
+ def install(base = nil, notifier = Chronos)
26
+ target = base || (::ActiveJob::Base if defined?(::ActiveJob::Base))
27
+ return false unless target && target.respond_to?(:prepend)
28
+
29
+ @mutex.synchronize do
30
+ return false if target.ancestors.include?(JobExtensions)
31
+
32
+ JobExtensions.notifier = notifier
33
+ target.send(:prepend, JobExtensions)
34
+ end
35
+ true
36
+ rescue StandardError
37
+ false
38
+ end
39
+
40
+ def envelope(notifier)
41
+ source = notifier.respond_to?(:propagation_context) ? notifier.propagation_context : {}
42
+ source = {} unless source.is_a?(Hash)
43
+ context = %w(trace_id request_id).each_with_object({}) do |key, result|
44
+ value = source[key] || source[key.to_sym]
45
+ result[key] = bounded(value) unless value.to_s.empty?
46
+ end
47
+ context["trace_id"] = SecureRandom.uuid if context["trace_id"].to_s.empty?
48
+ {"schema_version" => SCHEMA_VERSION, "context" => context}
49
+ rescue StandardError
50
+ nil
51
+ end
52
+
53
+ def context(value)
54
+ return {} unless value.is_a?(Hash)
55
+ return {} unless (value["schema_version"] || value[:schema_version]).to_s == SCHEMA_VERSION
56
+
57
+ source = value["context"] || value[:context]
58
+ return {} unless source.is_a?(Hash)
59
+
60
+ %w(trace_id request_id).each_with_object({}) do |key, result|
61
+ candidate = source[key] || source[key.to_sym]
62
+ result[key] = bounded(candidate) unless candidate.to_s.empty?
63
+ end
64
+ rescue StandardError
65
+ {}
66
+ end
67
+
68
+ private
69
+
70
+ def bounded(value)
71
+ string = value.to_s
72
+ return string if string.bytesize <= MAX_IDENTIFIER_BYTES
73
+
74
+ string.byteslice(0, MAX_IDENTIFIER_BYTES)
75
+ rescue StandardError
76
+ ""
77
+ end
78
+ end
79
+
80
+ # Active Job instance hooks installed through Module#prepend.
81
+ module JobExtensions
82
+ class << self
83
+ attr_accessor :notifier
84
+ end
85
+
86
+ def serialize(*arguments)
87
+ data = super
88
+ begin
89
+ envelope = Chronos::Integrations::ActiveJob.envelope(JobExtensions.notifier || Chronos)
90
+ data[CONTEXT_KEY] = envelope if data.is_a?(Hash) && envelope
91
+ rescue StandardError
92
+ nil
93
+ end
94
+ data
95
+ end
96
+
97
+ def deserialize(job_data)
98
+ result = super
99
+ begin
100
+ @chronos_context = Chronos::Integrations::ActiveJob.context(
101
+ job_data.is_a?(Hash) ? (job_data[CONTEXT_KEY] || job_data[CONTEXT_KEY.to_sym]) : nil
102
+ )
103
+ rescue StandardError
104
+ @chronos_context = {}
105
+ end
106
+ result
107
+ end
108
+
109
+ def perform_now(*arguments, &block)
110
+ context = @chronos_context || {}
111
+ notifier = JobExtensions.notifier || Chronos
112
+ return super if context.empty? || !notifier.respond_to?(:with_context)
113
+
114
+ notifier.with_context(:context => context) { super }
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -33,6 +33,7 @@ module Chronos
33
33
  return false if self.class.applications[application.object_id]
34
34
 
35
35
  install_middleware(application, options)
36
+ install_active_job
36
37
  @subscriber.install
37
38
  self.class.applications[application.object_id] = true
38
39
  end
@@ -58,6 +59,12 @@ module Chronos
58
59
  true
59
60
  end
60
61
 
62
+ def install_active_job
63
+ return false unless defined?(::ActiveJob::Base)
64
+
65
+ Chronos::Integrations::ActiveJob.install(::ActiveJob::Base, @notifier)
66
+ end
67
+
61
68
  def environment
62
69
  defined?(::Rails) && ::Rails.respond_to?(:env) ? ::Rails.env.to_s : nil
63
70
  end
@@ -144,11 +144,39 @@ module Chronos
144
144
 
145
145
  def active_job(payload, duration)
146
146
  job = value(payload, :job)
147
+ exception = job_exception(payload)
147
148
  data = {
148
149
  "kind" => "active_job", "class" => safe_class_name(job),
149
- "queue" => safe_job_value(job, :queue_name), "duration_ms" => duration
150
+ "adapter" => active_job_adapter(job), "job_id" => safe_job_value(job, :job_id),
151
+ "provider_job_id" => safe_job_value(job, :provider_job_id),
152
+ "queue" => safe_job_value(job, :queue_name), "attempts" => job_attempts(job),
153
+ "duration_ms" => duration, "status" => exception ? "failed" : "completed"
150
154
  }
155
+ data["error_class"] = exception.class.name.to_s if exception
151
156
  @notifier.record_event("job", data)
157
+ @notifier.notify_once(exception, :context => {"job" => data}) if exception
158
+ end
159
+
160
+ def job_exception(payload)
161
+ exception = value(payload, :exception_object)
162
+ details = value(payload, :exception)
163
+ exception ||= RuntimeError.new(Array(details).last.to_s) if details
164
+ exception
165
+ end
166
+
167
+ def active_job_adapter(job)
168
+ adapter = job.respond_to?(:queue_adapter) ? job.queue_adapter : nil
169
+ adapter = adapter.class if adapter && !adapter.is_a?(Class)
170
+ adapter ? adapter.name.to_s.split("::").last.to_s.sub(/Adapter$/, "") : ""
171
+ rescue StandardError
172
+ ""
173
+ end
174
+
175
+ def job_attempts(job)
176
+ value = job.respond_to?(:executions) ? job.executions : 0
177
+ value.is_a?(Numeric) ? value.to_i : 0
178
+ rescue StandardError
179
+ 0
152
180
  end
153
181
 
154
182
  def cache(name, payload, duration)
data/lib/chronos/rails.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "chronos"
2
2
  require "chronos/rails/notifications_subscriber"
3
+ require "chronos/integrations/active_job"
3
4
  require "chronos/rails/installer"
4
5
 
5
6
  require "chronos/rails/railtie" if defined?(::Rails::Railtie)
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
2
  # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "0.9.0.pre.1".freeze
3
+ VERSION = "0.9.0.pre.2".freeze
4
4
  end
data/lib/chronos.rb CHANGED
@@ -33,6 +33,7 @@ require "chronos/core/breadcrumb"
33
33
  require "chronos/application/retry_policy"
34
34
  require "chronos/application/circuit_breaker"
35
35
  require "chronos/application/remote_configuration"
36
+ require "chronos/application/ignore_policy"
36
37
  require "chronos/application/delivery_pipeline"
37
38
  require "chronos/application/capture_exception"
38
39
  require "chronos/application/apm_error_classifier"
@@ -60,7 +61,7 @@ require "chronos/integrations/rack/middleware"
60
61
  # config.host = "https://chronos.example.com"
61
62
  # end
62
63
  # Chronos.notify(RuntimeError.new("failed"))
63
- module Chronos
64
+ module Chronos # rubocop:disable Metrics/ModuleLength
64
65
  extend ObservabilityFacade
65
66
 
66
67
  @mutex = Mutex.new
@@ -144,6 +145,13 @@ module Chronos
144
145
  false
145
146
  end
146
147
 
148
+ def ignore_if(&block)
149
+ agent = current_agent
150
+ agent ? agent.ignore_if(&block) : false
151
+ rescue StandardError
152
+ false
153
+ end
154
+
147
155
  def rails_integration_options(environment = nil, console = false)
148
156
  agent = current_agent
149
157
  agent ? agent.rails_integration_options(environment, console) : {:enabled => false}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chronos-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0.pre.1
4
+ version: 0.9.0.pre.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Jefferson
@@ -105,11 +105,15 @@ files:
105
105
  - docs/adr/ADR-015-bounded-apm-aggregation.md
106
106
  - docs/adr/ADR-016-explicit-observability-integrations.md
107
107
  - docs/adr/ADR-017-deploy-events-and-correlation.md
108
+ - docs/adr/ADR-018-pre-1.0-hardening.md
108
109
  - docs/architecture.md
109
110
  - docs/compatibility.md
110
111
  - docs/configuration.md
111
112
  - docs/data-collected.md
113
+ - docs/deprecation-policy.md
112
114
  - docs/examples/plain-ruby.md
115
+ - docs/migration-from-airbrake.md
116
+ - docs/modules/active-job.md
113
117
  - docs/modules/apm-aggregation.md
114
118
  - docs/modules/async-queue.md
115
119
  - docs/modules/cache-observability.md
@@ -117,6 +121,7 @@ files:
117
121
  - docs/modules/dependencies.md
118
122
  - docs/modules/deploy-tracking.md
119
123
  - docs/modules/external-http.md
124
+ - docs/modules/ignore-rules.md
120
125
  - docs/modules/notice-pipeline.md
121
126
  - docs/modules/rack-context.md
122
127
  - docs/modules/rails-legacy.md
@@ -129,6 +134,10 @@ files:
129
134
  - docs/modules/transport.md
130
135
  - docs/performance.md
131
136
  - docs/privacy-lgpd.md
137
+ - docs/protocol-v1.md
138
+ - docs/release-1.0-readiness.md
139
+ - docs/security-review.md
140
+ - docs/semver.md
132
141
  - docs/troubleshooting.md
133
142
  - lib/chronos.rb
134
143
  - lib/chronos/adapters.rb
@@ -143,6 +152,7 @@ files:
143
152
  - lib/chronos/application/circuit_breaker.rb
144
153
  - lib/chronos/application/delivery_pipeline.rb
145
154
  - lib/chronos/application/dependency_reporter.rb
155
+ - lib/chronos/application/ignore_policy.rb
146
156
  - lib/chronos/application/remote_configuration.rb
147
157
  - lib/chronos/application/retry_policy.rb
148
158
  - lib/chronos/capistrano.rb
@@ -169,6 +179,7 @@ files:
169
179
  - lib/chronos/core/telemetry_event.rb
170
180
  - lib/chronos/errors.rb
171
181
  - lib/chronos/integrations.rb
182
+ - lib/chronos/integrations/active_job.rb
172
183
  - lib/chronos/integrations/capistrano.rb
173
184
  - lib/chronos/integrations/job_payload.rb
174
185
  - lib/chronos/integrations/net_http.rb