dbwatcher 0.1.5
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 +7 -0
- data/README.md +282 -0
- data/Rakefile +72 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +38 -0
- data/app/views/dbwatcher/sessions/index.html.erb +37 -0
- data/app/views/dbwatcher/sessions/show.html.erb +150 -0
- data/app/views/layouts/dbwatcher/application.html.erb +34 -0
- data/bin/console +11 -0
- data/bin/release +385 -0
- data/bin/setup +8 -0
- data/config/routes.rb +10 -0
- data/lib/dbwatcher/configuration.rb +24 -0
- data/lib/dbwatcher/engine.rb +30 -0
- data/lib/dbwatcher/middleware.rb +40 -0
- data/lib/dbwatcher/model_extension.rb +61 -0
- data/lib/dbwatcher/storage.rb +110 -0
- data/lib/dbwatcher/tracker.rb +112 -0
- data/lib/dbwatcher/version.rb +5 -0
- data/lib/dbwatcher.rb +40 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ee5502341a37e588b1dcf38edd5f33fa9e0b3746f43efea7e419ec241e873150
|
4
|
+
data.tar.gz: 874907249da682606c7f02df3e423ba3f0b6ef8c553ff6165561ab5cb7ad0981
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b8d78baaa87bad96ac04968238b945d162c8acc0451c678803d7defdfa46ab66fbb9d7e96ab8bc11a4f09166713f995e641bea32090c2346ab335939faa7770
|
7
|
+
data.tar.gz: 13dd481403397e95a74c04857718f878ebd35dc223ffa164dfaf871f6da4a9c0d02559555b25f5b575e5bab2cf265ce953902e400026647d3441c9bdd38a3e92
|
data/README.md
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
# DBWatcher ๐
|
2
|
+
|
3
|
+
Track and visualize database changes in your Rails application for easier debugging and development.
|
4
|
+
|
5
|
+
DBWatcher is a powerful Rails gem that captures, stores, and visualizes all database operations in your application. Perfect for debugging complex data flows, understanding application behavior, and optimizing database performance.
|
6
|
+
|
7
|
+
## โจ Features
|
8
|
+
|
9
|
+
- **๐ Real-time Database Tracking**: Monitor all SQL operations (INSERT, UPDATE, DELETE, SELECT)
|
10
|
+
- **๐ฏ Selective Tracking**: Track specific code blocks or entire requests
|
11
|
+
- **๐ฑ Web Dashboard**: Beautiful, responsive interface built with Alpine.js and Tailwind CSS
|
12
|
+
- **๐พ File-based Storage**: No additional database setup required
|
13
|
+
- **๐ URL-based Activation**: Simple `?dbwatch=true` parameter enables tracking
|
14
|
+
- **๐งน Automatic Cleanup**: Configurable session cleanup and storage management
|
15
|
+
- **โก Zero Dependencies**: Works with any Rails application without complex setup
|
16
|
+
- **๐ Development-focused**: Designed for development and testing environments
|
17
|
+
|
18
|
+
## ๐ Installation
|
19
|
+
|
20
|
+
Add to your Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'dbwatcher', group: :development
|
24
|
+
```
|
25
|
+
|
26
|
+
Then run:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
The engine will automatically mount at `/dbwatcher` in your Rails application.
|
33
|
+
|
34
|
+
### Manual Route Mounting (Optional)
|
35
|
+
|
36
|
+
If you need custom mounting, add to your `config/routes.rb`:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Rails.application.routes.draw do
|
40
|
+
mount Dbwatcher::Engine => "/dbwatcher" if Rails.env.development?
|
41
|
+
# ... your other routes
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
## ๐ Usage
|
46
|
+
|
47
|
+
### ๐ฏ Targeted Tracking
|
48
|
+
|
49
|
+
Track specific code blocks with detailed context:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
Dbwatcher.track(name: "User Registration Flow") do
|
53
|
+
user = User.create!(
|
54
|
+
name: "John Doe",
|
55
|
+
email: "john@example.com"
|
56
|
+
)
|
57
|
+
|
58
|
+
user.create_profile!(
|
59
|
+
bio: "Software Developer",
|
60
|
+
location: "San Francisco"
|
61
|
+
)
|
62
|
+
|
63
|
+
user.posts.create!(
|
64
|
+
title: "Welcome Post",
|
65
|
+
content: "Hello World!"
|
66
|
+
)
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
### ๐ URL-based Tracking
|
71
|
+
|
72
|
+
Enable tracking for any request by adding `?dbwatch=true`:
|
73
|
+
|
74
|
+
```
|
75
|
+
# Track a user show page
|
76
|
+
GET /users/123?dbwatch=true
|
77
|
+
|
78
|
+
# Track a form submission
|
79
|
+
POST /users?dbwatch=true
|
80
|
+
|
81
|
+
# Track API endpoints
|
82
|
+
GET /api/posts?dbwatch=true
|
83
|
+
```
|
84
|
+
|
85
|
+
### ๐ View Tracking Results
|
86
|
+
|
87
|
+
Visit `/dbwatcher` in your Rails application to access the dashboard where you can:
|
88
|
+
- Browse all tracking sessions
|
89
|
+
- View detailed SQL queries and timing
|
90
|
+
- Analyze database operation patterns
|
91
|
+
- Monitor application performance
|
92
|
+
|
93
|
+
## โ๏ธ Configuration
|
94
|
+
|
95
|
+
Create an initializer for custom configuration:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# config/initializers/dbwatcher.rb
|
99
|
+
Dbwatcher.configure do |config|
|
100
|
+
# Storage location for tracking data
|
101
|
+
config.storage_path = Rails.root.join('tmp', 'dbwatcher')
|
102
|
+
|
103
|
+
# Enable/disable tracking (default: development only)
|
104
|
+
config.enabled = Rails.env.development?
|
105
|
+
|
106
|
+
# Maximum number of sessions to keep
|
107
|
+
config.max_sessions = 100
|
108
|
+
|
109
|
+
# Automatic cleanup after N days
|
110
|
+
config.auto_clean_after_days = 7
|
111
|
+
|
112
|
+
# Include query parameters in tracking
|
113
|
+
config.include_params = true
|
114
|
+
|
115
|
+
# Exclude certain SQL patterns
|
116
|
+
config.excluded_patterns = [
|
117
|
+
/SHOW TABLES/,
|
118
|
+
/DESCRIBE/
|
119
|
+
]
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
## ๐๏ธ Development & Testing
|
124
|
+
|
125
|
+
This project includes a comprehensive dummy Rails application for testing and development.
|
126
|
+
|
127
|
+
### Running Tests
|
128
|
+
|
129
|
+
```bash
|
130
|
+
# Run all tests
|
131
|
+
bundle exec rake test
|
132
|
+
|
133
|
+
# Run specific test suites
|
134
|
+
bundle exec rake unit # Unit tests
|
135
|
+
bundle exec rake acceptance # Feature tests
|
136
|
+
bundle exec cucumber -p chrome # Browser tests
|
137
|
+
```
|
138
|
+
|
139
|
+
### Development Server
|
140
|
+
|
141
|
+
```bash
|
142
|
+
# Start the dummy application
|
143
|
+
cd spec/dummy
|
144
|
+
bundle exec rails server -p 3001
|
145
|
+
|
146
|
+
# Visit the test interface
|
147
|
+
open http://localhost:3001
|
148
|
+
|
149
|
+
# Visit DBWatcher dashboard
|
150
|
+
open http://localhost:3001/dbwatcher
|
151
|
+
```
|
152
|
+
|
153
|
+
### Code Quality
|
154
|
+
|
155
|
+
```bash
|
156
|
+
# Run linter
|
157
|
+
bundle exec rubocop
|
158
|
+
|
159
|
+
# Auto-fix issues
|
160
|
+
bundle exec rubocop --autocorrect
|
161
|
+
|
162
|
+
# Security analysis
|
163
|
+
bundle exec brakeman
|
164
|
+
```
|
165
|
+
|
166
|
+
## ๐ ๏ธ Troubleshooting
|
167
|
+
|
168
|
+
### Route Helper Errors
|
169
|
+
|
170
|
+
If you encounter `undefined method 'dbwatcher_sessions_path'`:
|
171
|
+
|
172
|
+
1. **Restart your Rails server** after installing the gem
|
173
|
+
2. **Check Rails version** - requires Rails 6.0+
|
174
|
+
3. **Manual mounting** - add the mount line to your routes file
|
175
|
+
|
176
|
+
### Performance Considerations
|
177
|
+
|
178
|
+
- DBWatcher is designed for development environments
|
179
|
+
- Disable in production using `config.enabled = false`
|
180
|
+
- Use targeted tracking for performance-sensitive operations
|
181
|
+
- Regular cleanup prevents storage bloat
|
182
|
+
|
183
|
+
### Storage Location
|
184
|
+
|
185
|
+
- Default: `Rails.root/tmp/dbwatcher/`
|
186
|
+
- Files are JSON formatted for easy inspection
|
187
|
+
- Sessions auto-expire based on configuration
|
188
|
+
|
189
|
+
## ๐ง Advanced Usage
|
190
|
+
|
191
|
+
### Custom Metadata
|
192
|
+
|
193
|
+
Add context to your tracking sessions:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
Dbwatcher.track(
|
197
|
+
name: "Complex Business Logic",
|
198
|
+
metadata: {
|
199
|
+
user_id: current_user.id,
|
200
|
+
feature_flag: "new_checkout",
|
201
|
+
version: "2.1.0"
|
202
|
+
}
|
203
|
+
) do
|
204
|
+
# Your database operations
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
### Conditional Tracking
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
Dbwatcher.track(name: "Admin Operations") do
|
212
|
+
# This will only track if DBWatcher is enabled
|
213
|
+
User.where(admin: true).update_all(last_seen: Time.current)
|
214
|
+
end if Dbwatcher.enabled?
|
215
|
+
```
|
216
|
+
|
217
|
+
### Integration with Testing
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
# In your test suite
|
221
|
+
RSpec.describe "User Registration" do
|
222
|
+
it "creates user with proper associations" do
|
223
|
+
session_data = nil
|
224
|
+
|
225
|
+
Dbwatcher.track(name: "Test User Creation") do
|
226
|
+
user = create(:user)
|
227
|
+
expect(user.profile).to be_present
|
228
|
+
end
|
229
|
+
|
230
|
+
# Analyze the tracked operations
|
231
|
+
expect(Dbwatcher::Storage.last_session).to include_sql(/INSERT INTO users/)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
## ๐ Project Structure
|
237
|
+
|
238
|
+
```
|
239
|
+
dbwatcher/
|
240
|
+
โโโ app/
|
241
|
+
โ โโโ controllers/dbwatcher/ # Web interface controllers
|
242
|
+
โ โโโ views/dbwatcher/ # Dashboard templates
|
243
|
+
โโโ config/
|
244
|
+
โ โโโ routes.rb # Engine routes
|
245
|
+
โโโ lib/dbwatcher/
|
246
|
+
โ โโโ configuration.rb # Configuration management
|
247
|
+
โ โโโ engine.rb # Rails engine
|
248
|
+
โ โโโ middleware.rb # Rack middleware
|
249
|
+
โ โโโ storage.rb # File-based storage
|
250
|
+
โ โโโ tracker.rb # Core tracking logic
|
251
|
+
โโโ spec/
|
252
|
+
โโโ dummy/ # Test Rails application
|
253
|
+
โโโ acceptance/ # Feature tests
|
254
|
+
โโโ unit/ # Unit tests
|
255
|
+
```
|
256
|
+
|
257
|
+
## ๐ค Contributing
|
258
|
+
|
259
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
260
|
+
|
261
|
+
1. Fork the repository
|
262
|
+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
263
|
+
3. Make your changes and add tests
|
264
|
+
4. Ensure all tests pass: `bundle exec rake test`
|
265
|
+
5. Run the linter: `bundle exec rubocop`
|
266
|
+
6. Commit your changes: `git commit -am 'Add amazing feature'`
|
267
|
+
7. Push to the branch: `git push origin feature/amazing-feature`
|
268
|
+
8. Open a Pull Request
|
269
|
+
|
270
|
+
## ๐ License
|
271
|
+
|
272
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
273
|
+
|
274
|
+
## ๐ Acknowledgments
|
275
|
+
|
276
|
+
- Built with Rails Engine architecture
|
277
|
+
- UI powered by Alpine.js and Tailwind CSS
|
278
|
+
- Inspired by debugging needs in complex Rails applications
|
279
|
+
|
280
|
+
---
|
281
|
+
|
282
|
+
**Happy Debugging!** ๐โจ
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
# Unit tests
|
7
|
+
RSpec::Core::RakeTask.new(:unit) do |task|
|
8
|
+
task.pattern = "spec/unit/**/*_spec.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Acceptance tests (Cucumber)
|
12
|
+
begin
|
13
|
+
require "cucumber/rake/task"
|
14
|
+
|
15
|
+
Cucumber::Rake::Task.new(:acceptance) do |task|
|
16
|
+
task.cucumber_opts = ["spec/acceptance/features", "--require", "spec/acceptance"]
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
desc "Cucumber not available"
|
20
|
+
task :acceptance do
|
21
|
+
puts "Cucumber not available. Install cucumber-rails gem to run acceptance tests."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# All tests
|
26
|
+
task spec: [:unit]
|
27
|
+
task test: %i[spec acceptance]
|
28
|
+
|
29
|
+
require "rubocop/rake_task"
|
30
|
+
RuboCop::RakeTask.new
|
31
|
+
|
32
|
+
# Security tasks
|
33
|
+
desc "Run bundle audit to check for security vulnerabilities"
|
34
|
+
task :bundle_audit do
|
35
|
+
require "bundler/audit/task"
|
36
|
+
Bundler::Audit::Task.new
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Run Brakeman security scanner"
|
40
|
+
task :brakeman do
|
41
|
+
require "brakeman"
|
42
|
+
result = Brakeman.run app_path: "spec/dummy", config_file: ".brakeman.yml"
|
43
|
+
if result.warnings.any? || result.errors.any?
|
44
|
+
puts "โ Brakeman found security issues"
|
45
|
+
exit 1
|
46
|
+
else
|
47
|
+
puts "โ
Brakeman security check passed"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Run all security checks"
|
52
|
+
task security: %i[bundle_audit brakeman]
|
53
|
+
|
54
|
+
# Default task
|
55
|
+
task default: %i[test rubocop security]
|
56
|
+
|
57
|
+
# Setup tasks
|
58
|
+
desc "Set up test database and environment"
|
59
|
+
task :test_setup do
|
60
|
+
Dir.chdir("spec/dummy") do
|
61
|
+
system("bundle install")
|
62
|
+
system("bundle exec rails db:drop db:create db:migrate RAILS_ENV=test")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Start test server for manual testing"
|
67
|
+
task :test_server do
|
68
|
+
Dir.chdir("spec/dummy") do
|
69
|
+
ENV["BUNDLE_GEMFILE"] = File.expand_path("Gemfile", Dir.pwd)
|
70
|
+
system("bundle exec rails server -e test -p 3001")
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
class SessionsController < ActionController::Base
|
5
|
+
protect_from_forgery with: :exception
|
6
|
+
layout "dbwatcher/application"
|
7
|
+
|
8
|
+
def index
|
9
|
+
@sessions = Storage.all_sessions
|
10
|
+
end
|
11
|
+
|
12
|
+
def show
|
13
|
+
@session = Storage.load_session(params[:id])
|
14
|
+
|
15
|
+
respond_to do |format|
|
16
|
+
format.html
|
17
|
+
format.json { render json: @session.to_h }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy_all
|
22
|
+
Dbwatcher.reset!
|
23
|
+
redirect_to root_path, notice: "All sessions cleared"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Helper method to safely get the sessions path
|
29
|
+
def sessions_index_path
|
30
|
+
if respond_to?(:sessions_path)
|
31
|
+
sessions_path
|
32
|
+
else
|
33
|
+
"/dbwatcher"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
helper_method :sessions_index_path
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<div class="px-4 sm:px-0">
|
2
|
+
<h2 class="text-2xl font-bold mb-6">Tracking Sessions</h2>
|
3
|
+
|
4
|
+
<% if @sessions.empty? %>
|
5
|
+
<div class="bg-white overflow-hidden shadow rounded-lg p-6 text-center text-gray-500">
|
6
|
+
No tracking sessions yet. Start tracking with <code class="bg-gray-100 px-2 py-1 rounded">Dbwatcher.track { ... }</code>
|
7
|
+
</div>
|
8
|
+
<% else %>
|
9
|
+
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
10
|
+
<ul class="divide-y divide-gray-200">
|
11
|
+
<% @sessions.each do |session| %>
|
12
|
+
<li>
|
13
|
+
<%= link_to session_path(session[:id]), class: "block hover:bg-gray-50 px-4 py-4 sm:px-6" do %>
|
14
|
+
<div class="flex items-center justify-between">
|
15
|
+
<div class="flex-1">
|
16
|
+
<p class="text-sm font-medium text-indigo-600 truncate">
|
17
|
+
<%= session[:name] %>
|
18
|
+
</p>
|
19
|
+
<p class="mt-1 text-sm text-gray-600">
|
20
|
+
<%= Time.parse(session[:started_at]).strftime("%Y-%m-%d %H:%M:%S") %>
|
21
|
+
<span class="text-gray-400">โข</span>
|
22
|
+
<%= session[:change_count] %> changes
|
23
|
+
</p>
|
24
|
+
</div>
|
25
|
+
<div class="ml-2 flex-shrink-0">
|
26
|
+
<svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
27
|
+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
28
|
+
</svg>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
<% end %>
|
32
|
+
</li>
|
33
|
+
<% end %>
|
34
|
+
</ul>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
37
|
+
</div>
|
@@ -0,0 +1,150 @@
|
|
1
|
+
<div x-data="sessionViewer(<%= @session.to_h.to_json %>)" class="space-y-6">
|
2
|
+
<div class="flex justify-between items-center">
|
3
|
+
<h2 class="text-2xl font-bold" x-text="session.name"></h2>
|
4
|
+
<button @click="showFullRecords = !showFullRecords"
|
5
|
+
class="bg-gray-100 hover:bg-gray-200 px-4 py-2 rounded flex items-center gap-2">
|
6
|
+
<span x-text="showFullRecords ? 'Show Changes Only' : 'Show Full Records'"></span>
|
7
|
+
</button>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<!-- Summary -->
|
11
|
+
<div class="bg-white shadow rounded-lg p-6">
|
12
|
+
<h3 class="text-lg font-semibold mb-4">Summary</h3>
|
13
|
+
<div class="grid grid-cols-3 gap-4">
|
14
|
+
<template x-for="[key, count] in Object.entries(session.summary)" :key="key">
|
15
|
+
<div class="text-sm">
|
16
|
+
<span x-text="key.split(',')[0]" class="font-medium"></span>
|
17
|
+
<span x-text="key.split(',')[1]" :class="getOperationColor(key.split(',')[1])"></span>:
|
18
|
+
<span x-text="count"></span>
|
19
|
+
</div>
|
20
|
+
</template>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<!-- Changes by Table -->
|
25
|
+
<div class="bg-white shadow rounded-lg p-6">
|
26
|
+
<h3 class="text-lg font-semibold mb-4">Database Changes</h3>
|
27
|
+
|
28
|
+
<div class="space-y-2">
|
29
|
+
<template x-for="table in groupedChanges" :key="table.name">
|
30
|
+
<div class="border rounded">
|
31
|
+
<button @click="table.expanded = !table.expanded"
|
32
|
+
class="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50">
|
33
|
+
<div class="flex items-center gap-2">
|
34
|
+
<svg class="w-4 h-4 transition-transform" :class="{'rotate-90': table.expanded}" fill="currentColor" viewBox="0 0 20 20">
|
35
|
+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
36
|
+
</svg>
|
37
|
+
<span class="font-medium" x-text="table.name"></span>
|
38
|
+
<span class="text-sm text-gray-500" x-text="`(${table.changes.length} changes)`"></span>
|
39
|
+
</div>
|
40
|
+
</button>
|
41
|
+
|
42
|
+
<div x-show="table.expanded" x-collapse class="border-t">
|
43
|
+
<template x-for="record in table.records" :key="record.id">
|
44
|
+
<div class="border-b last:border-0">
|
45
|
+
<button @click="record.expanded = !record.expanded"
|
46
|
+
class="w-full px-6 py-2 flex items-center gap-2 hover:bg-gray-50 text-left">
|
47
|
+
<svg class="w-4 h-4 transition-transform" :class="{'rotate-90': record.expanded}" fill="currentColor" viewBox="0 0 20 20">
|
48
|
+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
49
|
+
</svg>
|
50
|
+
<span x-text="`Record #${record.id}`"></span>
|
51
|
+
</button>
|
52
|
+
|
53
|
+
<div x-show="record.expanded" x-collapse class="px-8 pb-4">
|
54
|
+
<template x-for="change in record.changes" :key="change.timestamp">
|
55
|
+
<div class="mt-3">
|
56
|
+
<div class="font-medium mb-2" :class="getOperationColor(change.operation)">
|
57
|
+
<span x-text="change.operation"></span> at
|
58
|
+
<span x-text="new Date(change.timestamp).toLocaleTimeString()"></span>
|
59
|
+
</div>
|
60
|
+
|
61
|
+
<div x-show="!showFullRecords" class="space-y-1">
|
62
|
+
<template x-for="col in change.changes" :key="col.column">
|
63
|
+
<div class="text-sm">
|
64
|
+
<span class="font-medium" x-text="col.column + ':'"></span>
|
65
|
+
<template x-if="col.old_value">
|
66
|
+
<span>
|
67
|
+
<span class="text-red-600 line-through" x-text="col.old_value"></span>
|
68
|
+
<span class="mx-1">โ</span>
|
69
|
+
</span>
|
70
|
+
</template>
|
71
|
+
<span class="text-green-600" x-text="col.new_value || 'null'"></span>
|
72
|
+
</div>
|
73
|
+
</template>
|
74
|
+
</div>
|
75
|
+
|
76
|
+
<div x-show="showFullRecords" class="bg-gray-50 p-3 rounded text-sm font-mono">
|
77
|
+
<pre x-text="JSON.stringify(change.record_snapshot, null, 2)"></pre>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
</template>
|
81
|
+
</div>
|
82
|
+
</div>
|
83
|
+
</template>
|
84
|
+
</div>
|
85
|
+
</div>
|
86
|
+
</template>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
|
91
|
+
<script>
|
92
|
+
function sessionViewer(sessionData) {
|
93
|
+
return {
|
94
|
+
session: sessionData,
|
95
|
+
showFullRecords: false,
|
96
|
+
groupedChanges: [],
|
97
|
+
|
98
|
+
init() {
|
99
|
+
// Calculate summary from changes
|
100
|
+
const summary = {};
|
101
|
+
this.session.changes.forEach(change => {
|
102
|
+
const key = `${change.table_name},${change.operation}`;
|
103
|
+
summary[key] = (summary[key] || 0) + 1;
|
104
|
+
});
|
105
|
+
this.session.summary = summary;
|
106
|
+
|
107
|
+
// Group changes by table and record
|
108
|
+
const tables = {};
|
109
|
+
|
110
|
+
this.session.changes.forEach(change => {
|
111
|
+
if (!tables[change.table_name]) {
|
112
|
+
tables[change.table_name] = {
|
113
|
+
name: change.table_name,
|
114
|
+
expanded: false,
|
115
|
+
changes: [],
|
116
|
+
records: {}
|
117
|
+
};
|
118
|
+
}
|
119
|
+
|
120
|
+
tables[change.table_name].changes.push(change);
|
121
|
+
|
122
|
+
if (!tables[change.table_name].records[change.record_id]) {
|
123
|
+
tables[change.table_name].records[change.record_id] = {
|
124
|
+
id: change.record_id,
|
125
|
+
expanded: false,
|
126
|
+
changes: []
|
127
|
+
};
|
128
|
+
}
|
129
|
+
|
130
|
+
tables[change.table_name].records[change.record_id].changes.push(change);
|
131
|
+
});
|
132
|
+
|
133
|
+
// Convert to array format
|
134
|
+
this.groupedChanges = Object.values(tables).map(table => ({
|
135
|
+
...table,
|
136
|
+
records: Object.values(table.records)
|
137
|
+
}));
|
138
|
+
},
|
139
|
+
|
140
|
+
getOperationColor(operation) {
|
141
|
+
const colors = {
|
142
|
+
'INSERT': 'text-green-600',
|
143
|
+
'UPDATE': 'text-blue-600',
|
144
|
+
'DELETE': 'text-red-600'
|
145
|
+
};
|
146
|
+
return colors[operation] || 'text-gray-600';
|
147
|
+
}
|
148
|
+
};
|
149
|
+
}
|
150
|
+
</script>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>DB Watcher</title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
9
|
+
</head>
|
10
|
+
<body class="bg-gray-50">
|
11
|
+
<div class="min-h-screen">
|
12
|
+
<nav class="bg-white shadow">
|
13
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
14
|
+
<div class="flex justify-between h-16">
|
15
|
+
<div class="flex items-center">
|
16
|
+
<h1 class="text-xl font-semibold">๐ DB Watcher</h1>
|
17
|
+
</div>
|
18
|
+
<div class="flex items-center space-x-4">
|
19
|
+
<%= link_to "Sessions", sessions_index_path, class: "text-gray-700 hover:text-gray-900" %>
|
20
|
+
<%= button_to "Reset All Sessions", destroy_all_sessions_path,
|
21
|
+
method: :delete,
|
22
|
+
data: { confirm: "Are you sure? This will delete all tracking data." },
|
23
|
+
class: "bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600" %>
|
24
|
+
</div>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
</nav>
|
28
|
+
|
29
|
+
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
30
|
+
<%= yield %>
|
31
|
+
</main>
|
32
|
+
</div>
|
33
|
+
</body>
|
34
|
+
</html>
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "dbwatcher"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require "irb"
|
11
|
+
IRB.start(__FILE__)
|