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
@@ -0,0 +1,115 @@
1
+ <% content_for(:title, "Groups - Rails Event Viewer") %>
2
+
3
+ <div class="space-y-6">
4
+ <div class="flex justify-between items-center">
5
+ <div>
6
+ <h1 class="text-2xl font-bold text-gray-900">Event Groups</h1>
7
+ <p class="mt-1 text-sm text-gray-500">
8
+ Track related events grouped by context or tags
9
+ </p>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="bg-white rounded-lg shadow p-4">
14
+ <%= form_with url: rails_event_viewer.groups_path, method: :get, class: "flex items-center space-x-4" do |f| %>
15
+ <div class="flex items-center space-x-2">
16
+ <%= f.label :key, "Group by:", class: "text-sm font-medium text-gray-700" %>
17
+ <%= f.select :key,
18
+ options_for_select(@group_keys.map { |k| [k.to_s.titleize, k] }, @selected_key),
19
+ {},
20
+ class: "rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 py-2 px-3" %>
21
+ </div>
22
+
23
+ <div class="flex items-center space-x-2">
24
+ <%= f.label :source, "Source:", class: "text-sm font-medium text-gray-700" %>
25
+ <%= f.select :source,
26
+ options_for_select([["Context", :context], ["Tags", :tags]], @source),
27
+ {},
28
+ class: "rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 py-2 px-3" %>
29
+ </div>
30
+
31
+ <%= f.submit "Apply",
32
+ class: "bg-indigo-600 text-white px-4 py-2 rounded-md font-medium hover:bg-indigo-700 cursor-pointer" %>
33
+ <% end %>
34
+ </div>
35
+
36
+ <% if @group_keys.empty? %>
37
+ <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
38
+ <div class="flex">
39
+ <svg class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
40
+ <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
41
+ </svg>
42
+ <div class="ml-3">
43
+ <h3 class="text-sm font-medium text-yellow-800">No group keys configured</h3>
44
+ <p class="mt-1 text-sm text-yellow-700">
45
+ Configure group keys in your initializer:
46
+ <code class="bg-yellow-100 px-1 rounded">config.group_keys = [:request_id, :order_id]</code>
47
+ </p>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <% elsif @groups.any? %>
52
+ <div class="bg-white shadow rounded-lg overflow-hidden">
53
+ <table class="min-w-full divide-y divide-gray-200">
54
+ <thead class="bg-gray-50">
55
+ <tr>
56
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
57
+ <%= @selected_key.to_s.titleize %>
58
+ </th>
59
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Events</th>
60
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">First Event</th>
61
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Event</th>
62
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody class="bg-white divide-y divide-gray-200">
66
+ <% @groups.each do |group| %>
67
+ <% duration = group[:last_event_at] && group[:first_event_at] ? group[:last_event_at] - group[:first_event_at] : nil %>
68
+ <tr class="hover:bg-gray-50">
69
+ <td class="px-6 py-4 whitespace-nowrap">
70
+ <%= link_to rails_event_viewer.group_path(group[:value], key: @selected_key, source: @source),
71
+ class: "text-indigo-600 hover:text-indigo-900 font-medium" do %>
72
+ <code class="text-sm bg-gray-100 px-2 py-1 rounded"><%= group[:value] %></code>
73
+ <% end %>
74
+ </td>
75
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
76
+ <%= number_with_delimiter(group[:count]) %>
77
+ </td>
78
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
79
+ <% if group[:first_event_at] %>
80
+ <%= time_ago_with_title(group[:first_event_at]) %>
81
+ <% else %>
82
+ -
83
+ <% end %>
84
+ </td>
85
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
86
+ <% if group[:last_event_at] %>
87
+ <%= time_ago_with_title(group[:last_event_at]) %>
88
+ <% else %>
89
+ -
90
+ <% end %>
91
+ </td>
92
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
93
+ <% if duration %>
94
+ <%= format_duration(duration) %>
95
+ <% else %>
96
+ -
97
+ <% end %>
98
+ </td>
99
+ </tr>
100
+ <% end %>
101
+ </tbody>
102
+ </table>
103
+ </div>
104
+ <% else %>
105
+ <div class="bg-white rounded-lg shadow px-6 py-12 text-center text-gray-500">
106
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
107
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
108
+ </svg>
109
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No groups found</h3>
110
+ <p class="mt-1 text-sm text-gray-500">
111
+ No events with <code class="bg-gray-100 px-1 rounded"><%= @selected_key %></code> in their <%= @source %> data.
112
+ </p>
113
+ </div>
114
+ <% end %>
115
+ </div>
@@ -0,0 +1,134 @@
1
+ <% content_for(:title, "Group: #{@value} - Rails Event Viewer") %>
2
+
3
+ <div class="space-y-6">
4
+ <!-- Header -->
5
+ <div class="flex items-center justify-between">
6
+ <div class="flex items-center space-x-4">
7
+ <%= link_to rails_event_viewer.groups_path(key: @key, source: @source), class: "text-gray-400 hover:text-gray-600", aria: { label: "Back to groups" } do %>
8
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
10
+ </svg>
11
+ <% end %>
12
+ <div>
13
+ <p class="text-sm text-gray-500"><%= @key.to_s.titleize %></p>
14
+ <h1 class="text-2xl font-bold text-gray-900">
15
+ <code class="bg-gray-100 px-2 py-1 rounded"><%= @value %></code>
16
+ </h1>
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <!-- Stats -->
22
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-4">
23
+ <%= render "rails_event_viewer/shared/stat_card",
24
+ title: "Total Events",
25
+ value: number_with_delimiter(@total_count),
26
+ icon: "database" %>
27
+ <%= render "rails_event_viewer/shared/stat_card",
28
+ title: "Event Types",
29
+ value: @event_names.size,
30
+ icon: "tag" %>
31
+ <%= render "rails_event_viewer/shared/stat_card",
32
+ title: "Duration",
33
+ value: @duration ? format_duration(@duration) : "-",
34
+ icon: "clock" %>
35
+ <%= render "rails_event_viewer/shared/stat_card",
36
+ title: "Source",
37
+ value: @source.to_s.titleize,
38
+ icon: "calendar" %>
39
+ </div>
40
+
41
+ <!-- Timeline -->
42
+ <div class="bg-white shadow rounded-lg overflow-hidden">
43
+ <div class="px-6 py-4 border-b border-gray-200">
44
+ <h3 class="text-lg font-medium text-gray-900">Event Timeline</h3>
45
+ <% if @first_event_at && @last_event_at %>
46
+ <p class="text-sm text-gray-500">
47
+ <%= @first_event_at.strftime("%Y-%m-%d %H:%M:%S") %> to <%= @last_event_at.strftime("%Y-%m-%d %H:%M:%S") %>
48
+ </p>
49
+ <% end %>
50
+ </div>
51
+
52
+ <% if @events.any? %>
53
+ <div class="relative">
54
+ <div class="absolute left-12 top-0 bottom-0 w-0.5 bg-gray-200"></div>
55
+
56
+ <div class="divide-y divide-gray-100">
57
+ <% @events.each_with_index do |event, index| %>
58
+ <a href="<%= rails_event_viewer.event_path(event_id(event)) %>" class="relative pl-16 pr-6 py-4 hover:bg-gray-50 block">
59
+ <div class="absolute left-10 top-6 w-4 h-4 rounded-full border-2 border-indigo-500 bg-white"></div>
60
+
61
+ <% if @first_event_at && event_occurred_at(event) %>
62
+ <% offset = event_occurred_at(event) - @first_event_at %>
63
+ <div class="absolute left-0 top-4 w-9 text-right">
64
+ <span class="text-xs text-gray-400" title="<%= format_duration(offset) %> from start">
65
+ <% if offset < 0.001 %>
66
+ 0
67
+ <% elsif offset < 1 %>
68
+ +<%= (offset * 1000).round %>ms
69
+ <% elsif offset < 60 %>
70
+ +<%= offset.round(1) %>s
71
+ <% else %>
72
+ +<%= (offset / 60).round %>m
73
+ <% end %>
74
+ </span>
75
+ </div>
76
+ <% end %>
77
+
78
+ <div class="flex items-start justify-between">
79
+ <div class="flex-1 min-w-0">
80
+ <!-- Event name badge -->
81
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
82
+ <%= event_name(event) %>
83
+ </span>
84
+
85
+ <!-- Payload preview -->
86
+ <% payload = event_payload(event) %>
87
+ <% if payload.present? && payload.any? %>
88
+ <div class="mt-2">
89
+ <code class="text-xs bg-gray-100 px-2 py-1 rounded text-gray-600">
90
+ <%= truncate_json(payload, length: 100) %>
91
+ </code>
92
+ </div>
93
+ <% end %>
94
+
95
+ <!-- Tags -->
96
+ <% tags = event_tags(event) %>
97
+ <% if tags.present? && tags.any? %>
98
+ <div class="mt-2 flex flex-wrap gap-1">
99
+ <% tags.first(3).each do |key, value| %>
100
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
101
+ <%= key %>: <%= value.to_s.truncate(20) %>
102
+ </span>
103
+ <% end %>
104
+ <% if tags.size > 3 %>
105
+ <span class="text-xs text-gray-500">+<%= tags.size - 3 %> more</span>
106
+ <% end %>
107
+ </div>
108
+ <% end %>
109
+ </div>
110
+
111
+ <div class="ml-4 flex-shrink-0 text-right">
112
+ <p class="text-sm text-gray-500">
113
+ <%= time_ago_with_title(event_occurred_at(event)) %>
114
+ </p>
115
+ <% if event_source_file(event).present? %>
116
+ <p class="text-xs text-gray-400 mt-1">
117
+ <%= event_short_filepath(event) %>:<%= event_source_line(event) %>
118
+ </p>
119
+ <% end %>
120
+ </div>
121
+ </div>
122
+ </a>
123
+ <% end %>
124
+ </div>
125
+ </div>
126
+
127
+ <%= render "rails_event_viewer/shared/pagination", pagy: @pagy %>
128
+ <% else %>
129
+ <div class="px-6 py-12 text-center text-gray-500">
130
+ No events found in this group.
131
+ </div>
132
+ <% end %>
133
+ </div>
134
+ </div>
@@ -0,0 +1,47 @@
1
+ <nav class="bg-indigo-600">
2
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
3
+ <div class="flex h-16 items-center justify-between">
4
+ <div class="flex items-center">
5
+ <div class="flex-shrink-0">
6
+ <%= link_to rails_event_viewer.root_path, class: "text-white text-xl font-bold" do %>
7
+ Event Viewer
8
+ <% end %>
9
+ </div>
10
+ <div class="ml-10 flex items-baseline space-x-4">
11
+ <%= link_to "Dashboard", rails_event_viewer.root_path,
12
+ class: nav_link_class("dashboard") %>
13
+ <%= link_to "Events", rails_event_viewer.events_path,
14
+ class: nav_link_class("events") %>
15
+ <%= link_to "Event Types", rails_event_viewer.event_types_path,
16
+ class: nav_link_class("event_types") %>
17
+ <%= link_to "Groups", rails_event_viewer.groups_path,
18
+ class: nav_link_class("groups") %>
19
+ <%= link_to "Analytics", rails_event_viewer.analytics_overview_path,
20
+ class: nav_link_class("analytics") %>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="flex items-center space-x-4">
25
+ <!-- Search -->
26
+ <%= form_with url: rails_event_viewer.search_events_path, method: :get, class: "relative", data: { turbo_frame: "_top" } do |f| %>
27
+ <div class="relative">
28
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
29
+ <svg class="h-4 w-4 text-indigo-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
30
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
31
+ </svg>
32
+ </div>
33
+ <%= f.search_field :q,
34
+ value: params[:q],
35
+ placeholder: "Search events...",
36
+ class: "w-64 rounded-md bg-indigo-500 py-2 pl-10 pr-3 text-sm text-white placeholder-indigo-200 focus:bg-white focus:text-gray-900 focus:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-white" %>
37
+ </div>
38
+ <% end %>
39
+
40
+ <!-- Timezone indicator -->
41
+ <div class="text-indigo-200 text-sm" title="Current timezone">
42
+ <%= Time.zone.name %>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </nav>
@@ -0,0 +1,60 @@
1
+ <% if pagy.pages > 1 %>
2
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
3
+ <div class="flex-1 flex items-center justify-between">
4
+ <div>
5
+ <p class="text-sm text-gray-700">
6
+ Showing
7
+ <span class="font-medium"><%= pagy.from %></span>
8
+ to
9
+ <span class="font-medium"><%= pagy.to %></span>
10
+ of
11
+ <span class="font-medium"><%= pagy.count %></span>
12
+ results
13
+ </p>
14
+ </div>
15
+ <nav class="flex items-center space-x-1" aria-label="Pagination">
16
+ <% if pagy.previous %>
17
+ <%= link_to url_for(page: pagy.previous), class: "relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" do %>
18
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
20
+ </svg>
21
+ <span class="ml-1">Prev</span>
22
+ <% end %>
23
+ <% else %>
24
+ <span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-300 bg-white border border-gray-200 rounded-md cursor-not-allowed">
25
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
27
+ </svg>
28
+ <span class="ml-1">Prev</span>
29
+ </span>
30
+ <% end %>
31
+
32
+ <% (1..pagy.pages).each do |page_num| %>
33
+ <% if page_num == pagy.page %>
34
+ <span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-indigo-600 border border-indigo-600 rounded-md"><%= page_num %></span>
35
+ <% elsif page_num == 1 || page_num == pagy.pages || (page_num >= pagy.page - 2 && page_num <= pagy.page + 2) %>
36
+ <%= link_to page_num, url_for(page: page_num), class: "relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" %>
37
+ <% elsif page_num == pagy.page - 3 || page_num == pagy.page + 3 %>
38
+ <span class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-700">...</span>
39
+ <% end %>
40
+ <% end %>
41
+
42
+ <% if pagy.next %>
43
+ <%= link_to url_for(page: pagy.next), class: "relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" do %>
44
+ <span class="mr-1">Next</span>
45
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
47
+ </svg>
48
+ <% end %>
49
+ <% else %>
50
+ <span class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-300 bg-white border border-gray-200 rounded-md cursor-not-allowed">
51
+ <span class="mr-1">Next</span>
52
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
54
+ </svg>
55
+ </span>
56
+ <% end %>
57
+ </nav>
58
+ </div>
59
+ </div>
60
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <div class="bg-white overflow-hidden shadow rounded-lg">
2
+ <div class="p-5">
3
+ <div class="flex items-center">
4
+ <div class="flex-shrink-0">
5
+ <% case icon %>
6
+ <% when "database" %>
7
+ <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
9
+ </svg>
10
+ <% when "calendar" %>
11
+ <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
13
+ </svg>
14
+ <% when "tag" %>
15
+ <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
16
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
17
+ </svg>
18
+ <% when "clock" %>
19
+ <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
20
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
21
+ </svg>
22
+ <% else %>
23
+ <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
24
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
25
+ </svg>
26
+ <% end %>
27
+ </div>
28
+ <div class="ml-5 w-0 flex-1">
29
+ <dl>
30
+ <dt class="text-sm font-medium text-gray-500 truncate"><%= title %></dt>
31
+ <dd class="text-lg font-semibold text-gray-900"><%= value %></dd>
32
+ </dl>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,18 @@
1
+ RailsEventViewer::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+
4
+ resources :events, only: [:index, :show] do
5
+ collection do
6
+ get :search
7
+ end
8
+ end
9
+
10
+ namespace :analytics do
11
+ get :overview
12
+ get :events_over_time
13
+ get :events_by_type
14
+ end
15
+
16
+ resources :event_types, only: [:index, :show], param: :name
17
+ resources :groups, only: [:index, :show]
18
+ end
@@ -0,0 +1,36 @@
1
+ class CreateRailsEventViewerEntries < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :rails_event_viewer_entries do |t|
4
+ t.string :name, null: false
5
+
6
+ # Use jsonb for PostgreSQL (better performance), json for others (SQLite, MySQL)
7
+ json_type = postgresql? ? :jsonb : :json
8
+ t.column :payload, json_type, default: {}
9
+ t.column :tags, json_type, default: {}
10
+ t.column :context, json_type, default: {}
11
+
12
+ t.string :source_file
13
+ t.integer :source_line
14
+ t.string :source_label
15
+ t.datetime :occurred_at, null: false
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :rails_event_viewer_entries, :name
21
+ add_index :rails_event_viewer_entries, :occurred_at
22
+ add_index :rails_event_viewer_entries, [:name, :occurred_at]
23
+
24
+ # GIN index for JSONB columns (PostgreSQL only)
25
+ if postgresql?
26
+ add_index :rails_event_viewer_entries, :tags, using: :gin
27
+ add_index :rails_event_viewer_entries, :context, using: :gin
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def postgresql?
34
+ connection.adapter_name.downcase.include?("postgresql")
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ Description:
2
+ Installs RailsEventViewer into your Rails application.
3
+
4
+ This will:
5
+ - Create a migration for the events table (if using ActiveRecord adapter)
6
+ - Create an initializer with configuration options
7
+ - Mount the engine in your routes file
8
+
9
+ Example:
10
+ bin/rails generate rails_event_viewer:install
11
+
12
+ This will create:
13
+ db/migrate/YYYYMMDDHHMMSS_create_rails_event_viewer_entries.rb
14
+ config/initializers/rails_event_viewer.rb
15
+
16
+ And add to config/routes.rb:
17
+ mount RailsEventViewer::Engine, at: "/events"
18
+
19
+ Options:
20
+ --storage-adapter=ADAPTER # Storage adapter to use: active_record (default), redis, memory
21
+ --skip-migration # Skip creating the migration file
22
+ --skip-initializer # Skip creating the initializer
23
+ --skip-routes # Skip mounting the engine in routes
24
+
25
+ Examples:
26
+ # Standard install with ActiveRecord
27
+ bin/rails generate rails_event_viewer:install
28
+
29
+ # Install with Redis adapter (no migration needed)
30
+ bin/rails generate rails_event_viewer:install --storage-adapter=redis
31
+
32
+ # Only create initializer (skip migration and routes)
33
+ bin/rails generate rails_event_viewer:install --skip-migration --skip-routes
@@ -0,0 +1,86 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module RailsEventViewer
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Installs RailsEventViewer: creates migration, initializer, and mounts the engine"
12
+
13
+ class_option :storage_adapter, type: :string, default: "active_record",
14
+ desc: "Storage adapter to use (active_record, redis, memory)"
15
+
16
+ class_option :skip_migration, type: :boolean, default: false,
17
+ desc: "Skip creating the migration file"
18
+
19
+ class_option :skip_initializer, type: :boolean, default: false,
20
+ desc: "Skip creating the initializer file"
21
+
22
+ class_option :skip_routes, type: :boolean, default: false,
23
+ desc: "Skip mounting the engine in routes"
24
+
25
+ def create_migration_file
26
+ return if options[:skip_migration]
27
+ return if options[:storage_adapter] != "active_record"
28
+
29
+ migration_template(
30
+ "create_rails_event_viewer_entries.rb.erb",
31
+ "db/migrate/create_rails_event_viewer_entries.rb"
32
+ )
33
+ end
34
+
35
+ def create_initializer
36
+ return if options[:skip_initializer]
37
+
38
+ template(
39
+ "initializer.rb.erb",
40
+ "config/initializers/rails_event_viewer.rb"
41
+ )
42
+ end
43
+
44
+ def mount_engine
45
+ return if options[:skip_routes]
46
+
47
+ route_line = 'mount RailsEventViewer::Engine, at: "/events"'
48
+
49
+ routes_file = File.join(destination_root, "config/routes.rb")
50
+ if File.exist?(routes_file) && File.read(routes_file).include?("RailsEventViewer::Engine")
51
+ say_status :skip, "Engine already mounted in routes", :yellow
52
+ return
53
+ end
54
+
55
+ route route_line
56
+ end
57
+
58
+ def show_post_install_message
59
+ say ""
60
+ say "RailsEventViewer has been installed!", :green
61
+ say ""
62
+ say "Next steps:"
63
+
64
+ if options[:storage_adapter] == "active_record" && !options[:skip_migration]
65
+ say " 1. Run migrations: rails db:migrate"
66
+ end
67
+
68
+ say " 2. Configure authentication in config/initializers/rails_event_viewer.rb"
69
+ say " 3. Visit /events to view your events"
70
+ say ""
71
+ say "For more information, visit: https://github.com/keshavbiswa/rails_event_viewer"
72
+ say ""
73
+ end
74
+
75
+ private
76
+
77
+ def migration_version
78
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
79
+ end
80
+
81
+ def storage_adapter
82
+ options[:storage_adapter]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,35 @@
1
+ class CreateRailsEventViewerEntries < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :rails_event_viewer_entries do |t|
4
+ t.string :name, null: false
5
+
6
+ json_type = postgresql? ? :jsonb : :json
7
+ t.column :payload, json_type, default: {}
8
+ t.column :tags, json_type, default: {}
9
+ t.column :context, json_type, default: {}
10
+
11
+ t.string :source_file
12
+ t.integer :source_line
13
+ t.string :source_label
14
+ t.datetime :occurred_at, null: false
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :rails_event_viewer_entries, :name
20
+ add_index :rails_event_viewer_entries, :occurred_at
21
+ add_index :rails_event_viewer_entries, [:name, :occurred_at]
22
+
23
+ # GIN index for JSONB columns (PostgreSQL only)
24
+ if postgresql?
25
+ add_index :rails_event_viewer_entries, :tags, using: :gin
26
+ add_index :rails_event_viewer_entries, :context, using: :gin
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def postgresql?
33
+ connection.adapter_name.downcase.include?("postgresql")
34
+ end
35
+ end