rails_event_viewer 0.1.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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +451 -0
  5. data/Rakefile +16 -0
  6. data/app/assets/stylesheets/rails_event_viewer/application.css +32 -0
  7. data/app/assets/stylesheets/rails_event_viewer/tailwind.css +54 -0
  8. data/app/controllers/rails_event_viewer/analytics_controller.rb +78 -0
  9. data/app/controllers/rails_event_viewer/application_controller.rb +84 -0
  10. data/app/controllers/rails_event_viewer/dashboard_controller.rb +18 -0
  11. data/app/controllers/rails_event_viewer/event_types_controller.rb +16 -0
  12. data/app/controllers/rails_event_viewer/events_controller.rb +72 -0
  13. data/app/controllers/rails_event_viewer/groups_controller.rb +52 -0
  14. data/app/helpers/rails_event_viewer/application_helper.rb +163 -0
  15. data/app/models/rails_event_viewer/application_record.rb +5 -0
  16. data/app/models/rails_event_viewer/entry.rb +34 -0
  17. data/app/views/layouts/rails_event_viewer/application.html.erb +45 -0
  18. data/app/views/rails_event_viewer/analytics/overview.html.erb +134 -0
  19. data/app/views/rails_event_viewer/dashboard/index.html.erb +113 -0
  20. data/app/views/rails_event_viewer/event_types/index.html.erb +59 -0
  21. data/app/views/rails_event_viewer/event_types/show.html.erb +102 -0
  22. data/app/views/rails_event_viewer/events/_event_row.html.erb +42 -0
  23. data/app/views/rails_event_viewer/events/_filters_sidebar.html.erb +66 -0
  24. data/app/views/rails_event_viewer/events/index.html.erb +65 -0
  25. data/app/views/rails_event_viewer/events/show.html.erb +155 -0
  26. data/app/views/rails_event_viewer/groups/index.html.erb +115 -0
  27. data/app/views/rails_event_viewer/groups/show.html.erb +134 -0
  28. data/app/views/rails_event_viewer/shared/_navigation.html.erb +47 -0
  29. data/app/views/rails_event_viewer/shared/_pagination.html.erb +60 -0
  30. data/app/views/rails_event_viewer/shared/_stat_card.html.erb +36 -0
  31. data/config/routes.rb +18 -0
  32. data/db/migrate/20241208000001_create_rails_event_viewer_entries.rb +36 -0
  33. data/lib/generators/rails_event_viewer/install/USAGE +33 -0
  34. data/lib/generators/rails_event_viewer/install/install_generator.rb +86 -0
  35. data/lib/generators/rails_event_viewer/install/templates/create_rails_event_viewer_entries.rb.erb +35 -0
  36. data/lib/generators/rails_event_viewer/install/templates/initializer.rb.erb +90 -0
  37. data/lib/rails_event_viewer/adapter.rb +93 -0
  38. data/lib/rails_event_viewer/adapters/active_record.rb +227 -0
  39. data/lib/rails_event_viewer/adapters/concerns/in_memory_filtering.rb +136 -0
  40. data/lib/rails_event_viewer/adapters/memory.rb +128 -0
  41. data/lib/rails_event_viewer/adapters/null.rb +63 -0
  42. data/lib/rails_event_viewer/adapters/redis.rb +213 -0
  43. data/lib/rails_event_viewer/engine.rb +24 -0
  44. data/lib/rails_event_viewer/events_relation.rb +130 -0
  45. data/lib/rails_event_viewer/json_query.rb +111 -0
  46. data/lib/rails_event_viewer/pagy_countable.rb +31 -0
  47. data/lib/rails_event_viewer/subscriber.rb +255 -0
  48. data/lib/rails_event_viewer/time_utils.rb +24 -0
  49. data/lib/rails_event_viewer/version.rb +3 -0
  50. data/lib/rails_event_viewer.rb +122 -0
  51. data/lib/tasks/rails_event_viewer_tasks.rake +81 -0
  52. metadata +151 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 323623721d960260a7ea2077cf30edc17c4708b7c8d96ab5d9fc7320b45f8113
4
+ data.tar.gz: 7afbd0a2835c53520bbbb8d1f323b1643297bc8e641948c614ddc407d6791edd
5
+ SHA512:
6
+ metadata.gz: f28f9dff6c4085b5c319744f19c71ae1f373dede9add4a8f5a0bda335bb088ec283e353c151cd28eb5ae71ba2d9b4a6f8c13f45bb7a03d62b687ba706b405ff9
7
+ data.tar.gz: 544807b6dbee7af798bce690b6b51e0ada6f168b7681d867d2b57ce634aa3995b1878ba3f3e3dbe3976d6ce55edf9bb6f069f1b4b2722988154b97b86164103c
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-09-29)
4
+
5
+ Initial release.
6
+
7
+ - Web dashboard for browsing, searching, and analyzing events emitted via `Rails.event` (Rails 8.1+).
8
+ - Storage adapters: ActiveRecord (default), Redis, Memory, Null, or a custom class.
9
+ - Buffered async ingestion with configurable buffer size, flush interval, and sampling.
10
+ - Event filtering with `captured_events` and `ignored_events`.
11
+ - Event groups by context or tag keys, such as `request_id`.
12
+ - Analytics: events over time and counts by event name.
13
+ - HTTP Basic Auth or custom authentication. The dashboard returns 403 outside development and test when neither is configured, and when Basic Auth is enabled with a blank user or password.
14
+ - Rake tasks: `stats`, `cleanup`, `flush`, `clear`, `install:migrations`.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Keshav Biswa
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # RailsEventViewer
2
+
3
+ A Rails engine for viewing and analyzing events emitted via `Rails.event`.
4
+ Provides a web UI dashboard for browsing, filtering, and visualizing domain events in your Rails application.
5
+
6
+ ## Features
7
+
8
+ - Web-based dashboard for viewing events
9
+ - Flexible storage adapters (ActiveRecord, Redis, Memory, or custom)
10
+ - High-performance buffered ingestion with configurable batch writes
11
+ - Fluent query interface for programmatic access
12
+ - Event filtering by name, tags, and time range
13
+ - Full-text search across event names and payloads
14
+ - Analytics with event counts over time
15
+ - Configurable retention and automatic cleanup
16
+ - HTTP Basic Auth or custom authentication
17
+
18
+ ## Requirements
19
+
20
+ - Rails 8.1+
21
+ - Ruby 3.2+
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem "rails_event_viewer"
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ ```bash
34
+ bundle install
35
+ ```
36
+
37
+ Run the install generator:
38
+
39
+ ```bash
40
+ rails generate rails_event_viewer:install
41
+ ```
42
+
43
+ This will:
44
+ - Create a migration for the events table (if using ActiveRecord adapter)
45
+ - Create an initializer with configuration options
46
+ - Mount the engine at `/events`
47
+
48
+ Run the migration:
49
+
50
+ ```bash
51
+ rails db:migrate
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ Configure RailsEventViewer in `config/initializers/rails_event_viewer.rb`:
57
+
58
+ ```ruby
59
+ RailsEventViewer.configure do |config|
60
+ # Storage adapter: :active_record, :redis, :memory, :null, or a custom class
61
+ config.storage_adapter = :active_record
62
+
63
+ # Adapter-specific options
64
+ config.adapter_options = {}
65
+
66
+ # Performance settings
67
+ config.async = true # Enable buffered writes
68
+ config.buffer_size = 100 # Flush after N events
69
+ config.flush_interval = 2 # Flush every N seconds
70
+ config.sample_rate = 1.0 # 1.0 = capture all, 0.1 = capture 10%
71
+
72
+ # Retention
73
+ config.retention_period = 7.days
74
+
75
+ # Filtering
76
+ config.captured_events = [] # Empty = capture all
77
+ config.ignored_events = [] # Events to never capture
78
+
79
+ # UI settings
80
+ config.per_page = 25
81
+
82
+ # Authentication (choose one)
83
+ config.http_basic_auth_enabled = true
84
+ config.http_basic_auth_user = ENV["RAILS_EVENT_VIEWER_USER"]
85
+ config.http_basic_auth_password = ENV["RAILS_EVENT_VIEWER_PASSWORD"]
86
+
87
+ # Or use custom authentication
88
+ config.authentication = ->(controller) {
89
+ controller.authenticate_user!
90
+ controller.current_user.admin?
91
+ }
92
+ end
93
+ ```
94
+
95
+ ## Storage Adapters
96
+
97
+ ### ActiveRecord (default)
98
+
99
+ Stores events in your database. Best for most applications with full analytics support.
100
+
101
+ ```ruby
102
+ config.storage_adapter = :active_record
103
+ ```
104
+
105
+ The install generator creates the migration for you. If you skipped the generator, copy the migration from the engine instead. Do not do both, or you will get two migrations for the same table.
106
+
107
+ ```bash
108
+ rails rails_event_viewer:install:migrations
109
+ rails db:migrate
110
+ ```
111
+
112
+ ### Redis
113
+
114
+ Stores events in Redis sorted sets. Good for high-volume, ephemeral storage where you don't need long-term persistence.
115
+
116
+ First, add the redis gem to your Gemfile:
117
+
118
+ ```ruby
119
+ gem "redis"
120
+ ```
121
+
122
+ Then configure the adapter:
123
+
124
+ ```ruby
125
+ config.storage_adapter = :redis
126
+ config.adapter_options = {
127
+ redis_options: { url: ENV.fetch("REDIS_URL", "redis://localhost:6379") },
128
+ pool_size: 5,
129
+ key_prefix: "rails_event_viewer",
130
+ max_events: 10_000
131
+ }
132
+ ```
133
+
134
+ Options:
135
+ - `redis_options` - Hash passed to `Redis.new` whenever the pool opens a new connection (default: `{}`)
136
+ - `pool_size` - Number of pooled Redis connections, should match your app's thread count (default: `5`)
137
+ - `pool_timeout` - Seconds to wait for a free connection before raising (default: `5`)
138
+ - `key_prefix` - Prefix for Redis keys (default: `"rails_event_viewer"`). The adapter wraps it in braces, so keys look like `{rails_event_viewer}:events`. The braces are a Redis Cluster hash tag that keeps every key on one node, which the adapter's multi-key commands require. Do not add braces yourself.
139
+ - `max_events` - Maximum events to retain, older events are automatically trimmed (default: `10_000`)
140
+
141
+ The adapter uses a connection pool rather than a single shared connection, since one Redis connection is not safe to use concurrently across multiple threads. Each pooled connection is built independently from `redis_options`. Pass `pool:` with your own `ConnectionPool` for full control.
142
+
143
+ Note: Redis adapter performs analytics by fetching events into memory, which may be slower than ActiveRecord for large datasets.
144
+
145
+ Filtered queries and analytics scan every stored event. Keep `max_events` modest, around 10,000, if you filter often.
146
+
147
+ ### Memory
148
+
149
+ In-memory storage for development and testing.
150
+
151
+ ```ruby
152
+ config.storage_adapter = :memory
153
+ config.adapter_options = { max_events: 1000 }
154
+ ```
155
+
156
+ Options:
157
+ - `max_events` - Maximum events to retain (default: `1000`)
158
+
159
+ Note: Events are lost when the server process restarts. Not recommended for production use.
160
+
161
+ ### Null
162
+
163
+ Discards all events. Useful for disabling event capture in specific environments.
164
+
165
+ ```ruby
166
+ config.storage_adapter = :null
167
+ ```
168
+
169
+ ### Switching Adapters via Environment Variable
170
+
171
+ A common pattern is to use different adapters per environment:
172
+
173
+ ```ruby
174
+ RailsEventViewer.configure do |config|
175
+ adapter = ENV.fetch("EVENT_STORAGE", "active_record").to_sym
176
+ config.storage_adapter = adapter
177
+
178
+ case adapter
179
+ when :redis
180
+ config.adapter_options = {
181
+ redis_options: { url: ENV.fetch("REDIS_URL", "redis://localhost:6379") },
182
+ max_events: 10_000
183
+ }
184
+ when :memory
185
+ config.adapter_options = { max_events: 1000 }
186
+ end
187
+ end
188
+ ```
189
+
190
+ Then switch adapters when starting your server:
191
+
192
+ ```bash
193
+ # Use ActiveRecord (default)
194
+ bin/rails server
195
+
196
+ # Use Redis
197
+ EVENT_STORAGE=redis bin/rails server
198
+
199
+ # Use Memory
200
+ EVENT_STORAGE=memory bin/rails server
201
+ ```
202
+
203
+ ### Custom Adapter
204
+
205
+ Create your own adapter by including the `RailsEventViewer::Adapter` module:
206
+
207
+ ```ruby
208
+ class MyCustomAdapter
209
+ include RailsEventViewer::Adapter
210
+
211
+ def write_events(events)
212
+ end
213
+
214
+ def fetch_events(relation)
215
+ end
216
+
217
+ def count_events(relation)
218
+ end
219
+
220
+ def distinct_event_names
221
+ end
222
+
223
+ def find_event(id)
224
+ end
225
+
226
+ def delete_before(timestamp)
227
+ end
228
+
229
+ def distinct_group_values(key, source: :context)
230
+ end
231
+
232
+ def group_instances(key, source: :context, limit: 100)
233
+ end
234
+
235
+ def events_over_time(since:, interval:)
236
+ # Return { time => count }
237
+ end
238
+
239
+ def counts_by_name(limit:)
240
+ # Return { name => count }, highest first
241
+ end
242
+
243
+ def count_since(since)
244
+ # Return count of events since a time
245
+ end
246
+ end
247
+
248
+ config.storage_adapter = MyCustomAdapter
249
+ ```
250
+
251
+ ## Emitting Events
252
+
253
+ RailsEventViewer automatically subscribes to `Rails.event`. Emit events using the standard Rails API:
254
+
255
+ ```ruby
256
+ Rails.event.notify("user.created", user_id: user.id, email: user.email)
257
+
258
+ Rails.event.notify(
259
+ "order.placed",
260
+ order_id: order.id,
261
+ total: order.total,
262
+ tags: { environment: Rails.env, priority: "high" }
263
+ )
264
+ ```
265
+
266
+ ## Programmatic Access
267
+
268
+ Use the fluent query interface to access events:
269
+
270
+ ```ruby
271
+ # Get all events
272
+ RailsEventViewer.events.each { |e| puts e[:name] }
273
+
274
+ # Filter by name
275
+ RailsEventViewer.events.with_name("user.created").to_a
276
+
277
+ # Filter by tag
278
+ RailsEventViewer.events.with_tag(:environment, "production").to_a
279
+
280
+ # Time range
281
+ RailsEventViewer.events.since(1.hour.ago).until(30.minutes.ago).to_a
282
+
283
+ # Search
284
+ RailsEventViewer.events.search("user@example.com").to_a
285
+
286
+ # Combine filters
287
+ RailsEventViewer.events
288
+ .with_name("order.placed")
289
+ .with_tag(:priority, "high")
290
+ .since(1.day.ago)
291
+ .limit(50)
292
+ .each { |e| process(e) }
293
+
294
+ # Count
295
+ RailsEventViewer.events.with_name("user.created").count
296
+
297
+ # Pagination
298
+ RailsEventViewer.events.offset(25).limit(25).to_a
299
+ ```
300
+
301
+ ## Rake Tasks
302
+
303
+ ```bash
304
+ # Show statistics
305
+ bin/rails rails_event_viewer:stats
306
+
307
+ # Clean up old events (based on retention_period)
308
+ bin/rails rails_event_viewer:cleanup
309
+
310
+ # Flush buffered events
311
+ bin/rails rails_event_viewer:flush
312
+
313
+ # Clear all events (interactive confirmation required)
314
+ bin/rails rails_event_viewer:clear
315
+ ```
316
+
317
+ For automatic cleanup, add to your scheduler:
318
+
319
+ ```yaml
320
+ # config/recurring.yml (Solid Queue)
321
+ production:
322
+ rails_event_viewer_cleanup:
323
+ command: "RailsEventViewer.adapter.delete_before(RailsEventViewer.retention_period.ago)"
324
+ schedule: every day at 3am
325
+ ```
326
+
327
+ Or using the whenever gem:
328
+
329
+ ```ruby
330
+ # config/schedule.rb (whenever gem)
331
+ every 1.day, at: "3:00 am" do
332
+ rake "rails_event_viewer:cleanup"
333
+ end
334
+ ```
335
+
336
+ ## Event Filtering
337
+
338
+ Control which events are captured:
339
+
340
+ ```ruby
341
+ # Only capture specific events
342
+ config.captured_events = ["user.created", "order.placed"]
343
+
344
+ # Or use regex patterns
345
+ config.captured_events = [/^user\./, /^order\./]
346
+
347
+ # Ignore specific events
348
+ config.ignored_events = ["internal.heartbeat", /^debug\./]
349
+ ```
350
+
351
+ ## Sampling
352
+
353
+ For high-volume applications, reduce storage by sampling if needed:
354
+
355
+ ```ruby
356
+ # Capture only 10% of events
357
+ config.sample_rate = 0.1
358
+ ```
359
+
360
+ ## Authentication
361
+
362
+ If neither option below is configured, the dashboard is open in development and test, and returns `403 Forbidden` in every other environment, including staging. Enabling HTTP Basic Auth with a blank user or password also returns `403 Forbidden`.
363
+
364
+ Set `RAILS_ENV` on every deployed server. If it is missing, Rails falls back to development and the dashboard is open.
365
+
366
+ ### HTTP Basic Auth
367
+
368
+ ```ruby
369
+ config.http_basic_auth_enabled = true
370
+ config.http_basic_auth_user = ENV["RAILS_EVENT_VIEWER_USER"]
371
+ config.http_basic_auth_password = ENV["RAILS_EVENT_VIEWER_PASSWORD"]
372
+ ```
373
+
374
+ ### Custom Authentication
375
+
376
+ ```ruby
377
+ # Devise example
378
+ config.authentication = ->(controller) {
379
+ controller.authenticate_user!
380
+ controller.current_user.admin?
381
+ }
382
+ ```
383
+
384
+ ## Routes
385
+
386
+ The engine is mounted at the path specified in your routes (default `/events`):
387
+
388
+ ```ruby
389
+ # config/routes.rb
390
+ mount RailsEventViewer::Engine, at: "/events"
391
+ ```
392
+
393
+ Available routes:
394
+ - `/events` - Dashboard
395
+ - `/events/events` - Event list with filtering
396
+ - `/events/events/:id` - Single event detail
397
+ - `/events/analytics/overview` - Analytics dashboard
398
+ - `/events/groups` - Event groups
399
+
400
+ ## How Event Capture Works
401
+
402
+ RailsEventViewer subscribes to `Rails.event` (the Structured Event Reporter introduced in Rails 8.1),
403
+ which is built on top of `ActiveSupport::Notifications`. This means internal Rails instrumentation
404
+ events (`sql.active_record`, `process_action.action_controller`, etc.) are received by the subscriber.
405
+
406
+ By default, all internal Rails framework events are excluded via the built-in `ignored_events` patterns.
407
+ This prevents noise from flooding your storage.
408
+
409
+ **Captured by default:**
410
+ - Application events emitted via `Rails.event.notify("my.event", ...)`
411
+
412
+ **Ignored by default** (via `ignored_events`):
413
+ - `active_record.*`, `action_controller.*`, `action_view.*`, `active_job.*`
414
+ - `action_mailer.*`, `active_storage.*`, `action_cable.*`, `rails.*`
415
+ - `turbo.*`, `cache_*`, `*.sql`, `solid_*`
416
+
417
+ To capture internal Rails events, remove the relevant pattern from `ignored_events`:
418
+
419
+ ```ruby
420
+ RailsEventViewer.configure do |config|
421
+ config.ignored_events = RailsEventViewer.ignored_events.reject { |p| p == /^active_record\./ }
422
+ end
423
+ ```
424
+
425
+ ## Development
426
+
427
+ After checking out the repo, run:
428
+
429
+ ```bash
430
+ cd test/dummy
431
+ bin/rails db:migrate
432
+ bin/rails server
433
+ ```
434
+
435
+ Run the test suite:
436
+
437
+ ```bash
438
+ bundle exec rake test
439
+ ```
440
+
441
+ ## Contributing
442
+
443
+ 1. Fork it
444
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
445
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
446
+ 4. Push to the branch (`git push origin feature/my-new-feature`)
447
+ 5. Create a Pull Request
448
+
449
+ ## License
450
+
451
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
7
+
8
+ require "rake/testtask"
9
+
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << "test"
12
+ t.pattern = "test/**/*_test.rb"
13
+ t.verbose = false
14
+ end
15
+
16
+ task default: :test
@@ -0,0 +1,32 @@
1
+ /* RailsEventViewer Application Styles */
2
+ /* Note: Tailwind CSS is loaded via CDN in development. For production, compile using:
3
+ npx tailwindcss -i ./app/assets/stylesheets/rails_event_viewer/tailwind.css \
4
+ -o ./app/assets/stylesheets/rails_event_viewer/application.css --minify
5
+ */
6
+
7
+ /* Custom styles that don't require Tailwind compilation */
8
+ .json-tree {
9
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
10
+ font-size: 0.875rem;
11
+ }
12
+
13
+ .json-tree-key {
14
+ color: #4b5563;
15
+ font-weight: 500;
16
+ }
17
+
18
+ .json-tree-string {
19
+ color: #059669;
20
+ }
21
+
22
+ .json-tree-number {
23
+ color: #2563eb;
24
+ }
25
+
26
+ .json-tree-boolean {
27
+ color: #7c3aed;
28
+ }
29
+
30
+ .json-tree-null {
31
+ color: #9ca3af;
32
+ }
@@ -0,0 +1,54 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom component styles */
6
+ @layer components {
7
+ .btn {
8
+ @apply px-4 py-2 rounded-md text-sm font-medium transition-colors;
9
+ }
10
+
11
+ .btn-primary {
12
+ @apply bg-indigo-600 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
13
+ }
14
+
15
+ .btn-secondary {
16
+ @apply bg-white text-gray-700 border border-gray-300 hover:bg-gray-50;
17
+ }
18
+
19
+ .card {
20
+ @apply bg-white rounded-lg shadow;
21
+ }
22
+
23
+ .input {
24
+ @apply block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm;
25
+ }
26
+
27
+ .label {
28
+ @apply block text-sm font-medium text-gray-700;
29
+ }
30
+
31
+ .badge {
32
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
33
+ }
34
+
35
+ .badge-primary {
36
+ @apply bg-indigo-100 text-indigo-800;
37
+ }
38
+
39
+ .badge-gray {
40
+ @apply bg-gray-100 text-gray-800;
41
+ }
42
+
43
+ .badge-green {
44
+ @apply bg-green-100 text-green-800;
45
+ }
46
+
47
+ .table-header {
48
+ @apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
49
+ }
50
+
51
+ .table-cell {
52
+ @apply px-6 py-4 whitespace-nowrap text-sm;
53
+ }
54
+ }
@@ -0,0 +1,78 @@
1
+ module RailsEventViewer
2
+ class AnalyticsController < ApplicationController
3
+ before_action :check_analytics_support
4
+
5
+ def overview
6
+ @date_range = parse_date_range
7
+
8
+ relation = RailsEventViewer.events
9
+ .since(@date_range.begin)
10
+ .until(@date_range.end)
11
+
12
+ @total_events = relation.count
13
+ @unique_event_types = current_adapter.distinct_event_names.size
14
+ @events_by_type = current_adapter.counts_by_name(limit: 20)
15
+ @events_over_time = current_adapter.events_over_time(since: @date_range.begin, interval: period_for_range)
16
+
17
+ @avg_events_per_hour = calculate_avg_events_per_hour
18
+ @peak_period = find_peak_period
19
+ end
20
+
21
+ def events_over_time
22
+ since = params[:since]&.to_i&.seconds&.ago || 24.hours.ago
23
+ interval = params[:interval]&.to_sym || :hour
24
+
25
+ @events_data = current_adapter.events_over_time(since: since, interval: interval)
26
+
27
+ respond_to do |format|
28
+ format.json { render json: @events_data }
29
+ format.html { render partial: "events_over_time_chart" }
30
+ end
31
+ end
32
+
33
+ def events_by_type
34
+ limit = (params[:limit] || 20).to_i.clamp(1, 100)
35
+ @data = current_adapter.counts_by_name(limit: limit)
36
+
37
+ respond_to do |format|
38
+ format.json { render json: @data }
39
+ format.html { render partial: "events_by_type_chart" }
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def check_analytics_support
46
+ unless analytics_supported?
47
+ redirect_to root_path, alert: "Analytics not supported by current storage adapter"
48
+ end
49
+ end
50
+
51
+ def parse_date_range
52
+ start_date = safe_parse_date(params[:start_date], default: 7.days.ago.to_date)
53
+ end_date = safe_parse_date(params[:end_date], default: Date.current)
54
+
55
+ start_date, end_date = end_date, start_date if start_date > end_date
56
+
57
+ start_date.beginning_of_day..end_date.end_of_day
58
+ end
59
+
60
+ def period_for_range
61
+ duration = (@date_range.end - @date_range.begin).to_i
62
+ duration <= 604800 ? :hour : :day
63
+ end
64
+
65
+ def calculate_avg_events_per_hour
66
+ hours = ((@date_range.end - @date_range.begin) / 3600.0).round(1)
67
+ return 0 if hours.zero?
68
+
69
+ (@total_events.to_f / hours).round(1)
70
+ end
71
+
72
+ def find_peak_period
73
+ return nil if @events_over_time.empty?
74
+
75
+ @events_over_time.max_by { |_, count| count }
76
+ end
77
+ end
78
+ end