allstak 0.1.1 → 0.2.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: 943cc01f8f95ed1cabef20b1c88b50e462b7be871c0a962a46c225e85902e355
4
- data.tar.gz: f61664c5d936089b368e22c3c71d32f58f05081a35b478814a5b4f041b799133
3
+ metadata.gz: f2487acc9b1a4a0491b0a86d246b62ffcdb3a02499d728c8eaf01af50c9779d3
4
+ data.tar.gz: 411db967c468cb7c1793ade463b314cb44169b19071a3ed1d90fdf5bc3a582dc
5
5
  SHA512:
6
- metadata.gz: 7262178e8689bed7b675cc0fd60c494978ce73f826eac9566fc063054d33043c8475350e0780acadfd81c23a626c30f3ab24e1b5ca2e3191b83898b81e473f82
7
- data.tar.gz: e41f14091f0e5abde8930f421394a36bdaf12e617d5376c4126f86cb700c16de740f6fdb632bd60ddc0f3dd15ba655e6c75d532b43b3bfe297b8f04ff03bfc43
6
+ metadata.gz: 9c91a85d0c9208021a2ed16ad0c3fa9a8d701b313d3dcd7e127b3d3a87c7d454cdd007dc65e948dec85fdf2f1a4cffaa4fc62367d13a23553df7cb7770566f55
7
+ data.tar.gz: 6e597806d72c3da0692ff4c14eea6aef1a09d7c6c83bbc917015fa925e9e55e82e9eebc5e91b6a9c232f0c40ce64f4b603870da485ec8526876710b34f0f938d
data/CHANGELOG.md CHANGED
@@ -3,6 +3,141 @@
3
3
  All notable changes to the AllStak Ruby SDK.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## Unreleased
7
+
8
+ ## [0.2.0] — 2026-05-29
9
+
10
+ ### Added — Release-health session tracking
11
+ - `AllStak::SessionTracker` — server-mode single-session release-health
12
+ lifecycle that mirrors the AllStak Java SDK. On boot it POSTs
13
+ `/ingest/v1/sessions/start` (off the hot path, on a daemon thread) with a
14
+ per-process `sessionId`, the resolved release, environment, and SDK identity;
15
+ on shutdown it POSTs `/ingest/v1/sessions/end` (synchronous, best-effort)
16
+ with `durationMs` + final status. Status model `ok | errored | crashed |
17
+ abnormal`: handled errors transition `ok -> errored`, an unhandled/fatal
18
+ exception sets `crashed` (terminal). Status transitions are in-memory only —
19
+ only `start`/`end` perform network I/O, so per-error latency is unaffected,
20
+ enabling crash-free session/user rates on the backend.
21
+ - Idempotent and re-entrancy safe (double `start`/`end` are no-ops). Sessions
22
+ are never sampled. Fully fail-open: a network error or read-only runtime must
23
+ never crash app boot or shutdown.
24
+ - Opt-out via `config.enable_auto_session_tracking = false`. Auto-disables under
25
+ a unit-test runtime so the suite never emits session traffic. The active
26
+ `sessionId` is attached to error/event payloads (and is allowlisted in the
27
+ sanitizer so it survives PII scrubbing).
28
+
29
+ ### Added — Offline / persistent transport queue
30
+ - `AllStak::Transport::EventSpool` — a filesystem spool for un-sent telemetry
31
+ envelopes so events survive a process restart, network outage, or shutdown.
32
+ One JSON file per envelope (atomic temp-write + rename) so a partially written
33
+ file is discarded without losing the rest of the queue. Persisted bytes are
34
+ **already PII-scrubbed** by the caller before they hit disk.
35
+ - Bounded by COUNT (default 100), BYTES (default 4 MiB), and AGE (default 48h),
36
+ evicting OLDEST-first via an embedded millis+seq ordering. Stale entries past
37
+ max-age are dropped on drain. On the next SDK init the queue is replayed and
38
+ delivered entries are removed; unreadable/oversized entries are dropped.
39
+ - Default ON for server runtimes (`config.enable_offline_queue`), spool dir
40
+ `<tmpdir>/allstak-spool` (override via `config.offline_queue_dir`), with
41
+ configurable `offline_queue_max_entries` / `_max_bytes` / `_max_age_s`. Env
42
+ overrides: `ALLSTAK_OFFLINE_QUEUE`, `ALLSTAK_OFFLINE_QUEUE_DIR`. Fully
43
+ fail-open: a read-only / sandboxed / serverless FS degrades silently to
44
+ in-memory behavior and never blocks capture or init.
45
+
46
+ ### Added — Value-pattern PII scrubbing + sendDefaultPii
47
+ - `AllStak::Sanitizer` gains a second scrubbing layer alongside the existing
48
+ key-name denylist: VALUE-PATTERN redaction that scans free-text string values
49
+ for PII regardless of key name (Sentry data-scrubbing parity). Tier A is
50
+ ALWAYS scrubbed — Luhn-validated credit-card numbers (13–19 digits, optional
51
+ space/hyphen separators) and hyphenated US SSNs. Tier B — email addresses and
52
+ IPv4/IPv6 — is scrubbed UNLESS `send_default_pii` is enabled.
53
+ - `config.send_default_pii` (default `false`, Sentry parity; env
54
+ `ALLSTAK_SEND_DEFAULT_PII`) opts into shipping email/IP values. The
55
+ always-on financial/identity scrubbers are not affected by this flag.
56
+ - Structural exemptions prevent over-redaction: the explicit `user` object and
57
+ `stackTrace`/`frames` subtrees are key-name-redacted but never value-scrubbed,
58
+ and structured scalar keys (release, sdk/version, url/path/host, span/trace
59
+ ids, `sessionId`, timestamp) are skipped. Strings over 16 KiB are passed
60
+ through. Value scrubbing is fail-open: any error falls back to the
61
+ key-redacted (non-value-scrubbed) structure.
62
+
63
+ ### Added — Global capture, sampling, and release auto-detection
64
+ - `AllStak::GlobalHandler` — global `at_exit` hook captures uncaught exceptions
65
+ (mechanism `at_exit`), feeding session crash status, before re-raising.
66
+ - `before_send` callback + client-side event/trace sampling hooks.
67
+ - Runtime release auto-detection from local git (`AllStak.register_runtime_release`
68
+ / `config.detect_release`) so events are attributed to a release without
69
+ manual configuration; auto-disabled under a test runtime.
70
+ - Transport hardening: every wire payload is sanitized at the single
71
+ `HttpTransport#post` chokepoint, the `sdk.version` wire value is corrected,
72
+ and `Retry-After` is honored on 429/503.
73
+
74
+ ### Added — Sidekiq integration
75
+ - `AllStak::Integrations::Sidekiq::Middleware` — Sidekiq **server** middleware
76
+ that starts a fresh trace per job, wraps execution in a `queue.process`
77
+ span + breadcrumb, and auto-captures job failures with worker class, jid,
78
+ queue, and (PII-sanitized via `Sanitizer`) args, re-raising so Sidekiq's
79
+ retry machinery still runs.
80
+ - `death_handler` registration captures jobs that exhaust their retries
81
+ (`mechanism=sidekiq.death`).
82
+ - Auto-registered by `AllStak.configure` via `Sidekiq.configure_server` when
83
+ Sidekiq is present; a graceful, idempotent no-op otherwise.
84
+
85
+ ### Added — Rails Railtie
86
+ - `AllStak::Integrations::Rails::Railtie` — a real `Rails::Railtie` that
87
+ auto-inserts the Rack middleware into the app's middleware stack via an
88
+ initializer. Rails apps are now instrumented **without manual wiring**,
89
+ matching the prior README claim. Guarded (only loads when `Rails::Railtie`
90
+ is defined) and idempotent.
91
+
92
+ ### Docs
93
+ - README + gemspec description corrected to reflect real auto-install behavior
94
+ (Railtie + Sidekiq server middleware) instead of the previously overstated
95
+ "Rails middleware" claim. Fixed stale `AllStak::Rack::Middleware` and
96
+ `AllStak.start_span` references.
97
+
98
+ ### Tests / tooling
99
+ - New minitest coverage for the Sidekiq middleware (capture + context + span +
100
+ no-op without Sidekiq), the Railtie (middleware insertion + guarded without
101
+ Rails), and backfill for `Client`, `HttpTransport`, and `Modules::Errors`.
102
+ - Removed unused `rspec` / `webmock` dev-dependencies (the suite is minitest
103
+ only; transport tests stub `Net::HTTP` directly). CI workflow simplified
104
+ accordingly.
105
+ - Added minitest coverage for the new waves: `test_session_tracker`,
106
+ `test_session_error_wiring`, `test_event_spool`, `test_offline_queue`,
107
+ `test_send_default_pii`, plus value-pattern cases in `test_sanitizer` and
108
+ `test_global_handler` / `test_release_autodetect` / `test_retry_after`.
109
+ Full suite: 177 runs, 448 assertions, 0 failures.
110
+
111
+ ### Repo hygiene
112
+ - Untracked stray build/vendor artifacts that the `.gitignore` already covers
113
+ but were committed before the ignore rules existed: the root `*.gem` packs
114
+ (`allstak-0.1.0.gem`, `allstak-0.1.1.gem`) and the committed
115
+ `vendor/bundle/` dependency cache. These are not part of the published gem
116
+ (`spec.files` ships only `lib/**/*.rb`, README, CHANGELOG, LICENSE, gemspec).
117
+
118
+ ## 0.1.2 — 2026-05-18
119
+
120
+ ### Added — Recursive payload sanitizer
121
+ - New `AllStak::Sanitizer` module with `scrub(payload, extra_denylist: nil)`.
122
+ 25-term canonical denylist, recursive over `Hash` / `Array`, `[REDACTED]`
123
+ substitution, `object_id` Set cycle protection, pure (no caller mutation).
124
+ - Wired into `AllStak::Transport::HttpTransport#post` — every wire payload
125
+ is scrubbed before serialization. One chokepoint protects errors, logs,
126
+ http, db, traces.
127
+ - Fail-open: if the sanitizer raises on a pathological payload, the SDK
128
+ logs at Warning level and sends raw — telemetry is never blocked.
129
+
130
+ ### Live canary E2E
131
+ - Event `03188061-8054-439a-8784-a3b52436549e` against `api.allstak.sa`.
132
+ - ClickHouse confirmed `leak_pos = 0` across `metadata`, `stack_trace`,
133
+ `breadcrumbs`, `message`. Canary `should_not_leak_ruby` planted in
134
+ `password`, `authorization`, `cookie`, `Bearer`, `api_key`, request
135
+ headers / body, `credit_card`, `ssn`, and a 3-level-nested `token`.
136
+ All scrubbed.
137
+
138
+ ### Tests
139
+ - `test/test_sanitizer.rb` — 12 runs, 51 assertions, 0 failures.
140
+
6
141
  ## 0.1.0 — 2026-04-11
7
142
 
8
143
  First public release. Driven end-to-end through a real Sinatra + ActiveRecord
data/README.md CHANGED
@@ -1,291 +1,123 @@
1
- # AllStak Ruby SDK
1
+ # allstak
2
2
 
3
- Official Ruby SDK for [AllStak](https://allstak.dev) error tracking,
4
- structured logs, HTTP + ActiveRecord monitoring, distributed tracing, and cron
5
- monitoring for Rack-based Ruby applications (Rails, Sinatra, Roda, Hanami).
3
+ AllStak SDK for Ruby, Rails, Rack, and Sidekiq. Captures exceptions, logs, inbound and outbound HTTP requests, ActiveRecord queries, Sidekiq job failures, spans, and cron heartbeats. Rails apps are auto-instrumented via a Railtie; Sidekiq via a server middleware.
6
4
 
7
- ```ruby
8
- gem "allstak"
9
- ```
5
+ ## Install
10
6
 
11
7
  ```bash
12
- bundle install
13
- # or
14
8
  gem install allstak
15
9
  ```
16
10
 
17
- ## 60-second setup
18
-
19
- ```ruby
20
- require "allstak"
21
-
22
- AllStak.configure do |c|
23
- c.api_key = ENV["ALLSTAK_API_KEY"]
24
- c.environment = "production"
25
- c.release = "myapp@1.2.3"
26
- c.service_name = "myapp-api"
27
- end
28
-
29
- # Rack / Sinatra / Rails:
30
- use AllStak::Integrations::Rack::Middleware
31
-
32
- # Manual capture:
33
- begin
34
- risky!
35
- rescue => e
36
- AllStak.capture_exception(e)
37
- end
38
- ```
39
-
40
- That is the whole setup. Every request, every unhandled exception, every
41
- ActiveRecord query, every outbound Net::HTTP call, and every trace are
42
- captured automatically.
43
-
44
- ## Public API (cross-SDK consistent)
45
-
46
- Every method below is a module-level method on `AllStak`, matching the
47
- names used by the JS, Python, Java, Go, PHP, and .NET SDKs so docs carry
48
- across languages.
11
+ Or add it to your Gemfile:
49
12
 
50
13
  ```ruby
51
- AllStak.configure { |c| ... } # once at bootstrap
52
-
53
- AllStak.set_user(id: "42", email: "alice@example.com") # user context
54
- AllStak.clear_user
55
-
56
- AllStak.set_tag("service", "checkout") # sticky tag
57
- AllStak.set_tags(region: "us-east-1", tier: "web") # bulk
58
- AllStak.set_context("deployment", "canary") # sticky context
59
-
60
- AllStak.capture_exception(exc) # preferred for errors
61
- AllStak.capture_error("DomainError", "bad input") # without a throwable
62
- AllStak.capture_message("hello", level: "info") # plain string event
63
-
64
- AllStak.log.info("request started", metadata: {...}) # structured logs
65
- AllStak.tracing # Tracing module
66
- AllStak.http # HTTP monitor
67
- AllStak.database # DB query monitor
68
- AllStak.cron.job("daily-report") { run_job } # cron heartbeats
69
-
70
- AllStak.flush # drain buffers
71
- AllStak.shutdown # drain + close
14
+ gem "allstak"
72
15
  ```
73
16
 
74
- `AllStak.capture_message`, `AllStak.set_tag`, and `AllStak.set_context`
75
- landed in 0.1.1 as cross-SDK parity additions. Older 0.1.0 code that only
76
- used `capture_exception` / `capture_error` keeps working — no breaking
77
- changes.
78
-
79
- ## Rails
17
+ ## Setup
80
18
 
81
19
  ```ruby
82
- # config/initializers/allstak.rb
83
20
  require "allstak"
84
21
 
85
- AllStak.configure do |c|
86
- c.api_key = ENV["ALLSTAK_API_KEY"]
87
- c.environment = Rails.env
88
- c.release = "myapp@#{ENV['GIT_SHA'] || 'dev'}"
89
- c.service_name = "myapp-api"
22
+ AllStak.configure do |config|
23
+ config.api_key = ENV["ALLSTAK_API_KEY"]
24
+ config.environment = ENV.fetch("APP_ENV", "production")
25
+ config.release = ENV["ALLSTAK_RELEASE"]
26
+ config.service_name = "checkout-api"
90
27
  end
91
28
 
92
- Rails.application.config.middleware.use AllStak::Integrations::Rack::Middleware
29
+ AllStak.capture_exception(StandardError.new("checkout failed"))
30
+ AllStak.log.info("payment retry", metadata: { order_id: "ord_123" })
93
31
  ```
94
32
 
95
- ## Sinatra
33
+ ## Rails
34
+
35
+ For Rails apps there is **no manual wiring**. Requiring the gem loads a
36
+ `Railtie` that auto-inserts the AllStak Rack middleware into your application's
37
+ middleware stack during boot. Just configure the SDK during app initialization
38
+ (e.g. in `config/initializers/allstak.rb`):
96
39
 
97
40
  ```ruby
98
- require "sinatra/base"
99
41
  require "allstak"
100
42
 
101
- AllStak.configure { |c| c.api_key = ENV["ALLSTAK_API_KEY"] }
102
-
103
- class MyApp < Sinatra::Base
104
- use AllStak::Integrations::Rack::Middleware
105
- # ...
43
+ AllStak.configure do |config|
44
+ config.api_key = ENV["ALLSTAK_API_KEY"]
45
+ config.environment = Rails.env
46
+ config.service_name = "checkout-api"
106
47
  end
107
48
  ```
108
49
 
109
- ## What gets captured automatically
110
-
111
- | What | How |
112
- | ------------------------------------- | ------------------------------------------ |
113
- | Unhandled exceptions | Rack middleware |
114
- | Inbound HTTP requests | Rack middleware |
115
- | Per-request trace ID | Rack middleware |
116
- | User context (from env/session) | Rack middleware |
117
- | ActiveRecord SQL queries | `sql.active_record` subscriber |
118
- | Outbound HTTP via `Net::HTTP` | `Net::HTTP#request` patched |
50
+ You then get inbound request telemetry, trace propagation, unhandled-exception
51
+ capture, ActiveRecord query instrumentation, and outbound `Net::HTTP` capture
52
+ automatically. The middleware insertion is idempotent.
119
53
 
120
- The ActiveRecord subscriber and Net::HTTP patch are installed automatically
121
- by `AllStak.configure` — no extra setup needed.
54
+ ## Rack (non-Rails)
122
55
 
123
- ## ActiveRecord
124
-
125
- `AllStak.configure` installs an `ActiveSupport::Notifications` subscriber on
126
- `sql.active_record`, which gives you normalized SQL, duration, row counts,
127
- status, and error messages for every query — whether it's from an Active Record
128
- relation, a `find_by_sql`, or a raw `connection.execute`. No duplication,
129
- because every AR query fires exactly one `sql.active_record` event.
56
+ For plain Rack apps (Sinatra, Roda, etc.), add the middleware yourself:
130
57
 
131
58
  ```ruby
132
- # Nothing to do — this just works:
133
- User.where(email: "alice@example.com").first
134
- # → captured as: SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?
59
+ use AllStak::Integrations::Rack::Middleware
135
60
  ```
136
61
 
137
- ## Outbound HTTP (Net::HTTP)
62
+ ## Sidekiq
138
63
 
139
- ```ruby
140
- # Also nothing to do:
141
- Net::HTTP.get(URI("https://api.example.com/v1/data"))
142
- # captured as outbound HTTP telemetry with method, host, path, status, duration
143
- ```
144
-
145
- The SDK patches `Net::HTTP#request` at `configure` time. Since every
146
- convenience method (`get`, `post`, `post_form`, etc.) funnels through
147
- `#request`, there is no duplication. Calls to your AllStak ingest host are
148
- skipped to avoid recursive instrumentation.
64
+ When Sidekiq is present, `AllStak.configure` registers a Sidekiq **server
65
+ middleware** automatically no manual setup. It wraps each job in a span and
66
+ breadcrumb, and captures job failures with worker class, jid, queue, and
67
+ (PII-sanitized) args as context, then re-raises so Sidekiq's retry machinery
68
+ still runs. Jobs that exhaust their retries are also captured via a death
69
+ handler. When Sidekiq is not installed, this is a graceful no-op.
149
70
 
150
- ## Manual capture cheat sheet
71
+ ## Spans
151
72
 
152
73
  ```ruby
153
- # Errors
154
- AllStak.capture_exception(exc, metadata: { order_id: "ORD-123" })
155
- AllStak.capture_error("StripeTimeout", "Stripe /v1/charges timed out after 30s", level: "error")
156
-
157
- # Logs (buffered, flushed in background)
158
- AllStak.log.info("Order placed", metadata: { id: "ORD-1" })
159
- AllStak.log.warn("Slow query", metadata: { ms: 4500 })
160
- AllStak.log.error("Payment failed", metadata: { gateway: "stripe" })
161
- # valid levels: debug | info | warn | error | fatal
162
-
163
- # Distributed tracing (block-form)
164
- AllStak.tracing.in_span("db.query", description: "SELECT users") do |span|
165
- span.set_tag("db.type", "postgresql")
166
- rows = User.all.to_a
74
+ AllStak.tracing.in_span("checkout.authorize") do
75
+ authorize_payment
167
76
  end
77
+ ```
168
78
 
169
- # Cron monitoring — slug auto-created on first ping
170
- AllStak.cron.job("daily-report") do
171
- generate_report
172
- end
79
+ ## Configuration
173
80
 
174
- # User context (for events that should show who was affected)
175
- AllStak.set_user(id: "u-1", email: "alice@example.com")
176
- AllStak.clear_user
81
+ | Option | Description |
82
+ | --- | --- |
83
+ | `api_key` | Project API key. |
84
+ | `environment` | Deployment environment. |
85
+ | `release` | App version or commit SHA. |
86
+ | `service_name` | Logical service name. |
87
+ | `flush_interval_ms` | Background flush interval. |
88
+ | `buffer_size` | Max buffered events. |
89
+ | `install_at_exit_handler` | Install a process-wide `at_exit` hook that captures the exception terminating the process as an unhandled event (default `true`). |
90
+ | `before_send` | Callable invoked with the event hash just before transport. Return a modified hash, or `nil` to drop the event. Fails open (sends the original) if it raises. |
91
+ | `sample_rate` | Float in `[0.0, 1.0]` head-sampling rate for error/message events (default `1.0` = keep all). |
92
+ | `traces_sample_rate` | Float in `[0.0, 1.0]` span sampling rate. `nil` (default) keeps every span and the `traceparent` sampled flag; when set, span creation is sampled and the propagated `traceparent` flag reflects the decision. |
93
+
94
+ For top-level uncaught exceptions outside Rack (workers, threads, rake tasks), the `at_exit` handler catches genuine unhandled terminations automatically. You can also report manually at a boundary:
177
95
 
178
- # Graceful flush
179
- AllStak.flush
96
+ ```ruby
97
+ begin
98
+ run_worker
99
+ rescue => e
100
+ AllStak.capture_unhandled(e) # mechanism=at_exit, handled=false
101
+ raise
102
+ end
180
103
  ```
181
104
 
182
- ## Dashboard mapping
105
+ ## Privacy
183
106
 
184
- | Your code | Dashboard page |
185
- | ----------------------------------------- | --------------------- |
186
- | `AllStak.capture_exception` / middleware | **Errors**, **Incidents** |
187
- | `AllStak.log.*` | **Logs** |
188
- | Rack middleware (inbound) | **Requests** |
189
- | `Net::HTTP` (outbound, auto) | **Requests** (outbound) |
190
- | ActiveRecord queries (auto) | **Database** |
191
- | `AllStak.tracing.in_span` | **Traces** |
192
- | `AllStak.cron.job` / `cron.ping` | **Cron Jobs** |
193
-
194
- ## Configuration
195
-
196
- | Option | Default | Notes |
197
- | ------------------------- | ------------------------- | ----- |
198
- | `api_key` | `ENV["ALLSTAK_API_KEY"]` | Your `ask_live_...` key. |
199
- | `host` | `https://api.allstak.sa` | Override with your AllStak ingest host. |
200
- | `environment` | `nil` | e.g. `"production"` |
201
- | `release` | `nil` | e.g. `"myapp@1.2.3"` |
202
- | `service_name` | `"ruby-service"` | Shown on spans and logs. |
203
- | `flush_interval_ms` | `2000` | Background flush interval. |
204
- | `buffer_size` | `500` | Max buffered items per feature. |
205
- | `debug` | `false` | Verbose SDK logging. |
206
- | `connect_timeout` | `3` | Transport connect timeout (seconds). |
207
- | `read_timeout` | `3` | Transport read timeout (seconds). |
208
- | `max_retries` | `5` | Retry 5xx with exponential backoff. |
209
- | `capture_unhandled_exceptions` | `true` | Auto-capture from middleware. |
210
- | `capture_http_requests` | `true` | Auto-capture inbound HTTP. |
211
- | `capture_user_context` | `true` | Attach user claims to errors. |
212
- | `capture_sql` | `true` | Auto-capture AR queries. |
213
-
214
- Environment variables: `ALLSTAK_API_KEY`, `ALLSTAK_HOST`, `ALLSTAK_ENVIRONMENT`,
215
- `ALLSTAK_RELEASE`, `ALLSTAK_SERVICE`, `ALLSTAK_DEBUG`.
216
-
217
- ## Production notes
218
-
219
- - **Never crashes your app.** Every integration catches its own exceptions
220
- and logs at debug level. The middleware re-raises so your framework's
221
- exception handler still runs.
222
- - **Retries.** 5xx and network errors retry with exponential backoff
223
- (1s → 2s → 4s → 8s, +jitter, max 5 attempts). 4xx are not retried.
224
- - **401 disables the SDK.** An invalid API key disables the SDK for the
225
- rest of the process — no further events are sent, a warning is logged
226
- once, and your app keeps running.
227
- - **Flush on shutdown.** `at_exit` triggers a best-effort flush.
228
- - **Thread-safe.** All public APIs are safe to call from any thread.
229
- Trace context uses Ruby's thread-local storage.
230
- - **Non-blocking.** Telemetry is buffered and flushed on background threads.
231
- Your request pipeline is never blocked by SDK work.
107
+ The SDK redacts common sensitive headers and fields. Avoid putting secrets in custom metadata.
232
108
 
233
109
  ## Troubleshooting
234
110
 
235
- | Symptom | Fix |
236
- | ------------------------------------ | ------------------------------------------------ |
237
- | No events in dashboard | Check `host` and `api_key`. Set `debug = true`. |
238
- | 401 warning | Invalid API key. Create a new one in Settings. |
239
- | Inbound requests missing | Make sure `use AllStak::Integrations::Rack::Middleware`. |
240
- | DB queries missing | Make sure `AllStak.configure` runs BEFORE your first AR query. |
241
- | Outbound HTTP missing | Same — `configure` must run before the first `Net::HTTP` call. |
242
- | Cron monitor not appearing | Auto-created on first ping; check the slug matches. |
111
+ - No events: confirm `ALLSTAK_API_KEY` is available before configuration.
112
+ - Missing request telemetry (Rails): confirm `require "allstak"` runs at boot so the Railtie can auto-insert the middleware.
113
+ - Missing request telemetry (plain Rack): confirm `use AllStak::Integrations::Rack::Middleware` is in your Rack stack.
114
+ - Short-lived job: call `AllStak.flush` before exit when possible.
243
115
 
244
- ## Full Sinatra + ActiveRecord example
116
+ ## Contributing and Support
245
117
 
246
- ```ruby
247
- require "sinatra/base"
248
- require "active_record"
249
- require "allstak"
250
-
251
- AllStak.configure do |c|
252
- c.api_key = ENV["ALLSTAK_API_KEY"]
253
- c.environment = "production"
254
- c.release = "taskflow@1.4.2"
255
- c.service_name = "taskflow-api"
256
- end
257
-
258
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "app.db")
259
-
260
- class Task < ActiveRecord::Base; end
261
-
262
- class TaskFlow < Sinatra::Base
263
- use AllStak::Integrations::Rack::Middleware
264
-
265
- get "/tasks" do
266
- Task.all.to_json
267
- end
268
-
269
- post "/tasks/:id/notify" do
270
- task = Task.find(params[:id])
271
- AllStak.tracing.in_span("http.notify", description: "POST httpbin.org/post") do |span|
272
- span.set_tag("task.id", task.id.to_s)
273
- uri = URI("https://httpbin.org/post")
274
- Net::HTTP.post(uri, { task_id: task.id }.to_json, "Content-Type" => "application/json")
275
- end
276
- { ok: true }.to_json
277
- end
278
-
279
- error do
280
- # Framework-level rescue. Sinatra handles the exception before Rack middleware
281
- # sees it, so forward manually:
282
- e = env["sinatra.error"]
283
- AllStak.capture_exception(e) if e
284
- status 500
285
- { error: e.class.name, message: e.message }.to_json
286
- end
287
- end
288
- ```
118
+ - Report bugs with the GitHub bug report template: https://github.com/AllStak/allstak-ruby/issues/new/choose
119
+ - Open pull requests using the checklist in [CONTRIBUTING.md](CONTRIBUTING.md).
120
+ - Report security vulnerabilities privately through [SECURITY.md](SECURITY.md).
289
121
 
290
122
  ## License
291
123
 
data/allstak.gemspec CHANGED
@@ -4,19 +4,19 @@ Gem::Specification.new do |spec|
4
4
  spec.name = "allstak"
5
5
  spec.version = AllStak::VERSION
6
6
  spec.authors = ["AllStak"]
7
- spec.email = ["sdk@allstak.dev"]
7
+ spec.email = ["sdk@allstak.sa"]
8
8
 
9
9
  spec.summary = "Official AllStak Ruby SDK — error tracking, logs, HTTP + ActiveRecord monitoring, tracing, and cron monitoring"
10
- spec.description = "Production-ready Ruby SDK for AllStak observability: Rack/Rails middleware, ActiveRecord instrumentation, outbound HTTP capture, distributed tracing, cron monitoring, and structured logs."
11
- spec.homepage = "https://allstak.dev"
10
+ spec.description = "Production-ready Ruby SDK for AllStak observability: auto-installing Rails Railtie and Rack middleware, a Sidekiq server middleware, ActiveRecord instrumentation, outbound HTTP capture, distributed tracing, cron monitoring, and structured logs."
11
+ spec.homepage = "https://allstak.sa"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = ">= 3.0.0"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/allstak-io/allstak-ruby"
17
- spec.metadata["changelog_uri"] = "https://github.com/allstak-io/allstak-ruby/blob/main/CHANGELOG.md"
18
- spec.metadata["bug_tracker_uri"] = "https://github.com/allstak-io/allstak-ruby/issues"
19
- spec.metadata["documentation_uri"] = "https://allstak.dev/docs/sdks/ruby"
16
+ spec.metadata["source_code_uri"] = "https://github.com/AllStak/allstak-ruby"
17
+ spec.metadata["changelog_uri"] = "https://github.com/AllStak/allstak-ruby/blob/main/CHANGELOG.md"
18
+ spec.metadata["bug_tracker_uri"] = "https://github.com/AllStak/allstak-ruby/issues"
19
+ spec.metadata["documentation_uri"] = "https://allstak.sa/docs/sdks/ruby"
20
20
  spec.metadata["rubygems_mfa_required"] = "true"
21
21
 
22
22
  spec.files = Dir[
@@ -33,8 +33,9 @@ Gem::Specification.new do |spec|
33
33
  # Framework integrations (Rack, Rails, ActiveRecord, Net::HTTP) are loaded
34
34
  # lazily and only activate when the host app has them available.
35
35
 
36
- spec.add_development_dependency "rspec", "~> 3.12"
37
- spec.add_development_dependency "webmock", "~> 3.19"
36
+ # Tests use minitest only (Ruby's bundled stdlib test framework); the
37
+ # transport tests stub Net::HTTP directly, so no rspec/webmock is needed.
38
+ spec.add_development_dependency "rake", "~> 13.0"
38
39
  spec.add_development_dependency "rack", "~> 3.0"
39
40
  spec.add_development_dependency "activerecord", "~> 8.0"
40
41
  end
@@ -1,14 +1,19 @@
1
1
  module AllStak
2
2
  # The AllStak SDK client. Create once via {AllStak.configure}.
3
3
  class Client
4
- attr_reader :config, :logger, :errors, :logs, :http, :tracing, :database, :cron, :tags, :contexts
4
+ attr_reader :config, :logger, :errors, :logs, :http, :tracing, :database, :cron, :tags, :contexts, :session_tracker
5
5
 
6
6
  def initialize(config, logger)
7
7
  @config = config
8
8
  @logger = logger
9
9
  @transport = Transport::HttpTransport.new(config, logger)
10
10
 
11
- @errors = Modules::Errors.new(@transport, config, logger)
11
+ # Release-health session tracker: one session per process. Started by
12
+ # AllStak.configure after the release is resolved; ended in #shutdown.
13
+ @session_tracker = SessionTracker.new(config, @transport, logger)
14
+
15
+ @errors = Modules::Errors.new(@transport, config, logger,
16
+ session_id_provider: -> { @session_tracker&.current_session_id })
12
17
  @logs = Modules::Logs.new(@transport, config, logger)
13
18
  @http = Modules::HttpMonitor.new(@transport, config, logger)
14
19
  @tracing = Modules::Tracing.new(@transport, config, logger)
@@ -20,6 +25,37 @@ module AllStak
20
25
  at_exit { shutdown rescue nil }
21
26
  end
22
27
 
28
+ # The active release-health session id (nil before start / after shutdown).
29
+ def current_session_id
30
+ @session_tracker&.current_session_id
31
+ end
32
+
33
+ # Begin the release-health session. Idempotent + fail-open. Called by
34
+ # AllStak.configure once the release has been finalized.
35
+ def start_session
36
+ @session_tracker&.start
37
+ end
38
+
39
+ # Replay any telemetry persisted by a previous process/outage. Runs on a
40
+ # daemon thread so init never blocks on the network; fully fail-open. Called
41
+ # by AllStak.configure after the client is built. No-op when the offline
42
+ # queue is disabled/unavailable.
43
+ def drain_offline_queue
44
+ transport = @transport
45
+ return unless transport.respond_to?(:drain_spool)
46
+ thread = Thread.new do
47
+ begin
48
+ transport.drain_spool
49
+ rescue StandardError => e
50
+ @logger&.debug("[AllStak] offline drain failed: #{e.class}: #{e.message}")
51
+ end
52
+ end
53
+ thread.abort_on_exception = false
54
+ self
55
+ rescue StandardError
56
+ self
57
+ end
58
+
23
59
  def set_user(id: nil, email: nil, ip: nil)
24
60
  @errors.set_user(id: id, email: email, ip: ip)
25
61
  end
@@ -30,11 +66,13 @@ module AllStak
30
66
 
31
67
  def capture_exception(exc, **kw)
32
68
  kw = merge_default_metadata(kw)
69
+ mark_session_for(kw)
33
70
  @errors.capture_exception(exc, **kw)
34
71
  end
35
72
 
36
73
  def capture_error(exception_class, message, **kw)
37
74
  kw = merge_default_metadata(kw)
75
+ mark_session_for(kw)
38
76
  @errors.capture_error(exception_class, message, **kw)
39
77
  end
40
78
 
@@ -80,10 +118,28 @@ module AllStak
80
118
  @http.shutdown
81
119
  @tracing.shutdown
82
120
  @database.shutdown
121
+ # Graceful shutdown: end the release-health session last so its
122
+ # /sessions/end carries the final accumulated status. Best-effort.
123
+ @session_tracker&.end rescue nil
83
124
  end
84
125
 
85
126
  private
86
127
 
128
+ # Mark the active session errored/crashed based on the captured event's
129
+ # mechanism. An at_exit/unhandled event (handled=false) is a crash;
130
+ # everything else is a handled error. Fail-open — never raises.
131
+ def mark_session_for(kw)
132
+ tracker = @session_tracker
133
+ return unless tracker
134
+ meta = kw[:metadata]
135
+ handled_false =
136
+ meta.is_a?(Hash) &&
137
+ (meta["handled"] == false || meta[:handled] == false)
138
+ handled_false ? tracker.record_crash : tracker.record_error
139
+ rescue StandardError
140
+ nil
141
+ end
142
+
87
143
  # Fold the persistent tags + contexts into any explicit metadata caller
88
144
  # passed. Caller-supplied keys win on conflict.
89
145
  def merge_default_metadata(kw)