findbug 0.4.0 → 0.5.0

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: ca9a3e20d87733caff1807d5b89935d349a316cddf15ee2e7565f3b861cafc10
4
- data.tar.gz: d135aae0a004b454448d2688e04cf22f6121ed3d64b743baf7663466ef5894b0
3
+ metadata.gz: bc983a190a0ac08221e7bbaf108e52d55ecc20a28712f5bdf380acf61a27e43d
4
+ data.tar.gz: 77bc4565c0686c4d60646b4e7d9dc65f4faae482c83d8df6b98a4c02f540c162
5
5
  SHA512:
6
- metadata.gz: 00d61fb0d96254b5195951d9c55a0c459f896ad61b3f813ed1fbeb817856b186ee8ceac24d5d659934e3909cfaa9bb901f38cd3fc3bc68e8aa5df4d7e04c3627
7
- data.tar.gz: 01c241d176885ea96d700623048bf1f7d8af8832057b722b6d23c8a079dedc61410322abb897313a4e7a3d937a0ce6197c248b6438197725ab6656333f1dc311
6
+ metadata.gz: c6d0d26042c62b4ea65bd5f574e6c43d588fdfa83914b416a44638dae85c97d3fa12d74d4ffa6bb08b8575d96daa11ed87aeaefc314ee319353cb4acf961a0d6
7
+ data.tar.gz: 3d8ef1a2aece7c33efb7b629aa5b19013fe96da2b6ec3ec741eba8447b2fa80b36a5c3b6f1be926c4f19cda0ff44388eebc0a6eede1ce42d4e5fe199b448a5b2
data/README.md CHANGED
@@ -8,16 +8,20 @@
8
8
 
9
9
  FindBug provides Sentry-like functionality with all data stored on your own infrastructure using Redis and your database. Zero external dependencies, full data ownership.
10
10
 
11
+ 📚 **Full documentation:** [findbug.dev/docs](https://findbug.dev/docs)
12
+
11
13
  ## Features
12
14
 
13
15
  - **Error Tracking** - Capture exceptions with full context, stack traces, and request data
14
16
  - **Performance Monitoring** - Track request timing, SQL queries, and automatic N+1 detection
15
- - **Self-Hosted** - All data stays on your infrastructure (Redis + PostgreSQL/MySQL)
17
+ - **Self-Hosted** - All data stays on your infrastructure (Redis + your existing database)
18
+ - **Database-Agnostic** - Works on PostgreSQL, MySQL, and SQLite — adapter detected automatically
16
19
  - **Zero Performance Impact** - Async writes via Redis buffer, never blocks your requests
17
20
  - **Built-in Dashboard** - Beautiful web UI for viewing errors and performance metrics
18
21
  - **Multi-channel Alerts** - Email, Slack, Discord, and custom webhooks
19
22
  - **Works Out of the Box** - Built-in background persister, no job scheduler required
20
23
  - **Rails 7+ Native** - Designed for modern Rails applications
24
+ - **Tested** - Comprehensive RSpec suite covering every adapter path
21
25
 
22
26
  ## Why FindBug?
23
27
 
@@ -34,7 +38,7 @@ FindBug provides Sentry-like functionality with all data stored on your own infr
34
38
  - Ruby 3.1+
35
39
  - Rails 7.0+
36
40
  - Redis 4.0+
37
- - PostgreSQL or MySQL
41
+ - A relational database — PostgreSQL, MySQL, or SQLite
38
42
 
39
43
  ## Installation
40
44
 
@@ -240,7 +244,7 @@ end
240
244
  │ └────────┬─────────┘ │
241
245
  │ │ │
242
246
  │ ▼ │
243
- │ Dashboard ◄──────────────────── Database (PostgreSQL/MySQL)
247
+ │ Dashboard ◄──────────────────── Database (PostgreSQL/MySQL/SQLite)
244
248
  │ (/findbug) │
245
249
  │ │
246
250
  └─────────────────────────────────────────────────────────────────┘
@@ -274,11 +278,44 @@ rails findbug:clear_buffers
274
278
  rails findbug:db:stats
275
279
  ```
276
280
 
277
- ## Multi-Tenant Applications (Apartment/ros-apartment)
281
+ ## Database Support
282
+
283
+ As of v0.5.0 FindBug is database-agnostic. The install generator's migrations and the model layer detect your `ActiveRecord::Base.connection.adapter_name` at runtime and pick the right column types and SQL functions automatically.
284
+
285
+ | Adapter | JSON column | Time bucketing SQL |
286
+ |--------------------|-------------|------------------------------------|
287
+ | PostgreSQL/PostGIS | `jsonb` | `date_trunc(...)` |
288
+ | MySQL/Mysql2 | `json` | `DATE_FORMAT(...)` / `DATE(...)` |
289
+ | SQLite | `text` (with JSON serialisation in the model) | `strftime(...)` / `DATE(...)` |
290
+
291
+ The JSON accessors on the models normalise reads to a native Ruby `Hash` / `Array` regardless of the underlying column type, so your application code is identical across adapters.
292
+
293
+ If you're writing your own migrations against the same multi-DB strategy, the `Findbug::AdapterHelper` module is part of the public API:
294
+
295
+ ```ruby
296
+ Findbug::AdapterHelper.json_column_type # :jsonb / :json / :text
297
+ Findbug::AdapterHelper.json_default({}) # adapter-appropriate default
298
+ Findbug::AdapterHelper.date_trunc_sql("hour", "captured_at")
299
+ ```
300
+
301
+ ## Multi-Tenant Applications
302
+
303
+ Multi-tenant Rails apps are supported, but the setup depends on *how* your app isolates tenants. The matrix below shows what's possible per adapter:
304
+
305
+ | Adapter | Tenancy model | FindBug support |
306
+ |--------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------|
307
+ | PostgreSQL | Schema-per-tenant (Apartment's default) | ✅ First-class — keep FindBug tables in the `public` schema (see below). |
308
+ | PostgreSQL/MySQL/SQLite | Row-level (`tenant_id` column) | ✅ Nothing special — FindBug's tables aren't tenant-scoped. |
309
+ | MySQL | Database-per-tenant (Apartment with `use_schemas = false`) | ⚠️ Doable but awkward — point FindBug at a separate connection via `connects_to`. |
310
+ | SQLite | File-per-tenant | ❌ Not practical — use row-level scoping instead. |
311
+
312
+ For the full discussion (including MySQL setup options), see the [Multi-tenant section in the docs](https://findbug.dev/docs#multi-tenant).
313
+
314
+ ### PostgreSQL + Apartment (schema-per-tenant)
278
315
 
279
- If you're using [ros-apartment](https://github.com/rails-on-services/apartment) or similar multi-tenant gems with PostgreSQL schemas, FindBug's tables need to stay in the public schema and the dashboard path should be excluded from tenant switching.
316
+ If you're using [ros-apartment](https://github.com/rails-on-services/apartment) with PostgreSQL schemas, FindBug's tables need to stay in the public schema and the dashboard path should be excluded from tenant switching.
280
317
 
281
- ### 1. Exclude FindBug Models
318
+ #### 1. Exclude FindBug Models
282
319
 
283
320
  Add FindBug models to the `excluded_models` list in `config/initializers/apartment.rb`:
284
321
 
@@ -293,7 +330,7 @@ Apartment.configure do |config|
293
330
  end
294
331
  ```
295
332
 
296
- ### 2. Exclude FindBug Dashboard Path
333
+ #### 2. Exclude FindBug Dashboard Path
297
334
 
298
335
  Add `/findbug` to your tenant switching middleware's excluded paths:
299
336
 
@@ -317,7 +354,7 @@ class SwitchTenantMiddleware < Apartment::Elevators::Generic
317
354
  end
318
355
  ```
319
356
 
320
- ### 3. Run Migrations in Public Schema
357
+ #### 3. Run Migrations in Public Schema
321
358
 
322
359
  Ensure FindBug migrations run in the public schema:
323
360
 
@@ -27,14 +27,15 @@ module Findbug
27
27
  # t.timestamps
28
28
  # end
29
29
  #
30
- # WHY JSONB FOR CONTEXT?
31
- # ======================
30
+ # WHY OVERRIDE JSON ACCESSORS?
31
+ # =============================
32
32
  #
33
- # Context is semi-structured - different errors have different context.
34
- # JSONB (in PostgreSQL) or JSON (in other DBs) lets us store any shape
35
- # of data without schema migrations.
33
+ # The column type for context/request_data varies by adapter:
34
+ # PostgreSQL jsonb (AR returns Hash natively)
35
+ # MySQL → json (AR returns Hash natively)
36
+ # SQLite → text (AR returns raw JSON String)
36
37
  #
37
- # For querying, we create GIN indexes on commonly queried paths.
38
+ # The overrides below normalise both cases so callers always get a Hash.
38
39
  #
39
40
  class ErrorEvent < ActiveRecord::Base
40
41
  self.table_name = "findbug_error_events"
@@ -55,6 +56,33 @@ module Findbug
55
56
  validates :status, inclusion: { in: [STATUS_UNRESOLVED, STATUS_RESOLVED, STATUS_IGNORED] }
56
57
  validates :severity, inclusion: { in: [SEVERITY_ERROR, SEVERITY_WARNING, SEVERITY_INFO] }
57
58
 
59
+ # JSON field accessors — normalise across adapters (jsonb/json/text).
60
+ # Reader always returns a Hash; writer stores a JSON string on text columns
61
+ # and the native object on json/jsonb columns.
62
+ %i[context request_data].each do |field|
63
+ define_method(field) do
64
+ val = read_attribute(field)
65
+ return {} if val.nil?
66
+ val.is_a?(String) ? JSON.parse(val) : val
67
+ rescue JSON::ParserError
68
+ {}
69
+ end
70
+
71
+ define_method(:"#{field}=") do |val|
72
+ col_type = self.class.columns_hash[field.to_s]&.type
73
+ if col_type == :text
74
+ serialized = case val
75
+ when nil then nil
76
+ when String then val
77
+ else val.to_json
78
+ end
79
+ write_attribute(field, serialized)
80
+ else
81
+ write_attribute(field, val)
82
+ end
83
+ end
84
+ end
85
+
58
86
  # Scopes
59
87
  scope :unresolved, -> { where(status: STATUS_UNRESOLVED) }
60
88
  scope :resolved, -> { where(status: STATUS_RESOLVED) }
@@ -17,11 +17,11 @@ module Findbug
17
17
  # t.float :db_time_ms, default: 0
18
18
  # t.float :view_time_ms, default: 0
19
19
  # t.integer :query_count, default: 0
20
- # t.jsonb :slow_queries, default: []
21
- # t.jsonb :n_plus_one_queries, default: []
20
+ # t.column :slow_queries, <json_type>, default: []
21
+ # t.column :n_plus_one_queries, <json_type>, default: []
22
22
  # t.boolean :has_n_plus_one, default: false
23
23
  # t.integer :view_count, default: 0
24
- # t.jsonb :context, default: {}
24
+ # t.column :context, <json_type>, default: {}
25
25
  # t.string :environment
26
26
  # t.string :release_version
27
27
  # t.datetime :captured_at
@@ -48,6 +48,33 @@ module Findbug
48
48
  TYPE_CUSTOM = "custom"
49
49
  TYPE_JOB = "job"
50
50
 
51
+ # JSON field accessors — normalise across adapters (jsonb/json/text).
52
+ # Reader returns the native Array/Hash; writer serialises to JSON string
53
+ # for text columns and passes through to AR's type system otherwise.
54
+ { slow_queries: [], n_plus_one_queries: [], context: {} }.each do |field, empty|
55
+ define_method(field) do
56
+ val = read_attribute(field)
57
+ return empty.dup if val.nil?
58
+ val.is_a?(String) ? JSON.parse(val) : val
59
+ rescue JSON::ParserError
60
+ empty.dup
61
+ end
62
+
63
+ define_method(:"#{field}=") do |val|
64
+ col_type = self.class.columns_hash[field.to_s]&.type
65
+ if col_type == :text
66
+ serialized = case val
67
+ when nil then nil
68
+ when String then val
69
+ else val.to_json
70
+ end
71
+ write_attribute(field, serialized)
72
+ else
73
+ write_attribute(field, val)
74
+ end
75
+ end
76
+ end
77
+
51
78
  # Validations
52
79
  validates :transaction_name, presence: true
53
80
  validates :duration_ms, presence: true, numericality: { greater_than_or_equal_to: 0 }
@@ -181,28 +208,23 @@ module Findbug
181
208
  # @return [Array<Hash>] time series data
182
209
  #
183
210
  def self.throughput_over_time(since: 24.hours.ago, interval: "hour")
184
- # This uses database-specific date truncation
185
- # Works with PostgreSQL; adjust for other databases
186
- time_column = case interval
187
- when "minute" then "date_trunc('minute', captured_at)"
188
- when "hour" then "date_trunc('hour', captured_at)"
189
- when "day" then "date_trunc('day', captured_at)"
190
- else "date_trunc('hour', captured_at)"
191
- end
211
+ time_sql = Findbug::AdapterHelper.date_trunc_sql(interval, "captured_at")
192
212
 
193
213
  where("captured_at >= ?", since)
194
- .group(Arel.sql(time_column))
214
+ .group(Arel.sql(time_sql))
195
215
  .select(
196
- Arel.sql("#{time_column} as time_bucket"),
216
+ Arel.sql("#{time_sql} as time_bucket"),
197
217
  "COUNT(*) as request_count",
198
218
  "AVG(duration_ms) as avg_duration"
199
219
  )
200
- .order(Arel.sql(time_column))
220
+ .order(Arel.sql(time_sql))
201
221
  .map do |row|
222
+ time = row.time_bucket
223
+ time = Time.parse(time.to_s) unless time.respond_to?(:strftime)
202
224
  {
203
- time: row.time_bucket,
225
+ time: time,
204
226
  count: row.request_count,
205
- avg_duration_ms: row.avg_duration.round(2)
227
+ avg_duration_ms: row.avg_duration&.round(2) || 0
206
228
  }
207
229
  end
208
230
  end