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

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: 44c7cad424497d7998727de5a916594ecc3ce9dfda7580376fe4a754edd9096e
4
- data.tar.gz: 3f7a64cbf5b5879e107d7102c1f72cdd2ac4c941e1640a05b6210db79cd948f9
3
+ metadata.gz: 34b5d9d1193017931d129dc366922b276d9b85ced0321e78d3f4a5b1a8c13e40
4
+ data.tar.gz: 72277b74f51825118ce6cb2dda83652f035bbfef3cb56c18b7ee98cc3e6d1d54
5
5
  SHA512:
6
- metadata.gz: f62fa57bfcfec70f7172afc4c939d905aa9c7d9215960b73fa0c54406568b43b93e41b02ba22eb13b381243e06cf61a087544db00ed97ea4ce5805f84e9901e7
7
- data.tar.gz: 3a66ec4e2a85f7f2e79868523fe63a493aeb598aff810dfd21b84a25dd0505e21ee66cac35a7113927a9b4b5a5c5ea2f41d25b89b452b38088c0ef3570f876bf
6
+ metadata.gz: 2c6b44a856bbb8975c6a4dc9aba5d9bcd2cf9a9d44456c19fe373860872491ff310f11a2a1b4c7588306ae2795caf4af8cf008e38c10bcdbe0322fca9a803ece
7
+ data.tar.gz: c4ff3f4da09810013c0e4d352bb18cd04c0a652c160816465d4b088490d5eea5f722c326ab8a6dcc9806f7b74c050ee0c77e8c6a4c604ddf650cb08fc66a3fd4
data/CHANGELOG.md CHANGED
@@ -12,6 +12,31 @@ 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.1] - 2026-07-21
16
+
17
+ ### Added
18
+
19
+ - synchronous `Chronos.notify_deploy` API with bounded environment, revision, version, repository, actor, deploy ID, service, region, and instance fields;
20
+ - fixed bounded release/deploy correlation on every new exception and telemetry envelope;
21
+ - explicit configuration for `revision`, `deploy_id`, `region`, and `instance_id`;
22
+ - automatic deploy IDs and credential removal from common HTTP/SCP repository references;
23
+ - optional idempotent Capistrano post-publish task loaded through `chronos/capistrano`;
24
+ - manual, Kamal-command, and GitHub Actions deployment examples;
25
+ - deploy/correlation contracts, tests, benchmark, module documentation, and ADR-017.
26
+
27
+ ### Changed
28
+
29
+ - dependency inventory can be refreshed once after a successful deploy notification with the new release;
30
+ - deploy events use their normalized environment/service/release values in the common envelope;
31
+ - version advanced to `0.9.0.pre.1`.
32
+
33
+ ### Known limitations
34
+
35
+ - `notify_deploy` does not mutate the immutable correlation of an already running agent;
36
+ - Kamal support is command/documentation based rather than a plugin;
37
+ - Capistrano must load its task DSL before the optional Chronos entry point;
38
+ - deployment policy must decide whether a `false` notification result blocks publication.
39
+
15
40
  ## [0.8.0.pre.1] - 2026-07-20
16
41
 
17
42
  ### Added
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Chronos Ruby
2
2
 
3
- Chronos Ruby is the framework-independent client for sending Ruby application errors and bounded telemetry to Chronos. Version 0.8 adds optional per-instance outbound `Net::HTTP` timing, privacy-safe cache identity, and one bounded dependency inventory per agent.
3
+ Chronos Ruby is the framework-independent client for sending Ruby application errors and bounded telemetry to Chronos. Version 0.9 adds synchronous deploy tracking and bounded release correlation across every event.
4
4
 
5
5
  ## What the gem collects
6
6
 
7
- Version 0.8 can collect:
7
+ Version 0.9 can collect:
8
8
 
9
9
  - exception class, message, structured backtrace, and chained causes;
10
10
  - timestamp, severity, tags, and an optional fingerprint;
@@ -13,6 +13,8 @@ Version 0.8 can collect:
13
13
  - application version, environment, and service name.
14
14
  - Rack method, normalized route, status, duration, request ID, host, query-free path, optional user agent, controller/action, response size, trace ID, and already-parsed parameters when the middleware is used;
15
15
  - bounded breadcrumbs explicitly supplied by the application or integration.
16
+ - release, revision, deploy ID, environment, service, region, and instance correlation;
17
+ - explicit bounded deployment metadata supplied through the public API or integration.
16
18
 
17
19
  See [Data collected](docs/data-collected.md) for the complete field table.
18
20
 
@@ -31,7 +33,7 @@ See [Compatibility](docs/compatibility.md).
31
33
  The current public build is a pre-release. Add its exact version to the application's `Gemfile`:
32
34
 
33
35
  ```ruby
34
- gem "chronos-ruby", "0.8.0.pre.1"
36
+ gem "chronos-ruby", "0.9.0.pre.1"
35
37
  ```
36
38
 
37
39
  Install with a Bundler version compatible with the application. For the oldest supported runtime:
@@ -52,7 +54,7 @@ gem install chronos-ruby --pre
52
54
  Version 0.5 exposes Rails support explicitly, keeping Rails and ActiveSupport out of plain Ruby applications:
53
55
 
54
56
  ```ruby
55
- gem "chronos-ruby", "0.8.0.pre.1", :require => "chronos/rails"
57
+ gem "chronos-ruby", "0.9.0.pre.1", :require => "chronos/rails"
56
58
  ```
57
59
 
58
60
  Generate the initializer with:
@@ -191,7 +193,7 @@ Version `0.6.0.pre.1` adds optional Sidekiq 4/5 middleware:
191
193
 
192
194
  ```ruby
193
195
  gem "sidekiq", "~> 5.0"
194
- gem "chronos-ruby", "0.8.0.pre.1", :require => "chronos/sidekiq"
196
+ gem "chronos-ruby", "0.9.0.pre.1", :require => "chronos/sidekiq"
195
197
  ```
196
198
 
197
199
  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).
@@ -220,7 +222,21 @@ Rails cache telemetry omits raw keys by default. Set `cache_key_mode = :sha256`
220
222
 
221
223
  ## Deploy tracking
222
224
 
223
- Deploy notifications are not implemented in version 0.8. `app_version` is included in event service metadata and the once-per-agent dependency inventory for release correlation.
225
+ Version 0.9 sends deployment metadata synchronously and adds a bounded correlation block to every event:
226
+
227
+ ```ruby
228
+ Chronos.notify_deploy(
229
+ :environment => "production",
230
+ :revision => ENV["GIT_SHA"],
231
+ :version => ENV["APP_VERSION"],
232
+ :repository => "owner/repository",
233
+ :actor => ENV["DEPLOY_USER"]
234
+ )
235
+ ```
236
+
237
+ Configure `app_version`, `revision`, `deploy_id`, `environment`, `service_name`, `region`, and `instance_id` in each newly deployed process so subsequent telemetry carries the same release identity. The gem never scans environment variables or Git automatically.
238
+
239
+ Optional Capistrano support loads through `chronos/capistrano`. Manual, Kamal-command, and GitHub Actions examples share the explicit deploy command under `examples/deploy/`. See [Deploy tracking and release correlation](docs/modules/deploy-tracking.md).
224
240
 
225
241
  ## Asynchronous queue
226
242
 
@@ -296,6 +312,11 @@ Chronos.configure do |config|
296
312
  config.external_http_enabled = false
297
313
  config.cache_key_mode = :none
298
314
  config.dependency_reporting = true
315
+ config.app_version = ENV["APP_VERSION"]
316
+ config.revision = ENV["GIT_SHA"]
317
+ config.deploy_id = ENV["DEPLOY_ID"]
318
+ config.region = ENV["REGION"]
319
+ config.instance_id = ENV["INSTANCE_ID"]
299
320
  end
300
321
  ```
301
322
 
@@ -307,7 +328,7 @@ Configuration errors are raised during `Chronos.configure`. Capture and delivery
307
328
 
308
329
  ## Benchmark
309
330
 
310
- Run the version 0.8 benchmarks with:
331
+ Run the version 0.9 benchmarks with:
311
332
 
312
333
  ```bash
313
334
  bundle _1.17.3_ exec ruby benchmarks/capture_exception.rb
@@ -320,13 +341,14 @@ bundle _1.17.3_ exec ruby benchmarks/rails_notifications.rb
320
341
  bundle _1.17.3_ exec ruby benchmarks/sidekiq_middleware.rb
321
342
  bundle _1.17.3_ exec ruby benchmarks/apm_aggregation.rb
322
343
  bundle _1.17.3_ exec ruby benchmarks/external_http.rb
344
+ bundle _1.17.3_ exec ruby benchmarks/correlation.rb
323
345
  ```
324
346
 
325
347
  Results depend on runtime, hardware, and payload. No performance comparison is claimed until repeatable measurements are published.
326
348
 
327
349
  ## Migration from Airbrake
328
350
 
329
- An Airbrake migration guide will be added before the legacy 1.0 release. Version 0.8 does not claim API compatibility or automatic replacement.
351
+ An Airbrake migration guide will be added before the legacy 1.0 release. Version 0.9 does not claim API compatibility or automatic replacement.
330
352
 
331
353
  ## Local development
332
354
 
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://chronos.example/schemas/deploy-v1.schema.json",
4
+ "title": "Chronos deploy payload v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "deploy_id", "environment", "revision", "version", "repository",
9
+ "actor", "service", "region", "instance"
10
+ ],
11
+ "properties": {
12
+ "deploy_id": {"type": "string", "minLength": 1, "maxLength": 128},
13
+ "environment": {"type": "string", "minLength": 1, "maxLength": 128},
14
+ "revision": {"type": ["string", "null"], "maxLength": 128},
15
+ "version": {"type": ["string", "null"], "maxLength": 128},
16
+ "repository": {"type": ["string", "null"], "maxLength": 512},
17
+ "actor": {"type": ["string", "null"], "maxLength": 128},
18
+ "service": {"type": ["string", "null"], "maxLength": 128},
19
+ "region": {"type": ["string", "null"], "maxLength": 128},
20
+ "instance": {"type": ["string", "null"], "maxLength": 128}
21
+ },
22
+ "anyOf": [
23
+ {"properties": {"revision": {"type": "string", "minLength": 1}}},
24
+ {"properties": {"version": {"type": "string", "minLength": 1}}}
25
+ ]
26
+ }
@@ -20,7 +20,7 @@
20
20
  "properties": {
21
21
  "schema_version": {"const": "1.0"},
22
22
  "event_id": {"type": "string", "minLength": 1},
23
- "event_type": {"enum": ["exception", "request", "query", "job", "cache", "external_http", "dependencies", "metric_batch"]},
23
+ "event_type": {"enum": ["exception", "request", "query", "job", "cache", "external_http", "dependencies", "deploy", "metric_batch"]},
24
24
  "occurred_at": {"type": "string"},
25
25
  "sent_at": {"type": "string"},
26
26
  "project_key": {"type": "string", "minLength": 1},
@@ -34,6 +34,20 @@
34
34
  "instance_id": {"type": ["string", "null"]}
35
35
  }
36
36
  },
37
+ "correlation": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["release", "revision", "deploy_id", "environment", "service", "region", "instance"],
41
+ "properties": {
42
+ "release": {"type": ["string", "null"], "maxLength": 128},
43
+ "revision": {"type": ["string", "null"], "maxLength": 128},
44
+ "deploy_id": {"type": ["string", "null"], "maxLength": 128},
45
+ "environment": {"type": ["string", "null"], "maxLength": 128},
46
+ "service": {"type": ["string", "null"], "maxLength": 128},
47
+ "region": {"type": ["string", "null"], "maxLength": 128},
48
+ "instance": {"type": ["string", "null"], "maxLength": 128}
49
+ }
50
+ },
37
51
  "runtime": {"type": "object"},
38
52
  "context": {"type": "object"},
39
53
  "payload": {"type": "object"}
@@ -0,0 +1,25 @@
1
+ # ADR-017 — Synchronous deploy events and immutable release correlation
2
+
3
+ ## Status
4
+
5
+ Accepted for version 0.9.
6
+
7
+ ## Context
8
+
9
+ The SaaS needs stable release dimensions to compare error and performance behavior before and after deployments. Deployment tools are short-lived processes, while application telemetry is long-lived and concurrent. Implicit Git, lockfile, or environment discovery would make behavior surprising and could expose credentials.
10
+
11
+ ## Decision
12
+
13
+ Add a versioned `deploy` telemetry payload delivered synchronously through the existing transport policy. Require an environment and either revision or version, generate a deploy ID when omitted, bound every field, and remove credentials from common repository URL forms.
14
+
15
+ Emit a `correlation` object on every new v1 envelope with release, revision, deploy ID, environment, service, region, and instance. Keep the schema property optional so payloads from earlier 0.x clients remain valid. Populate it from the immutable configuration snapshot for application events and from normalized deploy fields for deploy events. Do not mutate a running agent's correlation after notification.
16
+
17
+ Refresh the bounded dependency inventory after a successful deploy notification. Integrate Capistrano through its task DSL without a runtime dependency. Support Kamal by documented command and GitHub Actions through an example workflow.
18
+
19
+ ## Alternatives
20
+
21
+ Reading Git state, all environment variables, or deployment manifests automatically was rejected for determinism and privacy. Asynchronous-only deploy delivery was rejected because short-lived commands can exit before queue drain. Mutating global release state after `notify_deploy` was rejected because concurrent events could receive mixed correlation. Adding direct dependencies on Capistrano, Kamal, or GitHub SDKs was rejected for the legacy compatibility line.
22
+
23
+ ## Consequences
24
+
25
+ Every event has a stable bounded correlation shape and deploy commands receive an explicit success result. Applications must pass release metadata into each newly started process. Integration setup remains explicit, and delivery failure policy stays under the deployment pipeline's control.
data/docs/architecture.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Architecture
2
2
 
3
- Chronos Ruby 0.8 uses hexagonal boundaries so the legacy core remains independent of frameworks and delivery infrastructure.
3
+ Chronos Ruby 0.9 uses hexagonal boundaries so the legacy core remains independent of frameworks and delivery infrastructure.
4
4
 
5
5
  ```mermaid
6
6
  flowchart TB
@@ -24,6 +24,8 @@ flowchart TB
24
24
  NetHTTP[Explicit Net::HTTP instance wrapper] --> Facade
25
25
  Rails --> Cache[Core / CacheNormalizer]
26
26
  Application --> Dependencies[Application / DependencyReporter]
27
+ Deploy[Manual, Capistrano, Kamal command, GitHub workflow] --> Facade
28
+ Core --> Correlation[Core / DeployNormalizer and CorrelationContext]
27
29
  ```
28
30
 
29
31
  ## Boundaries
@@ -55,6 +57,10 @@ Version 0.7 routes request, query, and job observations through `ApmAggregator`.
55
57
 
56
58
  Version 0.8 prepends a wrapper only to each explicitly selected `Net::HTTP` object. The wrapper records bounded outcome metadata and feeds the existing APM aggregator; it does not modify `Net::HTTP` globally. `CacheNormalizer` turns public Rails cache notifications into bounded metadata before delivery. `DependencyReporter` reads already loaded runtime metadata once per agent under a mutex and queues it as a separate event.
57
59
 
60
+ Version 0.9 routes explicit deploy attributes through `DeployNormalizer`, synchronous `CaptureTelemetry`, the common sanitizer, and `DeliveryPipeline`. `CorrelationContext` supplies the same fixed seven-field shape to exception and telemetry serializers. Deploy event overrides apply only while serializing that event; the agent configuration remains immutable for concurrent application capture. A successful deployment resets the dependency reporter once and flushes the refreshed inventory.
61
+
62
+ Capistrano remains an optional integration task with no runtime dependency. Kamal and GitHub Actions call the same manual Ruby command rather than adding SDKs or transport paths.
63
+
58
64
  ## Failure policy
59
65
 
60
66
  Explicit invalid configuration raises `Chronos::ConfigurationError`. Capture, serialization, logger, worker, retry, circuit, TLS, network, HTTP, and remote-policy failures do not escape into the host application. They produce `false`, a classified transport result, a bounded state transition, or a bounded logger diagnostic.
@@ -23,6 +23,8 @@ Version `0.7.0.pre.1` keeps the same Ruby/Rails matrix and implements APM aggreg
23
23
 
24
24
  Version `0.8.0.pre.1` uses per-object `Module#prepend`, legacy `Net::HTTP`, standard-library SHA-256, and loaded-spec feature detection. It adds no runtime dependency and keeps the same matrix. Outbound HTTP, cache, and dependency gates must pass every listed runtime before support is promoted.
25
25
 
26
+ Version `0.9.0.pre.1` adds only standard-library URI/SecureRandom processing, bounded hashes, and the existing synchronous delivery path. Capistrano is optional and feature-detected; Kamal and GitHub Actions integrations are commands/examples. The Ruby/Rails matrix remains unchanged and `Best effort` until deploy/correlation payload gates pass every listed runtime.
27
+
26
28
  Status meanings:
27
29
 
28
30
  - Supported: the complete required compatibility gate passes.
@@ -8,8 +8,12 @@
8
8
  | `project_key` | Yes when enabled | `nil` | Secret authentication key sent only as an HTTP header |
9
9
  | `host` | Yes when enabled | `nil` | Absolute Chronos HTTP endpoint; HTTPS is required by default |
10
10
  | `environment` | Recommended | `production` | Application environment |
11
- | `app_version` | Optional | `nil` | Release or revision identifier |
11
+ | `app_version` | Optional | `nil` | Application release/version identifier |
12
12
  | `service_name` | Recommended | `nil` | Logical service name |
13
+ | `revision` | Optional | `nil` | Source revision correlated with every event; maximum 128 bytes |
14
+ | `deploy_id` | Optional | `nil` | Deployment identifier correlated with every event; maximum 128 bytes |
15
+ | `region` | Optional | `nil` | Deployment region correlated with every event; maximum 128 bytes |
16
+ | `instance_id` | Optional | `nil` | Explicit instance correlation; otherwise runtime hostname is used |
13
17
  | `root_directory` | Optional | `Dir.pwd` | Used to identify application backtrace frames |
14
18
  | `logger` | Optional | `nil` | Logger receiving bounded internal diagnostics |
15
19
  | `timeout` | Optional | `5.0` | HTTP read timeout in seconds |
@@ -39,7 +43,7 @@
39
43
  | `remote_configuration` | Optional | `true` | Accepts only the documented bounded remote policy fields |
40
44
  | `remote_config_max_bytes` | Optional | `4096` | Maximum remote policy response-header bytes |
41
45
  | `sampling_rate` | Optional | `1.0` | Local upper bound for event sampling |
42
- | `enabled_event_types` | Optional | exception, request, query, job, cache, external_http, dependencies, metric_batch | Local allowlist for supported event envelopes |
46
+ | `enabled_event_types` | Optional | exception, request, query, job, cache, external_http, dependencies, deploy, metric_batch | Local allowlist for supported event envelopes |
43
47
  | `max_remote_send_interval` | Optional | `60.0` | Local upper bound for remotely requested send spacing |
44
48
  | `context_store` | Optional | `:thread_local` | `:thread_local` or an object implementing `get`, `set`, `clear`, and `with_context` |
45
49
  | `breadcrumb_capacity` | Optional | `20` | Positive count of newest breadcrumbs retained per execution |
@@ -70,6 +74,11 @@ Chronos.configure do |config|
70
74
  config.host = ENV["CHRONOS_HOST"]
71
75
  config.environment = ENV["APP_ENV"] || "production"
72
76
  config.service_name = "billing"
77
+ config.app_version = ENV["APP_VERSION"]
78
+ config.revision = ENV["GIT_SHA"]
79
+ config.deploy_id = ENV["DEPLOY_ID"]
80
+ config.region = ENV["REGION"]
81
+ config.instance_id = ENV["INSTANCE_ID"]
73
82
  config.queue_size = 100
74
83
  config.workers = 1
75
84
  config.blocklist_keys += [:medical_record, /bank_account/i]
@@ -102,3 +111,5 @@ Privacy matcher collections and filters are copied into the immutable runtime sn
102
111
  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.
103
112
 
104
113
  Remote configuration can only reduce collection within local bounds. It cannot increase `max_payload_size` or `sampling_rate`, enable an unsupported or locally disabled event type, change the endpoint, replace credentials, disable TLS verification, or install executable matching rules.
114
+
115
+ Release correlation values are copied into the immutable snapshot. Calling `Chronos.notify_deploy` reports a deployment but does not mutate those values for concurrent application events; configure each deployed process with its own release identity.
@@ -1,6 +1,6 @@
1
1
  # Data collected
2
2
 
3
- Version 0.8 emits exceptions, cache telemetry, one dependency inventory, and aggregated request/query/job/external-HTTP metric batches. All fields pass through the privacy sanitizer and bounded safe serializer before queueing, retry storage, or delivery.
3
+ Version 0.9 emits exceptions, cache telemetry, dependency/deploy events, and aggregated request/query/job/external-HTTP metric batches. All fields pass through the privacy sanitizer and bounded safe serializer before queueing, retry storage, or delivery.
4
4
 
5
5
  | Data | Default | Source |
6
6
  |---|---|---|
@@ -45,8 +45,10 @@ Version 0.8 emits exceptions, cache telemetry, one dependency inventory, and agg
45
45
  | Cache key hash | Disabled by default | Project-scoped SHA-256 when `cache_key_mode = :sha256` |
46
46
  | Loaded gem names/versions and Ruby runtime | Once per agent by default; bounded | `Gem.loaded_specs` and Ruby constants |
47
47
  | Rails, web server, database adapter, Sidekiq, release | Included when safely detectable/configured | Loaded constants/specs and `app_version` |
48
+ | Event release, revision, deploy ID, environment, service, region, instance | Present as bounded correlation; values optional | Explicit immutable configuration or normalized deploy payload |
49
+ | Deploy environment, revision, version, repository, actor, deploy ID, service, region, instance | Explicit synchronous deploy API | Application/deployment integration arguments |
48
50
 
49
- The gem never collects request bodies, response bodies, raw query strings, cookies, HTTP authorization headers, environment variables in bulk, source code, raw SQL, SQL bind values, database rows, raw cache keys/values, mail bodies/recipients, gem paths, or lockfile contents. Sidekiq JIDs/arguments and bounded loaded gem names/versions are documented integration fields.
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.
50
52
 
51
53
  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.
52
54
 
@@ -29,4 +29,10 @@ For network-free per-instance outbound HTTP instrumentation, run:
29
29
  bundle _1.17.3_ exec ruby examples/plain-ruby/external_http.rb
30
30
  ```
31
31
 
32
- The examples demonstrate final event construction, key redaction, e-mail and Bearer-token detection, application-specific blocklisting, identifier hashing, and the bounded external HTTP payload without opening a socket.
32
+ For a network-free deploy event with complete release correlation, run:
33
+
34
+ ```bash
35
+ bundle _1.17.3_ exec ruby examples/plain-ruby/deploy_tracking.rb
36
+ ```
37
+
38
+ The examples demonstrate final event construction, privacy filtering, bounded external HTTP fields, and deploy correlation without opening a socket.
@@ -11,3 +11,5 @@ Tests in `spec/unit/configuration_spec.rb` verify required fields, HTTPS default
11
11
  Version 0.7 adds bounded APM capacities, thresholds, and histogram boundaries. Invalid zero/negative capacities, batches above 50, N+1 thresholds below two, and non-increasing histogram boundaries are rejected before agent construction. See [Essential APM aggregation](apm-aggregation.md).
12
12
 
13
13
  Version 0.8 adds Boolean HTTP/dependency switches, `:none`/`:sha256` cache-key policy, and a dependency limit from 1 to 200. Invalid values are rejected before instrumentation or collection starts. See [External HTTP](external-http.md), [Cache observability](cache-observability.md), and [Dependency inventory](dependencies.md).
14
+
15
+ Version 0.9 adds optional `revision`, `deploy_id`, `region`, and `instance_id` strings, each limited to 128 bytes. Together with the existing release/environment/service options they form immutable event correlation. See [Deploy tracking and release correlation](deploy-tracking.md).
@@ -1,6 +1,6 @@
1
1
  # Dependency inventory
2
2
 
3
- Version `0.8.0.pre.1` emits a separate `dependencies` event at most once per configured agent. It is queued before the first capture or during explicit flush/close and is never attached to each exception.
3
+ Version `0.9.0.pre.1` emits a separate `dependencies` event at most once per configured agent and once again after a successful deploy notification. It is queued before the first capture or during explicit flush/close and is never attached to each exception.
4
4
 
5
5
  The inventory contains Ruby version/engine/platform, bounded names and versions from already loaded gem specs, the configured `app_version` release identifier, and Rails, Sidekiq, web server, or database adapter labels when safely detectable. The agent does not parse `Gemfile.lock`, activate missing gems, inspect gem paths/source, scan environment variables, open a database connection, or collect dependency configuration.
6
6
 
@@ -15,4 +15,4 @@ end
15
15
  Chronos.report_dependencies
16
16
  ```
17
17
 
18
- `dependency_max_items` accepts 1–200 and defaults to 100. Entries are sorted by name before truncation so repeated boots are stable. Set `dependency_reporting = false` when inventory collection is unnecessary or prohibited. Because collection is once-only, gems loaded after the first event appear only after a new agent configuration or process boot.
18
+ `dependency_max_items` accepts 1–200 and defaults to 100. Entries are sorted by name after bounded selection. Set `dependency_reporting = false` when inventory collection is unnecessary or prohibited. A successful `notify_deploy` resets the once-only guard and uses the deploy version for the refreshed inventory; otherwise gems loaded after the first event appear only after a new agent configuration or process boot.
@@ -0,0 +1,86 @@
1
+ # Deploy tracking and release correlation
2
+
3
+ Version `0.9.0.pre.1` adds synchronous deploy notifications and a bounded correlation block to every exception and telemetry envelope. The SaaS can compare errors and performance before and after a release using release, revision, deploy ID, environment, service, region, and instance.
4
+
5
+ ## Public API
6
+
7
+ Configure the agent, then provide deploy metadata explicitly:
8
+
9
+ ```ruby
10
+ Chronos.notify_deploy(
11
+ :environment => "production",
12
+ :revision => ENV["GIT_SHA"],
13
+ :version => ENV["APP_VERSION"],
14
+ :repository => "owner/repository",
15
+ :actor => ENV["DEPLOY_USER"],
16
+ :deploy_id => ENV["DEPLOY_ID"],
17
+ :service => "billing",
18
+ :region => "sa-east-1",
19
+ :instance => "web-1"
20
+ )
21
+ ```
22
+
23
+ Environment is required, and either revision or version must be present. A UUID deploy ID is generated when none is supplied. The call uses synchronous delivery, bypasses ordinary event sampling, then refreshes the bounded dependency inventory for the new release and flushes it before returning. Local/remote event disabling and the kill switch still apply. It returns `false` instead of leaking configuration, normalization, serialization, or transport failures.
24
+
25
+ Fields are limited to 128 bytes, except repository at 512 bytes. HTTP and SCP-style repository credentials are stripped before serialization. The shared sanitizer still filters explicit actor/repository content. Do not send access tokens, e-mail addresses, or unnecessary personal data.
26
+
27
+ ## Correlation on application events
28
+
29
+ Set the values that identify the currently running release during normal application configuration:
30
+
31
+ ```ruby
32
+ Chronos.configure do |config|
33
+ # project and endpoint settings omitted
34
+ config.app_version = ENV["APP_VERSION"]
35
+ config.revision = ENV["GIT_SHA"]
36
+ config.deploy_id = ENV["DEPLOY_ID"]
37
+ config.environment = ENV["APP_ENV"] || "production"
38
+ config.service_name = "billing"
39
+ config.region = ENV["REGION"]
40
+ config.instance_id = ENV["INSTANCE_ID"]
41
+ end
42
+ ```
43
+
44
+ The gem never reads these variables itself. The host application chooses each source. Configuration snapshots are immutable, so `notify_deploy` does not mutate correlation for an already running process; newly deployed processes must start with their own release values.
45
+
46
+ ## Capistrano
47
+
48
+ Require the optional entry point after Capistrano loads:
49
+
50
+ ```ruby
51
+ require "chronos/capistrano"
52
+
53
+ set :chronos_version, ENV["APP_VERSION"]
54
+ set :chronos_actor, ENV["DEPLOY_USER"]
55
+ set :chronos_deploy_id, ENV["DEPLOY_ID"]
56
+ set :chronos_service, "billing"
57
+ set :chronos_region, ENV["DEPLOY_REGION"]
58
+ ```
59
+
60
+ It registers `chronos:notify_deploy` after `deploy:published`, once per DSL object. Revision, stage, and repository use the public Capistrano variables `current_revision`, `stage`, and `repo_url`; all Chronos-specific values are optional and explicit. The integration adds no Capistrano runtime dependency.
61
+
62
+ ## Manual, Kamal, and GitHub Actions
63
+
64
+ [`examples/deploy/notify.rb`](../../examples/deploy/notify.rb) is the shared command. It requires project credentials/host and deploy environment, reads only named variables, notifies synchronously, closes the agent, and exits nonzero on failure.
65
+
66
+ For Kamal, pass the variables to the deployed container or command environment and execute after publication:
67
+
68
+ ```bash
69
+ kamal app exec --reuse "bundle exec ruby examples/deploy/notify.rb"
70
+ ```
71
+
72
+ Adapt the command to the application's Kamal hook lifecycle and secret-management policy. The repository also provides a non-active [GitHub Actions workflow example](../../examples/deploy/github-actions.yml) using read-only repository permission and secret-scoped Chronos credentials.
73
+
74
+ For a network-free payload demonstration:
75
+
76
+ ```bash
77
+ bundle _1.17.3_ exec ruby examples/plain-ruby/deploy_tracking.rb
78
+ ```
79
+
80
+ ## Limitations
81
+
82
+ - delivery is process-local and uses the existing finite retry/circuit/backlog policy;
83
+ - an unsuccessful synchronous delivery returns `false`; deployment policy decides whether that blocks publication;
84
+ - Capistrano auto-registration requires its task DSL to be loaded first;
85
+ - Kamal integration is command/documentation based, not a Kamal plugin;
86
+ - repository and actor are explicit metadata and remain subject to application privacy review.
@@ -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.8.0.pre.1", :require => "chronos/sidekiq"
7
+ gem "chronos-ruby", "0.9.0.pre.1", :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.
@@ -1,9 +1,11 @@
1
1
  # Framework telemetry events
2
2
 
3
- `Chronos::Core::TelemetryEvent` and `Chronos::Core::TelemetrySerializer` extend the v1 envelope to `request`, `query`, `job`, `cache`, `external_http`, `dependencies`, and `metric_batch` events. These values exist so integrations do not model operational telemetry as fake exceptions.
3
+ `Chronos::Core::TelemetryEvent` and `Chronos::Core::TelemetrySerializer` extend the v1 envelope to `request`, `query`, `job`, `cache`, `external_http`, `dependencies`, `deploy`, and `metric_batch` events. These values exist so integrations do not model operational telemetry as fake exceptions.
4
4
 
5
5
  Telemetry has the same `schema_version`, event ID, timestamps, project, environment, service, runtime, context, sanitization, payload-size limit, idempotency header, asynchronous queue, retry, circuit breaker, and memory backlog used by exception events. Unsupported types are rejected locally.
6
6
 
7
7
  `Chronos.record_event` is the narrow integration entry point. Application code should prefer documented higher-level APIs; its payload is allowlisted by each bundled integration and then sanitized. Remote configuration can reduce or disable any locally enabled telemetry type but cannot enable a type excluded by local configuration.
8
8
 
9
9
  Version 0.8 aggregates request, query, job, and enabled external HTTP observations into `metric_batch` events by default. Cache remains an individual event while contributing to an existing traced request breakdown. Dependencies use one separate event per agent. Set `apm_enabled = false` only when individual legacy telemetry is required for diagnostics. Percentiles remain server-side; see [Essential APM aggregation](apm-aggregation.md).
10
+
11
+ Version 0.9 delivers deploy telemetry synchronously because deployment commands are short-lived. Every telemetry and exception envelope contains the fixed correlation fields documented in [Deploy tracking and release correlation](deploy-tracking.md).
data/docs/performance.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Performance
2
2
 
3
- Performance is a functional requirement, but version 0.8 makes no unverified speed claim.
3
+ Performance is a functional requirement, but version 0.9 makes no unverified speed claim.
4
4
 
5
5
  Current controls:
6
6
 
@@ -24,6 +24,8 @@ Current controls:
24
24
  - outbound HTTP instrumentation uses two clock reads and bounded metadata without body/header traversal;
25
25
  - cache normalization is bounded and SHA-256 runs only when explicitly enabled;
26
26
  - dependency inventory runs at most once per agent and is capped at 200 loaded specs.
27
+ - event correlation copies exactly seven strings with a 128-byte bound each;
28
+ - deploy notification uses the existing synchronous delivery path and creates no timer or worker type.
27
29
 
28
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.
29
31
 
@@ -115,3 +117,15 @@ ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/external_http.rb
115
117
  The fixture compares a no-op Net::HTTP-compatible object with the per-instance wrapper. It includes trace-header injection, bounded outcome capture, and clock reads, but excludes DNS, sockets, TLS, serialization, queueing, and network delivery. Record a controlled result before making a performance claim.
116
118
 
117
119
  A local diagnostic run on 2026-07-20 used Ruby 2.2.10 on macOS arm64 and 10,000 calls. It measured approximately 15.155 microseconds of wrapper overhead per call. This single run has no controlled warmup, median, or dispersion and is not a production performance claim.
120
+
121
+ ## Version 0.9 correlation benchmark
122
+
123
+ Run:
124
+
125
+ ```bash
126
+ ITERATIONS=100000 bundle _1.17.3_ exec ruby benchmarks/correlation.rb
127
+ ```
128
+
129
+ The fixture builds the seven-field frozen correlation hash from an immutable configuration. It excludes notice/event construction, sanitization, JSON, queueing, retry, and network delivery. Record a controlled result before making a performance claim.
130
+
131
+ A local diagnostic run on 2026-07-21 used Ruby 2.2.10 on macOS arm64 and 10,000 correlations. It measured approximately 20.490 microseconds per correlation. This single run has no controlled warmup, median, or dispersion and is not a production performance claim.
data/docs/privacy-lgpd.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Privacy and LGPD
2
2
 
3
- Version 0.8 sanitizes exception, framework telemetry, dependency inventory, and APM metric batches before JSON serialization, queueing, retry backlog, or transport. This reduces accidental exposure, but the host application remains responsible for lawful purpose, minimization, access control, retention, and responses to data-subject requests.
3
+ Version 0.9 sanitizes exception, framework telemetry, dependency/deploy inventory, correlation, and APM metric batches before JSON serialization, queueing, retry backlog, or transport. This reduces accidental exposure, but the host application remains responsible for lawful purpose, minimization, access control, retention, and responses to data-subject requests.
4
4
 
5
5
  ## Default policy
6
6
 
@@ -19,8 +19,17 @@ Version 0.8 sanitizes exception, framework telemetry, dependency inventory, and
19
19
  | External HTTP | Host/method/status/timing only; URL path/query, Authorization, bodies, headers, and error messages omitted |
20
20
  | Cache key | Omitted by default; optional project-scoped SHA-256 hash; cache value never read |
21
21
  | Dependencies | Bounded loaded gem names/versions and detected runtime labels; paths and lockfiles omitted |
22
+ | Deploy correlation | Explicit bounded operational identifiers; no automatic Git or environment scan |
23
+ | Repository | Credentials removed from common HTTP/SCP forms; query/fragment omitted for parsed URLs |
24
+ | Actor | Explicit and sanitized; omit it when personal identity is unnecessary |
22
25
 
23
- The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.8 does not persist telemetry to disk.
26
+ The retry backlog exists only in process memory, accepts only `SerializedEvent`, has a fixed capacity, and disappears on process exit. Version 0.9 does not persist telemetry to disk.
27
+
28
+ ## Deploy metadata
29
+
30
+ Release, revision, deploy ID, environment, service, region, and instance are operational identifiers but can become personal or sensitive when applications encode customer, tenant, employee, or infrastructure-secret values in them. Use opaque bounded identifiers. Repository should be an owner/name identifier rather than a credential-bearing clone URL. Actor is optional; prefer a service account label when individual identity is unnecessary.
31
+
32
+ The deploy normalizer removes user information from standard HTTP(S) repository URLs and usernames from SCP-style Git references. This is defensive, not a substitute for secret management. Never pass access tokens in any deploy field. All values still traverse the common sanitizer before transport.
24
33
 
25
34
  ## External services, cache, and dependencies
26
35
 
@@ -36,6 +36,18 @@ Set `external_http_trace_headers = false` before configuring the agent, or do no
36
36
 
37
37
  The event is emitted once per agent and contains only gems loaded at collection time, capped by `dependency_max_items`. It does not parse the lockfile or activate optional frameworks. Call `Chronos.report_dependencies` after application boot if the first event can occur before all integrations load, or set `dependency_reporting = false` to disable collection.
38
38
 
39
+ ## `Chronos.notify_deploy` returns false
40
+
41
+ Confirm Chronos is configured/enabled, environment is present, and at least revision or version is non-empty. Check credentials, TLS, proxy, timeout, retry/circuit state, and endpoint response. The call is synchronous and deliberately returns the delivery result. Repository credentials are removed and do not help authenticate with Chronos.
42
+
43
+ ## Events have empty or stale release correlation
44
+
45
+ Set `app_version`, `revision`, `deploy_id`, `environment`, `service_name`, `region`, and `instance_id` before `Chronos.configure` creates its snapshot. `notify_deploy` does not mutate a running agent because that would mix release identities across concurrent events. Restart/configure each deployed process with the new values.
46
+
47
+ ## Capistrano task is missing
48
+
49
+ Require `chronos/capistrano` after the Capistrano/Rake task DSL is loaded. The installer registers once per DSL and hooks `chronos:notify_deploy` after `deploy:published`. Confirm the base Chronos configuration runs in the task process and the expected `stage`, `current_revision`, `repo_url`, and `chronos_*` variables exist.
50
+
39
51
  ## Sidekiq telemetry is missing
40
52
 
41
53
  Require `chronos/sidekiq` after Sidekiq is available and configure Chronos before jobs run. The entry point uses Sidekiq's public client/server middleware configuration and remains optional. Requiring only `chronos` or `chronos/rails` does not load Sidekiq. A job already enqueued before client middleware installation may lack propagated trace context, but server timing and failure capture can still run.
data/lib/chronos/agent.rb CHANGED
@@ -37,7 +37,7 @@ module Chronos
37
37
  pipeline_options
38
38
  )
39
39
  initialize_capture(options)
40
- @dependency_reporter = options[:dependency_reporter] || Application::DependencyReporter.new(config)
40
+ initialize_observability(options)
41
41
  end
42
42
 
43
43
  def notify(exception, context = {})
@@ -67,6 +67,8 @@ module Chronos
67
67
  end
68
68
 
69
69
  def record_event(event_type, payload = {}, context = {})
70
+ return false if event_type.to_s == "deploy"
71
+
70
72
  report_dependencies unless event_type.to_s == "dependencies"
71
73
  @telemetry.call(event_type, payload, telemetry_context(context))
72
74
  end
@@ -81,6 +83,18 @@ module Chronos
81
83
  false
82
84
  end
83
85
 
86
+ def notify_deploy(attributes = {}, timeout = DEFAULT_FLUSH_TIMEOUT)
87
+ payload = @deploy_normalizer.call(attributes)
88
+ return false unless @telemetry.call_sync("deploy", payload, {})
89
+
90
+ @dependency_reporter.reset(payload["version"]) if @dependency_reporter.respond_to?(:reset)
91
+ report_dependencies
92
+ flush(timeout)
93
+ rescue StandardError => error
94
+ @logger.warn("Chronos deploy notification failed: #{error.class}")
95
+ false
96
+ end
97
+
84
98
  def record_event_once(key, event_type, payload = {}, context = {})
85
99
  execution = @context_store.get
86
100
  captured = execution[:__chronos_captured_events] || {}
@@ -176,6 +190,11 @@ module Chronos
176
190
  @telemetry = options[:telemetry] || Application::CaptureTelemetry.new(@config, @delivery_pipeline, @logger)
177
191
  end
178
192
 
193
+ def initialize_observability(options)
194
+ @dependency_reporter = options[:dependency_reporter] || Application::DependencyReporter.new(@config)
195
+ @deploy_normalizer = options[:deploy_normalizer] || Core::DeployNormalizer.new(@config)
196
+ end
197
+
179
198
  def build_context_store(strategy)
180
199
  return Adapters::ThreadLocalContextStore.new if strategy == :thread_local
181
200
 
@@ -40,6 +40,17 @@ module Chronos
40
40
  false
41
41
  end
42
42
 
43
+ def call_sync(event_type, payload = {}, context = {})
44
+ return false unless @config.enabled_for_environment?
45
+ return false unless synchronous_capture_allowed?(event_type)
46
+
47
+ event = Core::TelemetryEvent.new(event_type, payload, context)
48
+ @delivery_pipeline.deliver_sync(@serializer.call(event))
49
+ rescue StandardError => error
50
+ @logger.warn("Chronos synchronous telemetry capture failed: #{error.class}")
51
+ false
52
+ end
53
+
43
54
  def flush
44
55
  enqueue_batches(@aggregator.flush)
45
56
  rescue StandardError => error
@@ -55,6 +66,12 @@ module Chronos
55
66
 
56
67
  private
57
68
 
69
+ def synchronous_capture_allowed?(event_type)
70
+ return @delivery_pipeline.event_enabled?(event_type) if event_type.to_s == "deploy"
71
+
72
+ @delivery_pipeline.capture_allowed?(event_type)
73
+ end
74
+
58
75
  def aggregate_type?(event_type)
59
76
  ApmAggregator::METRIC_TYPES.include?(event_type.to_s)
60
77
  end
@@ -19,6 +19,17 @@ module Chronos
19
19
  @constants = options[:constants] || {}
20
20
  @mutex = Mutex.new
21
21
  @reported = false
22
+ @release_override = nil
23
+ end
24
+
25
+ def reset(release = nil)
26
+ @mutex.synchronize do
27
+ @release_override = bounded(release, 128)
28
+ @reported = false
29
+ end
30
+ true
31
+ rescue StandardError
32
+ false
22
33
  end
23
34
 
24
35
  def call
@@ -45,7 +56,7 @@ module Chronos
45
56
  "web_server" => detected("web_server") { web_server },
46
57
  "database_adapter" => detected("database_adapter") { database_adapter },
47
58
  "sidekiq" => detected("sidekiq") { sidekiq_version },
48
- "release" => bounded(@config.app_version.to_s, 128)
59
+ "release" => @release_override || bounded(@config.app_version.to_s, 128)
49
60
  }
50
61
  payload.delete_if do |key, value|
51
62
  !["dependencies", "ruby"].include?(key) && value.to_s.empty?
@@ -14,7 +14,7 @@ module Chronos
14
14
  # @performance Validation is bounded by configured document and list limits.
15
15
  class RemoteConfiguration
16
16
  SUPPORTED_EVENT_TYPES = %w(
17
- exception request query job cache external_http dependencies metric_batch
17
+ exception request query job cache external_http dependencies deploy metric_batch
18
18
  ).freeze
19
19
  MAX_IGNORED_FINGERPRINTS = 100
20
20
  MAX_FINGERPRINT_BYTES = 256
@@ -0,0 +1,4 @@
1
+ require "chronos"
2
+ require "chronos/integrations/capistrano"
3
+
4
+ Chronos::Integrations::Capistrano.install(self) if respond_to?(:namespace, true) && respond_to?(:after, true)
@@ -65,9 +65,20 @@ module Chronos
65
65
  unless dependency_max_items.is_a?(Integer) && dependency_max_items >= 1 && dependency_max_items <= 200
66
66
  errors << "dependency_max_items must be between 1 and 200"
67
67
  end
68
+ correlation_attributes.each do |name, value|
69
+ errors << "#{name} must be a String with at most 128 bytes" unless bounded_optional_string?(value, 128)
70
+ end
68
71
  errors
69
72
  end
70
73
 
74
+ def correlation_attributes
75
+ {:revision => revision, :deploy_id => deploy_id, :region => region, :instance_id => instance_id}
76
+ end
77
+
78
+ def bounded_optional_string?(value, limit)
79
+ value.nil? || (value.is_a?(String) && value.bytesize <= limit)
80
+ end
81
+
71
82
  def boolean?(value)
72
83
  [true, false].include?(value)
73
84
  end
@@ -32,7 +32,8 @@ module Chronos
32
32
 
33
33
  ATTRIBUTES = [
34
34
  :project_id, :project_key, :host, :environment, :app_version,
35
- :service_name, :root_directory, :logger, :timeout, :open_timeout,
35
+ :service_name, :revision, :deploy_id, :region, :instance_id,
36
+ :root_directory, :logger, :timeout, :open_timeout,
36
37
  :queue_size, :workers, :enabled, :error_notifications,
37
38
  :ignored_environments, :proxy, :ssl_verify, :user_agent,
38
39
  :max_payload_size, :gzip, :blocklist_keys, :allowlist_keys,
@@ -59,6 +60,7 @@ module Chronos
59
60
  initialize_rails_defaults
60
61
  initialize_apm_defaults
61
62
  initialize_observability_defaults
63
+ initialize_correlation_defaults
62
64
  end
63
65
 
64
66
  def snapshot
@@ -120,6 +122,13 @@ module Chronos
120
122
  @breadcrumb_max_bytes = 2048
121
123
  end
122
124
 
125
+ def initialize_correlation_defaults
126
+ @revision = nil
127
+ @deploy_id = nil
128
+ @region = nil
129
+ @instance_id = nil
130
+ end
131
+
123
132
  def initialize_rails_defaults
124
133
  @rails_enabled = true
125
134
  @rails_capture_in_console = false
@@ -167,7 +176,7 @@ module Chronos
167
176
  @remote_config_max_bytes = 4096
168
177
  @sampling_rate = 1.0
169
178
  @enabled_event_types = [
170
- "exception", "request", "query", "job", "cache", "external_http", "dependencies", "metric_batch"
179
+ "exception", "request", "query", "job", "cache", "external_http", "dependencies", "deploy", "metric_batch"
171
180
  ]
172
181
  @max_remote_send_interval = 60.0
173
182
  end
@@ -0,0 +1,65 @@
1
+ module Chronos
2
+ module Core
3
+ # Builds the bounded release/deploy correlation shared by every event envelope.
4
+ #
5
+ # @responsibility Normalize release, revision, deploy, environment, service, region, and instance.
6
+ # @motivation Give the SaaS stable before/after-deploy dimensions without integration-specific fields.
7
+ # @limits Values come only from explicit configuration or caller overrides; no environment is scanned.
8
+ # @collaborators PayloadSerializer, TelemetrySerializer, and Configuration::Snapshot.
9
+ # @thread_safety Instances hold immutable configuration and calls allocate independent frozen hashes.
10
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
11
+ # @example
12
+ # CorrelationContext.new(config).call("revision" => "abc123")
13
+ # @errors Unreadable optional values become nil and do not escape.
14
+ # @performance Seven strings are copied with a fixed 128-byte maximum each.
15
+ class CorrelationContext
16
+ FIELDS = %w(release revision deploy_id environment service region instance).freeze
17
+
18
+ def initialize(config)
19
+ @config = config
20
+ end
21
+
22
+ def call(overrides = {})
23
+ values = defaults.merge(string_hash(overrides))
24
+ result = FIELDS.each_with_object({}) do |name, correlation|
25
+ correlation[name.freeze] = bounded(values[name], 128)
26
+ end
27
+ result.each_value { |value| value.freeze if value }
28
+ result.freeze
29
+ rescue StandardError
30
+ FIELDS.each_with_object({}) { |name, fallback| fallback[name] = nil }.freeze
31
+ end
32
+
33
+ private
34
+
35
+ def defaults
36
+ {
37
+ "release" => @config.app_version, "revision" => @config.revision,
38
+ "deploy_id" => @config.deploy_id, "environment" => @config.environment,
39
+ "service" => @config.service_name, "region" => @config.region,
40
+ "instance" => @config.instance_id
41
+ }
42
+ end
43
+
44
+ def string_hash(value)
45
+ return {} unless value.is_a?(Hash)
46
+
47
+ value.each_with_object({}) do |(key, child), result|
48
+ result[key.to_s] = child if key.is_a?(String) || key.is_a?(Symbol)
49
+ end
50
+ end
51
+
52
+ def bounded(value, limit)
53
+ return nil if value.nil?
54
+ return nil unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric)
55
+
56
+ text = value.to_s
57
+ text = text.scrub("?") if text.respond_to?(:scrub)
58
+ text = text.byteslice(0, limit) if text.bytesize > limit
59
+ text
60
+ rescue StandardError
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,102 @@
1
+ require "securerandom"
2
+ require "uri"
3
+
4
+ module Chronos
5
+ module Core
6
+ # Normalizes explicit deployment metadata into the versioned deploy payload.
7
+ #
8
+ # @responsibility Validate required deploy identity and bound every public API field.
9
+ # @motivation Deployment commands need predictable safe input independent of frameworks.
10
+ # @limits It does not inspect Git, environment variables, credentials, or deployment systems.
11
+ # @collaborators Chronos.notify_deploy, CorrelationContext, and Configuration::Snapshot.
12
+ # @thread_safety Stateless apart from immutable configuration and an injected ID generator.
13
+ # @compatibility Ruby 2.2.10 through Ruby 2.6.
14
+ # @example
15
+ # DeployNormalizer.new(config).call(:revision => "abc", :version => "1.2.3")
16
+ # @errors Missing environment or both revision/version raise ArgumentError for the agent to contain.
17
+ # @performance Constant work over nine bounded scalar fields.
18
+ class DeployNormalizer
19
+ def initialize(config, options = {})
20
+ @config = config
21
+ @id_generator = options[:id_generator] || proc { SecureRandom.uuid }
22
+ end
23
+
24
+ def call(attributes = {})
25
+ values = string_hash(attributes)
26
+ payload = {
27
+ "deploy_id" => deploy_id(values["deploy_id"] || @config.deploy_id),
28
+ "environment" => bounded(values["environment"] || @config.environment, 128),
29
+ "revision" => bounded(values["revision"] || @config.revision, 128),
30
+ "version" => bounded(values["version"] || @config.app_version, 128),
31
+ "repository" => repository(values["repository"]),
32
+ "actor" => bounded(values["actor"], 128),
33
+ "service" => bounded(values["service"] || @config.service_name, 128),
34
+ "region" => bounded(values["region"] || @config.region, 128),
35
+ "instance" => bounded(values["instance"] || @config.instance_id, 128)
36
+ }
37
+ validate!(payload)
38
+ payload
39
+ end
40
+
41
+ private
42
+
43
+ def validate!(payload)
44
+ raise ArgumentError, "deploy environment is required" if payload["environment"].to_s.empty?
45
+ return unless payload["revision"].to_s.empty? && payload["version"].to_s.empty?
46
+
47
+ raise ArgumentError, "deploy revision or version is required"
48
+ end
49
+
50
+ def generated_id
51
+ value = bounded(@id_generator.call, 128)
52
+ raise ArgumentError, "deploy ID is required" if value.to_s.empty?
53
+
54
+ value
55
+ end
56
+
57
+ def deploy_id(value)
58
+ normalized = bounded(value, 128)
59
+ normalized.to_s.empty? ? generated_id : normalized
60
+ end
61
+
62
+ def repository(value)
63
+ text = bounded(value, 512)
64
+ return nil if text.nil?
65
+
66
+ scp = text.match(/\A[^@]+@([^:]+):(.+)\z/)
67
+ return bounded("#{scp[1]}/#{scp[2]}", 512) if scp
68
+
69
+ uri = URI.parse(text)
70
+ return bounded("#{uri.host}#{uri.path}", 512) if uri.host
71
+ return bounded(uri.path, 512) unless uri.path.to_s.empty?
72
+
73
+ nil
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
78
+ def string_hash(value)
79
+ raise ArgumentError, "deploy attributes must be a Hash" unless value.is_a?(Hash)
80
+
81
+ value.each_with_object({}) do |(key, child), result|
82
+ result[key.to_s] = child if key.is_a?(String) || key.is_a?(Symbol)
83
+ end
84
+ end
85
+
86
+ def bounded(value, limit)
87
+ return nil if value.nil?
88
+ return nil unless scalar?(value)
89
+
90
+ text = value.to_s
91
+ text = text.scrub("?") if text.respond_to?(:scrub)
92
+ text.bytesize > limit ? text.byteslice(0, limit) : text
93
+ rescue StandardError
94
+ nil
95
+ end
96
+
97
+ def scalar?(value)
98
+ value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -44,6 +44,7 @@ module Chronos
44
44
  @sanitizer = options[:sanitizer] || Sanitizer.new(config)
45
45
  @safe_serializer = options[:safe_serializer] || SafeSerializer.new
46
46
  @max_payload_size = options[:max_payload_size] || proc { @config.max_payload_size }
47
+ @correlation = options[:correlation] || CorrelationContext.new(config)
47
48
  end
48
49
 
49
50
  def call(notice)
@@ -70,8 +71,10 @@ module Chronos
70
71
  "service" => {
71
72
  "name" => @config.service_name,
72
73
  "version" => @config.app_version,
73
- "instance_id" => notice.host
74
+ "instance_id" => @config.instance_id || notice.host
74
75
  },
76
+ "correlation" => @correlation.call("environment" => notice.environment,
77
+ "instance" => @config.instance_id || notice.host),
75
78
  "runtime" => notice.runtime,
76
79
  "context" => notice.context,
77
80
  "payload" => payload(notice)
@@ -16,7 +16,7 @@ module Chronos
16
16
  # @errors Unsupported types raise ArgumentError during construction.
17
17
  # @performance Construction is linear in supplied context and payload size.
18
18
  class TelemetryEvent
19
- TYPES = %w(request query job cache external_http dependencies metric_batch).freeze
19
+ TYPES = %w(request query job cache external_http dependencies deploy metric_batch).freeze
20
20
 
21
21
  attr_reader :event_id, :event_type, :timestamp, :context, :payload
22
22
 
@@ -68,6 +68,7 @@ module Chronos
68
68
  @safe_serializer = options[:safe_serializer] || SafeSerializer.new
69
69
  @max_payload_size = options[:max_payload_size] || proc { @config.max_payload_size }
70
70
  @runtime_info = RuntimeInfo.new
71
+ @correlation = options[:correlation] || CorrelationContext.new(config)
71
72
  end
72
73
 
73
74
  def call(event)
@@ -89,13 +90,39 @@ module Chronos
89
90
  "schema_version" => "1.0", "event_id" => event.event_id,
90
91
  "event_type" => event.event_type, "occurred_at" => event.timestamp,
91
92
  "sent_at" => @clock.call.utc.iso8601(6), "project_key" => @config.project_id,
92
- "environment" => @config.environment,
93
- "service" => {"name" => @config.service_name, "version" => @config.app_version,
94
- "instance_id" => runtime[:host]},
93
+ "environment" => event_environment(event),
94
+ "service" => event_service(event, runtime),
95
+ "correlation" => event_correlation(event, runtime),
95
96
  "runtime" => runtime[:runtime], "context" => event.context, "payload" => event.payload
96
97
  }
97
98
  end
98
99
 
100
+ def event_environment(event)
101
+ event.event_type == "deploy" ? event.payload["environment"] : @config.environment
102
+ end
103
+
104
+ def event_service(event, runtime)
105
+ deploy = event.event_type == "deploy" ? event.payload : {}
106
+ {
107
+ "name" => deploy["service"] || @config.service_name,
108
+ "version" => deploy["version"] || @config.app_version,
109
+ "instance_id" => deploy["instance"] || @config.instance_id || runtime[:host]
110
+ }
111
+ end
112
+
113
+ def event_correlation(event, runtime)
114
+ overrides = {"instance" => @config.instance_id || runtime[:host]}
115
+ if event.event_type == "deploy"
116
+ overrides.merge!(
117
+ "release" => event.payload["version"], "revision" => event.payload["revision"],
118
+ "deploy_id" => event.payload["deploy_id"], "environment" => event.payload["environment"],
119
+ "service" => event.payload["service"], "region" => event.payload["region"],
120
+ "instance" => event.payload["instance"]
121
+ )
122
+ end
123
+ @correlation.call(overrides)
124
+ end
125
+
99
126
  def compact_envelope(envelope)
100
127
  envelope["context"] = {"_truncated" => true}
101
128
  envelope["payload"] = {"_truncated" => true}
@@ -0,0 +1,61 @@
1
+ module Chronos
2
+ module Integrations
3
+ # Optional Capistrano task installer using only the public task DSL.
4
+ #
5
+ # @responsibility Register one post-publish task that delegates explicit values to notify_deploy.
6
+ # @motivation Legacy deployments need release reporting without a Capistrano runtime dependency.
7
+ # @limits Values are read only from configured Capistrano variables; Git and ENV are not inspected.
8
+ # @collaborators Capistrano/Rake DSL and the Chronos public facade.
9
+ # @thread_safety Installation is idempotent per DSL object before task execution.
10
+ # @compatibility Capistrano task DSLs available to supported Ruby 2.2.10 through Ruby 2.6 apps.
11
+ # @example
12
+ # require "chronos/capistrano"
13
+ # @errors Missing DSL methods return false; notification failures are contained by Chronos.
14
+ # @performance Adds one task and one after hook; no background worker is created.
15
+ module Capistrano
16
+ INSTALLED_KEY = :@__chronos_capistrano_installed
17
+
18
+ class << self
19
+ def install(dsl)
20
+ return false if dsl.instance_variable_get(INSTALLED_KEY)
21
+ return false unless compatible?(dsl)
22
+
23
+ dsl.send(:namespace, :chronos) do
24
+ dsl.send(:desc, "Notify Chronos about the published deployment") if dsl.respond_to?(:desc, true)
25
+ dsl.send(:task, :notify_deploy) { notify(dsl) }
26
+ end
27
+ dsl.send(:after, "deploy:published", "chronos:notify_deploy")
28
+ dsl.instance_variable_set(INSTALLED_KEY, true)
29
+ true
30
+ rescue StandardError
31
+ false
32
+ end
33
+
34
+ def notify(dsl)
35
+ Chronos.notify_deploy(
36
+ :environment => value(dsl, :stage).to_s,
37
+ :revision => value(dsl, :current_revision) || value(dsl, :branch),
38
+ :version => value(dsl, :chronos_version) || value(dsl, :release_name),
39
+ :repository => value(dsl, :repo_url), :actor => value(dsl, :chronos_actor),
40
+ :deploy_id => value(dsl, :chronos_deploy_id), :service => value(dsl, :chronos_service),
41
+ :region => value(dsl, :chronos_region), :instance => value(dsl, :chronos_instance)
42
+ )
43
+ rescue StandardError
44
+ false
45
+ end
46
+
47
+ private
48
+
49
+ def compatible?(dsl)
50
+ [:namespace, :task, :after, :fetch].all? { |name| dsl.respond_to?(name, true) }
51
+ end
52
+
53
+ def value(dsl, name)
54
+ dsl.send(:fetch, name, nil)
55
+ rescue StandardError
56
+ nil
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,7 +1,7 @@
1
1
  module Chronos
2
- # Public entry points for optional version 0.8 observability integrations.
2
+ # Public entry points for optional observability and deployment integrations.
3
3
  #
4
- # @responsibility Delegate dependency, cache, and outbound HTTP integration calls to the agent.
4
+ # @responsibility Delegate dependency, cache, outbound HTTP, and deploy calls to the agent.
5
5
  # @motivation Keep the main public facade small while preserving one stable Chronos namespace.
6
6
  # @limits It installs only explicitly supplied Net::HTTP connections and never enables features.
7
7
  # @collaborators Chronos::Agent and Chronos::Integrations::NetHttp.
@@ -19,6 +19,13 @@ module Chronos
19
19
  false
20
20
  end
21
21
 
22
+ def notify_deploy(attributes = {}, timeout = Agent::DEFAULT_FLUSH_TIMEOUT)
23
+ agent = current_agent
24
+ agent ? agent.notify_deploy(attributes, timeout) : false
25
+ rescue StandardError
26
+ false
27
+ end
28
+
22
29
  def external_http_integration_options
23
30
  agent = current_agent
24
31
  agent ? agent.external_http_integration_options : {:enabled => false}
@@ -1,4 +1,4 @@
1
1
  module Chronos
2
2
  # Current version of the legacy Chronos Ruby agent.
3
- VERSION = "0.8.0.pre.1".freeze
3
+ VERSION = "0.9.0.pre.1".freeze
4
4
  end
data/lib/chronos.rb CHANGED
@@ -14,6 +14,8 @@ require "chronos/core/notice_builder"
14
14
  require "chronos/core/sensitive_value_filter"
15
15
  require "chronos/core/sanitizer"
16
16
  require "chronos/core/safe_serializer"
17
+ require "chronos/core/correlation_context"
18
+ require "chronos/core/deploy_normalizer"
17
19
  require "chronos/core/payload_serializer"
18
20
  require "chronos/core/telemetry_event"
19
21
  require "chronos/core/sql_normalizer"
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.8.0.pre.1
4
+ version: 0.9.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antonio Jefferson
@@ -88,6 +88,7 @@ files:
88
88
  - SECURITY.md
89
89
  - contracts/apm-batch-v1.schema.json
90
90
  - contracts/dependencies-v1.schema.json
91
+ - contracts/deploy-v1.schema.json
91
92
  - contracts/event-v1.schema.json
92
93
  - contracts/rack-context-v1.schema.json
93
94
  - contracts/sidekiq-job-v1.schema.json
@@ -103,6 +104,7 @@ files:
103
104
  - docs/adr/ADR-014-sidekiq-envelope-context.md
104
105
  - docs/adr/ADR-015-bounded-apm-aggregation.md
105
106
  - docs/adr/ADR-016-explicit-observability-integrations.md
107
+ - docs/adr/ADR-017-deploy-events-and-correlation.md
106
108
  - docs/architecture.md
107
109
  - docs/compatibility.md
108
110
  - docs/configuration.md
@@ -113,6 +115,7 @@ files:
113
115
  - docs/modules/cache-observability.md
114
116
  - docs/modules/configuration.md
115
117
  - docs/modules/dependencies.md
118
+ - docs/modules/deploy-tracking.md
116
119
  - docs/modules/external-http.md
117
120
  - docs/modules/notice-pipeline.md
118
121
  - docs/modules/rack-context.md
@@ -142,6 +145,7 @@ files:
142
145
  - lib/chronos/application/dependency_reporter.rb
143
146
  - lib/chronos/application/remote_configuration.rb
144
147
  - lib/chronos/application/retry_policy.rb
148
+ - lib/chronos/capistrano.rb
145
149
  - lib/chronos/configuration.rb
146
150
  - lib/chronos/configuration/apm_validation.rb
147
151
  - lib/chronos/configuration/snapshot.rb
@@ -150,6 +154,8 @@ files:
150
154
  - lib/chronos/core/backtrace_parser.rb
151
155
  - lib/chronos/core/breadcrumb.rb
152
156
  - lib/chronos/core/cache_normalizer.rb
157
+ - lib/chronos/core/correlation_context.rb
158
+ - lib/chronos/core/deploy_normalizer.rb
153
159
  - lib/chronos/core/exception_cause_collector.rb
154
160
  - lib/chronos/core/metric_aggregate.rb
155
161
  - lib/chronos/core/notice.rb
@@ -163,6 +169,7 @@ files:
163
169
  - lib/chronos/core/telemetry_event.rb
164
170
  - lib/chronos/errors.rb
165
171
  - lib/chronos/integrations.rb
172
+ - lib/chronos/integrations/capistrano.rb
166
173
  - lib/chronos/integrations/job_payload.rb
167
174
  - lib/chronos/integrations/net_http.rb
168
175
  - lib/chronos/integrations/rack.rb