query_tracker 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +103 -0
- data/bin/setup +8 -0
- data/lib/query_tracker/compare/comparator.rb +33 -0
- data/lib/query_tracker/configuration.rb +12 -0
- data/lib/query_tracker/middleware.rb +37 -0
- data/lib/query_tracker/printer/base.rb +42 -0
- data/lib/query_tracker/printer/console.rb +30 -0
- data/lib/query_tracker/printer/html.rb +68 -0
- data/lib/query_tracker/printer/html_compare.rb +64 -0
- data/lib/query_tracker/printer/templates/bar_chart.js +153 -0
- data/lib/query_tracker/printer/templates/comparing.html.erb +66 -0
- data/lib/query_tracker/printer/templates/style.css +41 -0
- data/lib/query_tracker/printer/templates/template.html.erb +9 -0
- data/lib/query_tracker/printer/templates/template_base_query_counter.html.erb +51 -0
- data/lib/query_tracker/printer/templates/template_for_inject.html.erb +64 -0
- data/lib/query_tracker/recording/base.rb +28 -0
- data/lib/query_tracker/recording/tracker.rb +40 -0
- data/lib/query_tracker/version.rb +5 -0
- data/lib/query_tracker.rb +36 -0
- metadata +258 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f36bdfeaac971e0d3feb504cc6b99bd1ced63405f081bee5f15a104a7edc21d7
|
4
|
+
data.tar.gz: 519e7814892c3ba01e71cacc2c03591fd2657a85725582090ed90b71aa133734
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 56026f5ad302e525ba269ee5ff6bb88287a46b6777c14ac526df5de6903864301ada972fb03602b52ae2b704ef376a3bb06f083c294860c9d6d510bc7ba5f20c
|
7
|
+
data.tar.gz: a1c346e6b192ad8f53dadadd7edc3473fa96f83ac4c673d64dcb617c7ffcba1c476daead1ecf9995420218231df1ef5d4ad97d3b3bd6057615b0b3d6452c43cc
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# QueryTracker
|
2
|
+
|
3
|
+
`QueryTracker` is a Ruby gem designed to help you track and analyze SQL queries executed by your ActiveRecord models. By subscribing to ActiveSupport notifications, it provides detailed insights into the queries being run, including the tables involved and the locations in your code where the queries are generated.
|
4
|
+
There are three things this gem allows you to do
|
5
|
+
|
6
|
+
1. You can compare two codes to view the difference in SQL counts on locations with a graph or a table.
|
7
|
+
2. You can view an overview of the SQL that a code does in a graph, a table or in the console.
|
8
|
+
3. (In progress) You can see an overview of the current request on a controller action
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'query_tracker'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
bundle install
|
22
|
+
```
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
gem install query_tracker
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
There are three ways of using this gem:
|
33
|
+
|
34
|
+
1. With a block of code
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'query_tracker'
|
38
|
+
QueryTracker.start_with_block(printer: :html) do
|
39
|
+
# your code goes here
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
this will open up a html table with the SQL stats of your code
|
44
|
+
|
45
|
+
2. Starting recording manually
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'query_tracker'
|
49
|
+
|
50
|
+
QueryTracker.start_recording
|
51
|
+
# your code goes here
|
52
|
+
QueryTracker.end_recording(printer: :html)
|
53
|
+
```
|
54
|
+
|
55
|
+
3. Comparing two blocks of code (only available for html printer)
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
require 'query_tracker'
|
59
|
+
QueryTracker.compare do |bench|
|
60
|
+
bench.code('script1') do
|
61
|
+
end
|
62
|
+
bench.code('script2') do
|
63
|
+
end
|
64
|
+
bench.compare!
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
this will open up a graph comparing the quantity of SQL of the two codes
|
69
|
+
|
70
|
+
4. (In progress) Enabling a middleware to see an overview of the current request SQL's in Rack Application.
|
71
|
+
On `config/development.rb` or the initializer of the application.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
config.after_initialize do
|
75
|
+
QueryTracker.configure do |configuration|
|
76
|
+
configuration.enable_middleware = true
|
77
|
+
configuration.ignore_table_count = 1
|
78
|
+
configuration.max_locations_per_table = 4
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
### Printing options
|
84
|
+
|
85
|
+
There are two ways of displaying the collected queries data, `:console` and `:html`, to select one pass the printer argument to `start_with_block` o `end_recording` methods.
|
86
|
+
|
87
|
+
## Development
|
88
|
+
|
89
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
90
|
+
|
91
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
92
|
+
|
93
|
+
## Contributing
|
94
|
+
|
95
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/[USERNAME]/query_tracker>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/query_tracker/blob/master/CODE_OF_CONDUCT.md).
|
96
|
+
|
97
|
+
## License
|
98
|
+
|
99
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
100
|
+
|
101
|
+
## Code of Conduct
|
102
|
+
|
103
|
+
Everyone interacting in the QueryTracker project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/query_tracker/blob/master/CODE_OF_CONDUCT.md).
|
data/bin/setup
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module QueryTracker
|
2
|
+
module Compare
|
3
|
+
class Comparator
|
4
|
+
attr_accessor :results, :scripts_loaded
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@results = {}
|
8
|
+
@scripts_loaded = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def code(name)
|
12
|
+
@scripts_loaded += 1
|
13
|
+
|
14
|
+
QueryTracker.start_with_block(printer: :none) do
|
15
|
+
yield
|
16
|
+
@results[name] = QueryTracker.tracker.query_tracker.dup
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def compare!
|
21
|
+
raise 'Exactly two code blocks are required' if @scripts_loaded != 2
|
22
|
+
|
23
|
+
QueryTracker::Printer::HtmlCompare.new(data_1: results.slice(results.keys[0]),
|
24
|
+
data_2: results.slice(results.keys[1])).print
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.compare
|
29
|
+
comparator = Comparator.new
|
30
|
+
yield(comparator)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
3
|
+
|
4
|
+
module QueryTracker
|
5
|
+
module Configuration
|
6
|
+
mattr_accessor :ignore_table_count, :max_locations_per_table, :enable_middleware
|
7
|
+
|
8
|
+
self.ignore_table_count = ENV['QUERY_COUNTER_IGNORE_TABLE_COUNT'] || 10
|
9
|
+
self.max_locations_per_table = ENV['MAX_LOCATIONS_PER_TABLE'] || 3
|
10
|
+
self.enable_middleware = false
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module QueryTracker
|
5
|
+
class Middleware
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
return @app.call(env) unless Configuration.enable_middleware
|
12
|
+
|
13
|
+
status, headers, response, query_count_data = nil
|
14
|
+
QueryTracker.start_recording
|
15
|
+
status, headers, response = @app.call(env)
|
16
|
+
query_count_data = QueryTracker.tracker.query_tracker.clone
|
17
|
+
QueryTracker.end_recording(printer: :none)
|
18
|
+
if headers['Content-Type']&.include?('text/html') && response.respond_to?(:body) && response.body['<body']
|
19
|
+
response_body = inject_query_counter_js(response.body, query_count_data)
|
20
|
+
headers['Content-Length'] = response_body.bytesize.to_s
|
21
|
+
response = [response_body]
|
22
|
+
end
|
23
|
+
[status, headers, response]
|
24
|
+
end
|
25
|
+
|
26
|
+
def inject_query_counter_js(response_body, query_count_data)
|
27
|
+
injected_html = ::QueryTracker::Printer::Html.new(data: query_count_data).inject_in_html
|
28
|
+
# Parse the response body with Nokogiri
|
29
|
+
doc = Nokogiri::HTML(response_body)
|
30
|
+
|
31
|
+
# Inject the query counter JS before the closing </body> tag
|
32
|
+
doc.at('body').add_child(injected_html)
|
33
|
+
|
34
|
+
doc.to_html
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module QueryTracker
|
2
|
+
module Printer
|
3
|
+
class Base
|
4
|
+
def filter_data data
|
5
|
+
data = data.select { |_, v| v[:count] >= Configuration.ignore_table_count }
|
6
|
+
data = data.sort_by { |_, v| -v[:count] }.each do |_category, info|
|
7
|
+
info[:location] = info[:location].sort_by do |_, detail|
|
8
|
+
-detail[:count]
|
9
|
+
end.first(Configuration.max_locations_per_table).to_h
|
10
|
+
end
|
11
|
+
data.to_h
|
12
|
+
end
|
13
|
+
|
14
|
+
def sort_data data
|
15
|
+
data = data.sort_by { |_, v| -v[:count] }.each do |_category, info|
|
16
|
+
info[:location] = info[:location].sort_by do |_, detail|
|
17
|
+
-detail[:count]
|
18
|
+
end.to_h
|
19
|
+
end
|
20
|
+
data.to_h
|
21
|
+
end
|
22
|
+
|
23
|
+
def open_file html_dest
|
24
|
+
if ENV['WSL_DISTRIBUTION']
|
25
|
+
Launchy.open("file://wsl%24/#{ENV['WSL_DISTRIBUTION']}#{html_dest}")
|
26
|
+
else
|
27
|
+
Launchy.open(html_dest)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# this js is not used as erb, it could be changed
|
32
|
+
def js_content
|
33
|
+
File.read(@js_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
# this js is not used as erb, it could be changed
|
37
|
+
def css_content
|
38
|
+
File.read(@css_path)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module QueryTracker
|
4
|
+
module Printer
|
5
|
+
class Console < Base
|
6
|
+
def initialize data:
|
7
|
+
super()
|
8
|
+
@data = data
|
9
|
+
end
|
10
|
+
|
11
|
+
def print
|
12
|
+
data = filter_data(@data)
|
13
|
+
puts '[QueryTracker] Query count per table:'.colorize(:blue)
|
14
|
+
puts "Total query count: #{data.values.sum { |v| v[:count] }}\n\n"
|
15
|
+
puts "All tables with less than #{Configuration.ignore_table_count} queries are ignored. \n\n"
|
16
|
+
puts "For each table, the top #{Configuration.max_locations_per_table} locations with the most queries will be shown.\n\n"
|
17
|
+
data.each do |category, info|
|
18
|
+
puts "Table #{category.colorize(:cyan)}"
|
19
|
+
puts " Total query count: #{info[:count].to_s.colorize(:blue)}"
|
20
|
+
puts ' Locations where the table was called:'
|
21
|
+
info[:location].each do |loc, details|
|
22
|
+
puts " - File location: #{loc}"
|
23
|
+
puts " Query count: #{details[:count].to_s.colorize(:blue)}"
|
24
|
+
end
|
25
|
+
puts
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'launchy'
|
4
|
+
require 'pry-byebug'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module QueryTracker
|
8
|
+
module Printer
|
9
|
+
class Html < Base
|
10
|
+
attr_accessor :data
|
11
|
+
|
12
|
+
def initialize data:
|
13
|
+
@template_path = File.join(__dir__, 'templates', 'template.html.erb')
|
14
|
+
@base_query_counter_path = File.join(__dir__, 'templates', 'template_base_query_counter.html.erb')
|
15
|
+
@inject_template_path = File.join(__dir__, 'templates', 'template_for_inject.html.erb')
|
16
|
+
@css_path = File.join(__dir__, 'templates', 'style.css')
|
17
|
+
# este no falla, quizá pueda poner lo mismo aquí con chart.js y así no hacer lo del nonce y csp
|
18
|
+
@js_path = File.join(__dir__, 'templates', 'bar_chart.js')
|
19
|
+
@data = data
|
20
|
+
end
|
21
|
+
|
22
|
+
def chart_data
|
23
|
+
@chart_data ||= generate_chart_data(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
# maybe this should not be filtered
|
27
|
+
def total_query_count
|
28
|
+
@total_query_count ||= data.values.sum { |v| v[:count] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def inject_in_html
|
32
|
+
ERB.new(File.read(@inject_template_path)).result(binding)
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_query_counter_base_div
|
36
|
+
ERB.new(File.read(@base_query_counter_path)).result(binding)
|
37
|
+
end
|
38
|
+
|
39
|
+
def print
|
40
|
+
html_dest = generate_html(binding)
|
41
|
+
open_file(html_dest)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def generate_chart_data(data)
|
47
|
+
chart_data = { labels: [], data: [], locations: {} }
|
48
|
+
data.each do |table, info|
|
49
|
+
chart_data[:labels] << table
|
50
|
+
chart_data[:data] << info[:count]
|
51
|
+
chart_data[:locations][table] = info[:location].map do |loc, detail|
|
52
|
+
{ location: loc, count: detail[:count] }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
chart_data
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_html binding
|
59
|
+
template = ERB.new(File.read(@template_path))
|
60
|
+
html_content = template.result(binding)
|
61
|
+
temp_dir = Dir.mktmpdir
|
62
|
+
html_dest = File.join(temp_dir, 'query_counter_report.html')
|
63
|
+
File.write(html_dest, html_content)
|
64
|
+
html_dest
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'launchy'
|
4
|
+
require 'pry-byebug'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module QueryTracker
|
8
|
+
module Printer
|
9
|
+
class HtmlCompare < Base
|
10
|
+
def initialize data_1:, data_2:
|
11
|
+
super()
|
12
|
+
@template_path = File.join(__dir__, 'templates', 'comparing.html.erb')
|
13
|
+
@css_path = File.join(__dir__, 'templates', 'style.css')
|
14
|
+
@js_path = File.join(__dir__, 'templates', 'bar_chart.js')
|
15
|
+
@script_1_name = data_1.keys.first
|
16
|
+
@script_2_name = data_2.keys.first
|
17
|
+
@data_1 = data_1[@script_1_name]
|
18
|
+
@data_2 = data_2[@script_2_name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def print
|
22
|
+
# used by binding on erb templates
|
23
|
+
data_1 = sort_data(@data_1)
|
24
|
+
data_2 = sort_data(@data_2)
|
25
|
+
tables = data_1.keys | data_2.keys
|
26
|
+
total_query_count_1 = data_1.values.sum { |v| v[:count] }
|
27
|
+
total_query_count_2 = data_2.values.sum { |v| v[:count] }
|
28
|
+
chart_data = generate_chart_data_compare(data_1, data_2)
|
29
|
+
# end
|
30
|
+
html_dest = generate_html(binding)
|
31
|
+
open_file(html_dest)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def generate_chart_data_compare(data_1, data_2)
|
37
|
+
labels = (data_1.keys | data_2.keys).sort
|
38
|
+
chart_data = { labels: [], data_1: {}, data_2: {}, locations: {} }
|
39
|
+
chart_data[:data_1][:name] = @script_1_name
|
40
|
+
chart_data[:data_2][:name] = @script_2_name
|
41
|
+
chart_data[:data_1][:data] = []
|
42
|
+
chart_data[:data_2][:data] = []
|
43
|
+
labels.each do |label|
|
44
|
+
chart_data[:labels] << label
|
45
|
+
chart_data[:data_1][:data] << (data_1[label] ? data_1[label][:count] : 0)
|
46
|
+
chart_data[:data_2][:data] << (data_2[label] ? data_2[label][:count] : 0)
|
47
|
+
chart_data[:locations][label] =
|
48
|
+
(data_1[label] ? data_1[label][:location] : {}).merge(data_2[label] ? data_2[label][:location] : {})
|
49
|
+
end
|
50
|
+
chart_data
|
51
|
+
end
|
52
|
+
|
53
|
+
def generate_html binding
|
54
|
+
template = ERB.new(File.read(@template_path))
|
55
|
+
html_content = template.result(binding)
|
56
|
+
|
57
|
+
temp_dir = Dir.mktmpdir
|
58
|
+
html_dest = File.join(temp_dir, 'query_counter_report.html')
|
59
|
+
File.write(html_dest, html_content)
|
60
|
+
html_dest
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
|
2
|
+
function toggleView() {
|
3
|
+
const chart = document.getElementById('queryCountChart');
|
4
|
+
const chartContainer = chart.parentElement;
|
5
|
+
const table = document.getElementById('queryTable');
|
6
|
+
const button = document.getElementById('toggleButton');
|
7
|
+
if (chartContainer.style.display === 'none') {
|
8
|
+
chartContainer.style.display = 'block';
|
9
|
+
table.style.display = 'none';
|
10
|
+
button.textContent = 'Show Table View';
|
11
|
+
} else {
|
12
|
+
chartContainer.style.display = 'none';
|
13
|
+
table.style.display = 'table';
|
14
|
+
button.textContent = 'Show chartContainer View';
|
15
|
+
};
|
16
|
+
};
|
17
|
+
|
18
|
+
function toggleColumnContent() {
|
19
|
+
const columnHeader = document.getElementById('columnHeader');
|
20
|
+
const toggleContents = document.querySelectorAll('.toggle-content');
|
21
|
+
const showSQL = columnHeader.textContent === 'File Path';
|
22
|
+
|
23
|
+
columnHeader.textContent = showSQL ? 'SQL' : 'File Path';
|
24
|
+
|
25
|
+
toggleContents.forEach(content => {
|
26
|
+
const filepath = content.querySelector('.filepath');
|
27
|
+
const sql = content.querySelector('.sql');
|
28
|
+
|
29
|
+
if (showSQL) {
|
30
|
+
filepath.style.display = 'none';
|
31
|
+
sql.style.display = 'block';
|
32
|
+
} else {
|
33
|
+
filepath.style.display = 'block';
|
34
|
+
sql.style.display = 'none';
|
35
|
+
}
|
36
|
+
});
|
37
|
+
}
|
38
|
+
|
39
|
+
function initializeChart(chartData) {
|
40
|
+
console.log(chartData);
|
41
|
+
const ctx = document.getElementById('queryCountChart').getContext('2d');
|
42
|
+
const queryCountChart = new Chart(ctx, {
|
43
|
+
type: 'bar',
|
44
|
+
data: {
|
45
|
+
labels: chartData.labels,
|
46
|
+
datasets: [
|
47
|
+
{
|
48
|
+
label: 'Dataset 1',
|
49
|
+
data: chartData.data,
|
50
|
+
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
51
|
+
borderColor: 'rgba(75, 192, 192, 1)',
|
52
|
+
borderWidth: 1
|
53
|
+
}
|
54
|
+
]
|
55
|
+
},
|
56
|
+
options: {
|
57
|
+
scales: {
|
58
|
+
y: {
|
59
|
+
type: 'logarithmic',
|
60
|
+
beginAtZero: true,
|
61
|
+
title: {
|
62
|
+
display: true,
|
63
|
+
text: 'Logarithmic Scale'
|
64
|
+
},
|
65
|
+
ticks: {
|
66
|
+
callback: function(value, index, values) {
|
67
|
+
if (value === 0 || value === 10 || value === 100 || value === 1000 || value === 10000) {
|
68
|
+
return value.toString();
|
69
|
+
}
|
70
|
+
return null;
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
74
|
+
},
|
75
|
+
plugins: {
|
76
|
+
tooltip: {
|
77
|
+
callbacks: {
|
78
|
+
label: function(tooltipItem) {
|
79
|
+
const index = tooltipItem.dataIndex;
|
80
|
+
const table = chartData.labels[index];
|
81
|
+
// const locations = chartData.locations[table];
|
82
|
+
// Aquí se podría hacer el stackgraph
|
83
|
+
return [
|
84
|
+
`Total Queries: ${tooltipItem.raw}`,
|
85
|
+
];
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
});
|
92
|
+
document.getElementById('queryCountChart').parentElement.style.display = 'none';
|
93
|
+
}
|
94
|
+
|
95
|
+
function initializeChartCompare(chartData) {
|
96
|
+
const ctx = document.getElementById('queryCountChart').getContext('2d');
|
97
|
+
const queryCountChart = new Chart(ctx, {
|
98
|
+
type: 'bar',
|
99
|
+
data: {
|
100
|
+
labels: chartData.labels,
|
101
|
+
datasets: [
|
102
|
+
{
|
103
|
+
label: chartData.data_1.name,
|
104
|
+
data: chartData.data_1.data,
|
105
|
+
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
106
|
+
borderColor: 'rgba(75, 192, 192, 1)',
|
107
|
+
borderWidth: 1
|
108
|
+
},
|
109
|
+
{
|
110
|
+
label: chartData.data_2.name,
|
111
|
+
data: chartData.data_2.data,
|
112
|
+
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
113
|
+
borderColor: 'rgba(255, 99, 132, 1)',
|
114
|
+
borderWidth: 1
|
115
|
+
}
|
116
|
+
]
|
117
|
+
},
|
118
|
+
options: {
|
119
|
+
scales: {
|
120
|
+
y: {
|
121
|
+
type: 'logarithmic',
|
122
|
+
beginAtZero: true,
|
123
|
+
title: {
|
124
|
+
display: true,
|
125
|
+
text: 'Logarithmic Scale'
|
126
|
+
},
|
127
|
+
ticks: {
|
128
|
+
callback: function(value, index, values) {
|
129
|
+
if (value === 0 || value === 10 || value === 100 || value === 1000 || value === 10000) {
|
130
|
+
return value.toString();
|
131
|
+
}
|
132
|
+
return null;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
},
|
137
|
+
plugins: {
|
138
|
+
tooltip: {
|
139
|
+
callbacks: {
|
140
|
+
label: function(tooltipItem) {
|
141
|
+
const index = tooltipItem.dataIndex;
|
142
|
+
const table = chartData.labels[index];
|
143
|
+
return [
|
144
|
+
`Total Queries: ${tooltipItem.raw}`,
|
145
|
+
];
|
146
|
+
}
|
147
|
+
}
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
151
|
+
});
|
152
|
+
document.getElementById('queryCountChart').parentElement.style.display = 'none';
|
153
|
+
}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Query Counter Report</title>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<script>
|
8
|
+
<%= js_content %>
|
9
|
+
</script>
|
10
|
+
<style>
|
11
|
+
<%= css_content %>
|
12
|
+
</style>
|
13
|
+
<div id='query_counter_report_gem'>
|
14
|
+
<h2>Query Counter Report</h2>
|
15
|
+
<p>Total query count on <%= @script_1_name %>: <%= total_query_count_1 %></p>
|
16
|
+
<p>Total query count on <%= @script_2_name %>: <%= total_query_count_2 %></p>
|
17
|
+
<button id="toggleButton" onclick="toggleView()">Show Chart View</button>
|
18
|
+
<button id="toggleColumnButton" onclick="toggleColumnContent()">Show SQL</button>
|
19
|
+
|
20
|
+
<div class="center-container">
|
21
|
+
<canvas id="queryCountChart"></canvas>
|
22
|
+
</div>
|
23
|
+
<table id="queryTable">
|
24
|
+
<tr>
|
25
|
+
<th>Table</th>
|
26
|
+
<th>Total Query Count <%= @script_1_name %></th>
|
27
|
+
<th>Total Query Count <%= @script_2_name %></th>
|
28
|
+
<th id="columnHeader">File Path</th>
|
29
|
+
<th>Method</th>
|
30
|
+
<th>Location Count <%= @script_1_name %></th>
|
31
|
+
<th>Location Count <%= @script_2_name %></th>
|
32
|
+
</tr>
|
33
|
+
<% tables.each do |table_name| %>
|
34
|
+
<% locations = (data_1.dig(table_name, :location)&.keys || []) | (data_2.dig(table_name, :location)&.keys || [])%>
|
35
|
+
<tr>
|
36
|
+
<td rowspan="<%= locations.count + 1 %>"><%= table_name %></td>
|
37
|
+
<td rowspan="<%= locations.count + 1 %>"><%= data_1.dig(table_name, :count) || 0%></td>
|
38
|
+
<td rowspan="<%= locations.count + 1 %>"><%= data_2.dig(table_name, :count) || 0 %></td>
|
39
|
+
</tr>
|
40
|
+
<% locations.each do |loc| %>
|
41
|
+
<% details_1 = data_1.dig(table_name, :location, loc) || {}%>
|
42
|
+
<% details_2 = data_2.dig(table_name, :location, loc) || {}%>
|
43
|
+
<% present_detail = details_1 || details_2 %>
|
44
|
+
<tr class="sub-row">
|
45
|
+
<% match = loc&.match(/^(?<file_path>.*):in.*`(?<method>.*)'$/) %>
|
46
|
+
<% file_path = match ? match[:file_path] : loc %>
|
47
|
+
<% method = match ? match[:method] : nil %>
|
48
|
+
<td class="toggle-content">
|
49
|
+
<span class="filepath"><%= file_path %></span>
|
50
|
+
<span class="sql" style="display: none;"><%= present_detail[:sql]&.gsub('"', '') %></span>
|
51
|
+
</td>
|
52
|
+
<td class="method-column"><%= method %></td>
|
53
|
+
<td><%= details_1[:count] || 0 %></td>
|
54
|
+
<td><%= details_2[:count] || 0 %></td>
|
55
|
+
</tr>
|
56
|
+
<% end %>
|
57
|
+
<% end %>
|
58
|
+
</table>
|
59
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
60
|
+
<script>
|
61
|
+
const chartData = <%= chart_data.to_json %>;
|
62
|
+
initializeChartCompare(chartData);
|
63
|
+
</script>
|
64
|
+
</div>
|
65
|
+
</body>
|
66
|
+
</html>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#query_counter_report_gem {
|
2
|
+
font-family: Arial, sans-serif;
|
3
|
+
table {
|
4
|
+
width: 100%;
|
5
|
+
border-collapse: collapse;
|
6
|
+
margin-top: 20px;
|
7
|
+
|
8
|
+
height: 100%;
|
9
|
+
overflow:auto;
|
10
|
+
display: block; /* Ensure the table behaves as a block element */
|
11
|
+
box-sizing: border-box;
|
12
|
+
}
|
13
|
+
th, td {
|
14
|
+
border: 1px solid #ddd;
|
15
|
+
padding: 8px;
|
16
|
+
}
|
17
|
+
th {
|
18
|
+
background-color: #f2f2f2;
|
19
|
+
}
|
20
|
+
tr:nth-child(even) {
|
21
|
+
background-color: #f9f9f9;
|
22
|
+
}
|
23
|
+
.sub-row {
|
24
|
+
background-color: #f9f9f9;
|
25
|
+
}
|
26
|
+
.center-container {
|
27
|
+
text-align: center;
|
28
|
+
width: 100%;
|
29
|
+
height: 500px;
|
30
|
+
}
|
31
|
+
|
32
|
+
#queryCountChart {
|
33
|
+
display: none;
|
34
|
+
margin: 0 auto;
|
35
|
+
width: 1200px;
|
36
|
+
height: 500px;
|
37
|
+
}
|
38
|
+
#queryTable {
|
39
|
+
display: table;
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<script>
|
2
|
+
<%= js_content %>
|
3
|
+
</script>
|
4
|
+
<style>
|
5
|
+
<%= css_content %>
|
6
|
+
</style>
|
7
|
+
<div id='query_counter_report_gem'>
|
8
|
+
<h2>Query Counter Report</h2>
|
9
|
+
<p>Total query count on process: <%= total_query_count %></p>
|
10
|
+
<p>The table will show only tables with more than <%= ::QueryTracker::Configuration.ignore_table_count %> queries</p>
|
11
|
+
<p>Only the top <%= ::QueryTracker::Configuration.max_locations_per_table %> locations with the most occurrences will be shown.</p>
|
12
|
+
<button id="toggleButton" onclick="toggleView()">Show Chart View</button>
|
13
|
+
<button id="toggleColumnButton" onclick="toggleColumnContent()">Show SQL</button>
|
14
|
+
|
15
|
+
<div class="center-container">
|
16
|
+
<canvas id="queryCountChart"></canvas>
|
17
|
+
</div>
|
18
|
+
<table id="queryTable">
|
19
|
+
<tr>
|
20
|
+
<th>Table</th>
|
21
|
+
<th>Total Query Count</th>
|
22
|
+
<th id="columnHeader">File Path</th>
|
23
|
+
<th>Method</th>
|
24
|
+
<th>Location Count</th>
|
25
|
+
</tr>
|
26
|
+
<% filter_data(data).each do |table, info| %>
|
27
|
+
<tr>
|
28
|
+
<td rowspan="<%= info[:location].size + 1 %>"><%= table %></td>
|
29
|
+
<td rowspan="<%= info[:location].size + 1 %>"><%= info[:count] %></td>
|
30
|
+
</tr>
|
31
|
+
<% info[:location].each do |loc, detail| %>
|
32
|
+
<tr class="sub-row">
|
33
|
+
<% match = loc&.match(/^(?<file_path>.*):in.*`(?<method>.*)'$/) %>
|
34
|
+
<% file_path = match ? match[:file_path] : loc %>
|
35
|
+
<% method = match ? match[:method] : nil %>
|
36
|
+
<td class="toggle-content">
|
37
|
+
<span class="filepath"><%= file_path %></span>
|
38
|
+
<span class="sql" style="display: none;"><%= detail[:sql].gsub('"', '') %></span>
|
39
|
+
</td>
|
40
|
+
<td class="method-column"><%= method %></td>
|
41
|
+
<td><%= detail[:count] %></td>
|
42
|
+
</tr>
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
</table>
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
47
|
+
<script>
|
48
|
+
const chartData = <%= chart_data.to_json %>;
|
49
|
+
initializeChart(chartData);
|
50
|
+
</script>
|
51
|
+
</div>
|
@@ -0,0 +1,64 @@
|
|
1
|
+
<style>
|
2
|
+
#query-modal-button {
|
3
|
+
position: fixed;
|
4
|
+
top: 10px;
|
5
|
+
left: 10px;
|
6
|
+
z-index: 1000000;
|
7
|
+
background-color: #007bff;
|
8
|
+
color: white;
|
9
|
+
border: none;
|
10
|
+
padding: 10px;
|
11
|
+
cursor: pointer;
|
12
|
+
}
|
13
|
+
|
14
|
+
#query-modal-button:hover {
|
15
|
+
background-color: #0056b3;
|
16
|
+
}
|
17
|
+
|
18
|
+
#modal-close-button {
|
19
|
+
background-color: #d9534f;
|
20
|
+
color: white;
|
21
|
+
border: none;
|
22
|
+
padding: 5px;
|
23
|
+
cursor: pointer;
|
24
|
+
}
|
25
|
+
|
26
|
+
#modal-close-button:hover {
|
27
|
+
background-color: #c9302c;
|
28
|
+
}
|
29
|
+
|
30
|
+
#modal-content-query-count {
|
31
|
+
display: none;
|
32
|
+
position: fixed;
|
33
|
+
top: 10%;
|
34
|
+
left: 10%;
|
35
|
+
width: 80%;
|
36
|
+
height: 700px;
|
37
|
+
max-height: 80%;
|
38
|
+
background-color: white;
|
39
|
+
border: 1px solid #ccc;
|
40
|
+
padding: 10px;
|
41
|
+
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
42
|
+
z-index: 10000000;
|
43
|
+
overflow: auto;
|
44
|
+
}
|
45
|
+
|
46
|
+
<%= css_content %>
|
47
|
+
</style>
|
48
|
+
<script>
|
49
|
+
function toggleQueryModal() {
|
50
|
+
var modal = document.getElementById('modal-content-query-count');
|
51
|
+
if (modal.style.display === 'none' || modal.style.display === '') {
|
52
|
+
modal.style.display = 'block';
|
53
|
+
} else {
|
54
|
+
modal.style.display = 'none';
|
55
|
+
}
|
56
|
+
}
|
57
|
+
</script>
|
58
|
+
<div id="query-modal-button">
|
59
|
+
<button id="query-modal-button" onclick="toggleQueryModal()">Toggle Query Counter</button>
|
60
|
+
</div>
|
61
|
+
<div id="modal-content-query-count">
|
62
|
+
<button id="modal-close-button" onclick="toggleQueryModal()">Close</button>
|
63
|
+
<%= render_query_counter_base_div %>
|
64
|
+
</div>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module QueryTracker
|
2
|
+
module Recording
|
3
|
+
module Base
|
4
|
+
def start_with_block(printer: :console)
|
5
|
+
raise 'Block not given' unless block_given?
|
6
|
+
|
7
|
+
start_recording
|
8
|
+
yield
|
9
|
+
end_recording(printer:)
|
10
|
+
end
|
11
|
+
|
12
|
+
def start_recording
|
13
|
+
tracker.reset_query_count
|
14
|
+
tracker.subscribe
|
15
|
+
end
|
16
|
+
|
17
|
+
def end_recording(printer: :console)
|
18
|
+
tracker.unsubscribe
|
19
|
+
case printer
|
20
|
+
when :html
|
21
|
+
Printer::Html.new(data: tracker.query_tracker).print
|
22
|
+
when :console
|
23
|
+
Printer::Console.new(data: tracker.query_tracker).print
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module QueryTracker
|
2
|
+
class Tracker
|
3
|
+
REGEX_TABLE_SQL = /FROM\s+"(?<table>[^"]+)"/
|
4
|
+
attr_accessor :query_tracker, :subscription
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
reset_query_count
|
8
|
+
end
|
9
|
+
|
10
|
+
# This assums that in the same location of the code it will always be the same sql query
|
11
|
+
def reset_query_count
|
12
|
+
@query_tracker = Hash.new do |hash, key|
|
13
|
+
hash[key] = { count: 0, location: Hash.new do |loc_hash, loc_key|
|
14
|
+
loc_hash[loc_key] = { count: 0, sql: nil }
|
15
|
+
end }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def subscribe
|
20
|
+
return unless subscription.nil?
|
21
|
+
|
22
|
+
@subscription = ActiveSupport::Notifications.subscribe('sql.active_record') do |_a, _b, _c, _d, payload|
|
23
|
+
caller_from_sql = caller
|
24
|
+
sql = payload[:sql]
|
25
|
+
match = sql.match(REGEX_TABLE_SQL)
|
26
|
+
if match.present? && match[:table]
|
27
|
+
actual_location = Rails.backtrace_cleaner.clean(caller_from_sql).first
|
28
|
+
query_tracker[match[:table]][:count] += 1
|
29
|
+
query_tracker[match[:table]][:location][actual_location][:count] += 1
|
30
|
+
query_tracker[match[:table]][:location][actual_location][:sql] = sql
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def unsubscribe
|
36
|
+
ActiveSupport::Notifications.unsubscribe(@subscription)
|
37
|
+
@subscription = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_support/notifications'
|
2
|
+
require_relative 'query_tracker/version'
|
3
|
+
require_relative 'query_tracker/configuration'
|
4
|
+
require_relative 'query_tracker/printer/base'
|
5
|
+
require_relative 'query_tracker/printer/console'
|
6
|
+
require_relative 'query_tracker/printer/html'
|
7
|
+
require_relative 'query_tracker/recording/base'
|
8
|
+
require_relative 'query_tracker/recording/tracker'
|
9
|
+
require_relative 'query_tracker/compare/comparator'
|
10
|
+
require_relative 'query_tracker/middleware'
|
11
|
+
require_relative 'query_tracker/printer/html_compare'
|
12
|
+
module QueryTracker
|
13
|
+
extend Recording::Base
|
14
|
+
if defined?(Rails::Railtie)
|
15
|
+
class QueryTrackerRailtie < Rails::Railtie
|
16
|
+
initializer 'query_tracker.configure_rails_initialization' do |app|
|
17
|
+
app.middleware.use Middleware
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def configure
|
24
|
+
yield(Configuration)
|
25
|
+
end
|
26
|
+
|
27
|
+
def tracker
|
28
|
+
Thread.current[:query_counter_data] ||= Tracker.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def compare
|
32
|
+
comparator = Compare::Comparator.new
|
33
|
+
yield(comparator)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: query_tracker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jose Lara
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-07-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colorize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: launchy
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nokogiri
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.16.5
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.16.5
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '6.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '6.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mocha
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry-byebug
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: ruby-lsp
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: shoulda-context
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: shoulda-matchers
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '6.0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '6.0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: sqlite3
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '1.4'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '1.4'
|
209
|
+
description:
|
210
|
+
email:
|
211
|
+
- jvlara@uc.cl
|
212
|
+
executables: []
|
213
|
+
extensions: []
|
214
|
+
extra_rdoc_files: []
|
215
|
+
files:
|
216
|
+
- README.md
|
217
|
+
- bin/setup
|
218
|
+
- lib/query_tracker.rb
|
219
|
+
- lib/query_tracker/compare/comparator.rb
|
220
|
+
- lib/query_tracker/configuration.rb
|
221
|
+
- lib/query_tracker/middleware.rb
|
222
|
+
- lib/query_tracker/printer/base.rb
|
223
|
+
- lib/query_tracker/printer/console.rb
|
224
|
+
- lib/query_tracker/printer/html.rb
|
225
|
+
- lib/query_tracker/printer/html_compare.rb
|
226
|
+
- lib/query_tracker/printer/templates/bar_chart.js
|
227
|
+
- lib/query_tracker/printer/templates/comparing.html.erb
|
228
|
+
- lib/query_tracker/printer/templates/style.css
|
229
|
+
- lib/query_tracker/printer/templates/template.html.erb
|
230
|
+
- lib/query_tracker/printer/templates/template_base_query_counter.html.erb
|
231
|
+
- lib/query_tracker/printer/templates/template_for_inject.html.erb
|
232
|
+
- lib/query_tracker/recording/base.rb
|
233
|
+
- lib/query_tracker/recording/tracker.rb
|
234
|
+
- lib/query_tracker/version.rb
|
235
|
+
homepage: https://github.com/jvlara/query_tracker
|
236
|
+
licenses:
|
237
|
+
- MIT
|
238
|
+
metadata: {}
|
239
|
+
post_install_message:
|
240
|
+
rdoc_options: []
|
241
|
+
require_paths:
|
242
|
+
- lib
|
243
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
244
|
+
requirements:
|
245
|
+
- - ">="
|
246
|
+
- !ruby/object:Gem::Version
|
247
|
+
version: 3.2.2
|
248
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
249
|
+
requirements:
|
250
|
+
- - ">="
|
251
|
+
- !ruby/object:Gem::Version
|
252
|
+
version: '0'
|
253
|
+
requirements: []
|
254
|
+
rubygems_version: 3.4.10
|
255
|
+
signing_key:
|
256
|
+
specification_version: 4
|
257
|
+
summary: Display an overview of quantity of queries and their origin in Rails applications.
|
258
|
+
test_files: []
|