query_owl 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/query_owl/_02_layout.css +2 -1
- data/app/assets/stylesheets/query_owl/_03_table.css +10 -1
- data/app/assets/stylesheets/query_owl/_05_filters.css +80 -0
- data/app/controllers/query_owl/slow_queries_controller.rb +27 -7
- data/app/helpers/query_owl/application_helper.rb +16 -0
- data/app/javascript/query_owl/application.js +6 -0
- data/app/javascript/query_owl/table_filter_controller.js +32 -0
- data/app/views/layouts/query_owl/application.html.erb +1 -0
- data/app/views/query_owl/slow_queries/index.html.erb +75 -37
- data/config/importmap.rb +4 -0
- data/lib/generators/query_owl/install/templates/initializer.rb +13 -0
- data/lib/query_owl/engine.rb +16 -0
- data/lib/query_owl/test_helper.rb +85 -0
- data/lib/query_owl/version.rb +1 -1
- metadata +34 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: daca52db3c3f2564dc0e0e51b2271e995db9042ffd82d2b2a2ef25529fc5cbd4
|
|
4
|
+
data.tar.gz: 47cad0afa15471a0fb60dc6366dbadcc5bc480b2cf7b17321198230fd46f8d8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82cb7e1f4f42c553731c3e0ba32f4b4f7f01a0bf21c837644371fcb02313d4995f27f84fd70f574df3f6ef889266ddec6ba2c7bbf3c5302f3ee0c1084d431539
|
|
7
|
+
data.tar.gz: 8bf2d7078c201e0bb673943194dbb3d98d07e30a1ce6a9c64ac6c72f0416870aca1349a21050779c7edbc92c1d5f2609219d44fed7745591d60ca83dfe98c387
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
.qo-header h1 { font-size: 1.4rem; font-weight: 600; }
|
|
3
3
|
.qo-header p { color: var(--muted); margin-top: 0.2rem; }
|
|
4
4
|
|
|
5
|
-
.qo-empty
|
|
5
|
+
.qo-empty { color: var(--muted); font-style: italic; margin-top: 1rem; }
|
|
6
|
+
.qo-summary { margin-bottom: 0.5rem; }
|
|
@@ -31,4 +31,13 @@
|
|
|
31
31
|
.qo-table td:nth-child(2) { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
32
32
|
|
|
33
33
|
.qo-request { white-space: nowrap; }
|
|
34
|
-
.qo-path { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 11px; opacity: .7; }
|
|
34
|
+
.qo-path { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 11px; opacity: .7; }
|
|
35
|
+
|
|
36
|
+
.qo-sort-link {
|
|
37
|
+
color: inherit;
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
display: block;
|
|
40
|
+
}
|
|
41
|
+
.qo-sort-link:hover { color: var(--accent, #4f46e5); }
|
|
42
|
+
|
|
43
|
+
.qo-sort-active { color: var(--accent, #4f46e5); }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.qo-filters {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
margin-bottom: 1rem;
|
|
6
|
+
padding: 0.6rem 1rem;
|
|
7
|
+
background: var(--surface);
|
|
8
|
+
border-radius: var(--radius);
|
|
9
|
+
box-shadow: var(--shadow);
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.qo-filters label {
|
|
14
|
+
font-size: 11px;
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
text-transform: uppercase;
|
|
17
|
+
letter-spacing: .04em;
|
|
18
|
+
color: var(--muted);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.qo-select,
|
|
22
|
+
.qo-input {
|
|
23
|
+
font-family: inherit;
|
|
24
|
+
font-size: 13px;
|
|
25
|
+
padding: 0.3rem 0.6rem;
|
|
26
|
+
border: 1px solid var(--border);
|
|
27
|
+
border-radius: var(--radius);
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.qo-input { min-width: 160px; }
|
|
33
|
+
|
|
34
|
+
.qo-input-wrapper {
|
|
35
|
+
position: relative;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.qo-input-wrapper .qo-input { padding-right: 1.6rem; }
|
|
41
|
+
|
|
42
|
+
.qo-input-clear {
|
|
43
|
+
position: absolute;
|
|
44
|
+
right: 0.4rem;
|
|
45
|
+
background: none;
|
|
46
|
+
border: none;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
color: var(--muted);
|
|
49
|
+
font-size: 11px;
|
|
50
|
+
line-height: 1;
|
|
51
|
+
padding: 0.15rem 0.2rem;
|
|
52
|
+
border-radius: 2px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.qo-input-clear:hover { color: var(--text); background: var(--border); }
|
|
56
|
+
|
|
57
|
+
.qo-select:focus,
|
|
58
|
+
.qo-input:focus {
|
|
59
|
+
outline: none;
|
|
60
|
+
border-color: #86b7fe;
|
|
61
|
+
box-shadow: 0 0 0 2px rgba(13,110,253,.15);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.qo-btn {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
padding: 0.3rem 0.75rem;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
border-radius: var(--radius);
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
font-family: inherit;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.qo-btn--muted {
|
|
75
|
+
background: transparent;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
border: 1px solid var(--border);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.qo-btn--muted:hover { background: var(--bg); color: var(--text); }
|
|
@@ -6,23 +6,43 @@ module QueryOwl
|
|
|
6
6
|
|
|
7
7
|
before_action :check_dashboard_enabled, if: -> { request.format.html? }
|
|
8
8
|
|
|
9
|
+
SORTABLE_COLUMNS = %w[type info recorded_at].freeze
|
|
10
|
+
|
|
9
11
|
def index
|
|
10
12
|
filters = request.query_parameters
|
|
11
13
|
events = EventStore.all
|
|
12
|
-
events = events.select { |e| e[:type].to_s == filters["type"] }
|
|
13
|
-
events = events.select { |e| e[:controller]
|
|
14
|
-
events = events.select { |e| e[:action] == filters["action"] }
|
|
14
|
+
events = events.select { |e| e[:type].to_s == filters["type"] } if filters["type"].present?
|
|
15
|
+
events = events.select { |e| e[:controller].to_s.include?(filters["controller"]) } if filters["controller"].present?
|
|
16
|
+
events = events.select { |e| e[:action] == filters["action"] } if filters["action"].present?
|
|
15
17
|
|
|
16
18
|
respond_to do |format|
|
|
17
19
|
format.json { render json: events }
|
|
18
|
-
format.html
|
|
20
|
+
format.html do
|
|
21
|
+
@type_filter = filters["type"].presence
|
|
22
|
+
@controller_filter = filters["controller"].presence
|
|
23
|
+
@sort = SORTABLE_COLUMNS.include?(filters["sort"]) ? filters["sort"] : "recorded_at"
|
|
24
|
+
@direction = filters["direction"] == "asc" ? "asc" : "desc"
|
|
25
|
+
@events = sorted_events(events, @sort, @direction)
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
private
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
def sorted_events(events, sort, direction)
|
|
33
|
+
sorted = case sort
|
|
34
|
+
when "type"
|
|
35
|
+
events.sort_by { |e| e[:type].to_s }
|
|
36
|
+
when "info"
|
|
37
|
+
events.sort_by { |e| e[:duration_ms] || e[:count] || 0 }
|
|
38
|
+
else
|
|
39
|
+
events.sort_by { |e| e[:recorded_at] || Time.at(0) }
|
|
40
|
+
end
|
|
41
|
+
direction == "asc" ? sorted : sorted.reverse
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check_dashboard_enabled
|
|
45
|
+
head :forbidden unless QueryOwl.config.dashboard_enabled
|
|
46
|
+
end
|
|
27
47
|
end
|
|
28
48
|
end
|
|
@@ -5,5 +5,21 @@ module QueryOwl
|
|
|
5
5
|
css = dir.glob("_*.css").sort.map(&:read).join("\n")
|
|
6
6
|
content_tag(:style, css.html_safe)
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
def sort_th(label, column, current_sort, current_dir)
|
|
10
|
+
active = current_sort == column.to_s
|
|
11
|
+
next_dir = (active && current_dir == "asc") ? "desc" : "asc"
|
|
12
|
+
indicator = active ? (current_dir == "asc" ? " ▲" : " ▼") : ""
|
|
13
|
+
params = request.query_parameters.merge("sort" => column, "direction" => next_dir)
|
|
14
|
+
href = "?" + params.to_query
|
|
15
|
+
content_tag(:th) do
|
|
16
|
+
link_to(
|
|
17
|
+
"#{label}#{indicator}".html_safe,
|
|
18
|
+
href,
|
|
19
|
+
class: ["qo-sort-link", ("qo-sort-active" if active)].compact.join(" "),
|
|
20
|
+
data: { turbo_frame: "qo-events" }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
8
24
|
end
|
|
9
25
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["controllerInput", "clearButton"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this._updateClear()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
filter({ target }) {
|
|
11
|
+
clearTimeout(this._timer)
|
|
12
|
+
this._updateClear()
|
|
13
|
+
this._timer = setTimeout(() => target.form.requestSubmit(), 300)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
select({ target }) {
|
|
17
|
+
clearTimeout(this._timer)
|
|
18
|
+
target.form.requestSubmit()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
clearController() {
|
|
22
|
+
this.controllerInputTarget.value = ""
|
|
23
|
+
this._updateClear()
|
|
24
|
+
this.controllerInputTarget.form.requestSubmit()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_updateClear() {
|
|
28
|
+
if (this.hasClearButtonTarget) {
|
|
29
|
+
this.clearButtonTarget.hidden = this.controllerInputTarget.value.length === 0
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,43 +1,81 @@
|
|
|
1
1
|
<div class="qo-header">
|
|
2
2
|
<h1>QueryOwl</h1>
|
|
3
|
-
<p>Last <%= @events.length %> detected event<%= "s" unless @events.length == 1 %> (newest first)</p>
|
|
4
3
|
</div>
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
<form class="qo-filters" action="<%= slow_queries_path %>" method="get"
|
|
6
|
+
data-controller="table-filter"
|
|
7
|
+
data-turbo-frame="qo-events">
|
|
8
|
+
<input type="hidden" name="sort" value="<%= @sort %>">
|
|
9
|
+
<input type="hidden" name="direction" value="<%= @direction %>">
|
|
10
|
+
|
|
11
|
+
<label for="qo-type-filter">Type</label>
|
|
12
|
+
<select id="qo-type-filter" name="type" class="qo-select"
|
|
13
|
+
data-action="change->table-filter#select">
|
|
14
|
+
<option value="">All</option>
|
|
15
|
+
<option value="n_plus_one" <%= "selected" if @type_filter == "n_plus_one" %>>N+1</option>
|
|
16
|
+
<option value="slow_query" <%= "selected" if @type_filter == "slow_query" %>>Slow Query</option>
|
|
17
|
+
<option value="unused_eager_load" <%= "selected" if @type_filter == "unused_eager_load" %>>Unused Eager Load</option>
|
|
18
|
+
</select>
|
|
19
|
+
|
|
20
|
+
<label for="qo-controller-filter">Controller</label>
|
|
21
|
+
<div class="qo-input-wrapper">
|
|
22
|
+
<input id="qo-controller-filter" type="text" name="controller" class="qo-input"
|
|
23
|
+
placeholder="e.g. widgets"
|
|
24
|
+
value="<%= @controller_filter %>"
|
|
25
|
+
data-table-filter-target="controllerInput"
|
|
26
|
+
data-action="input->table-filter#filter">
|
|
27
|
+
<button type="button" class="qo-input-clear" aria-label="Clear controller filter"
|
|
28
|
+
data-table-filter-target="clearButton"
|
|
29
|
+
data-action="click->table-filter#clearController"
|
|
30
|
+
<%= "hidden" if @controller_filter.blank? %>>✕</button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% if @type_filter.present? || @controller_filter.present? %>
|
|
34
|
+
<%= link_to "Clear", slow_queries_path, class: "qo-btn qo-btn--muted" %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</form>
|
|
37
|
+
|
|
38
|
+
<turbo-frame id="qo-events">
|
|
39
|
+
<p class="qo-summary qo-muted">
|
|
40
|
+
<%= @events.length %> event<%= "s" unless @events.length == 1 %><%= " (filtered)" if @type_filter.present? || @controller_filter.present? %>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<% if @events.empty? %>
|
|
44
|
+
<p class="qo-empty">No events detected yet.</p>
|
|
45
|
+
<% else %>
|
|
46
|
+
<table class="qo-table">
|
|
47
|
+
<thead>
|
|
22
48
|
<tr>
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</
|
|
29
|
-
<td class="qo-request qo-muted">
|
|
30
|
-
<% if event[:controller] || event[:action] %>
|
|
31
|
-
<span class="qo-monospace"><%= [event[:controller], event[:action]].compact.join("#") %></span>
|
|
32
|
-
<% end %>
|
|
33
|
-
<% if event[:path] %>
|
|
34
|
-
<br><span class="qo-path"><%= event[:path] %></span>
|
|
35
|
-
<% end %>
|
|
36
|
-
</td>
|
|
37
|
-
<td class="qo-muted"><%= event[:recorded_at]&.strftime("%H:%M:%S") %></td>
|
|
38
|
-
<td class="qo-monospace qo-muted"><%= Array(event[:backtrace]).first %></td>
|
|
49
|
+
<%= sort_th("Type", "type", @sort, @direction) %>
|
|
50
|
+
<th>SQL / Details</th>
|
|
51
|
+
<%= sort_th("Info", "info", @sort, @direction) %>
|
|
52
|
+
<th>Request</th>
|
|
53
|
+
<%= sort_th("Recorded At", "recorded_at", @sort, @direction) %>
|
|
54
|
+
<th>Backtrace</th>
|
|
39
55
|
</tr>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
</thead>
|
|
57
|
+
<tbody>
|
|
58
|
+
<% @events.each do |event| %>
|
|
59
|
+
<tr>
|
|
60
|
+
<td><span class="qo-badge qo-badge--<%= event[:type] %>"><%= event[:type].to_s.tr("_", " ") %></span></td>
|
|
61
|
+
<td class="qo-monospace"><%= event[:sql] || "#{event[:model]}##{event[:association]}" %></td>
|
|
62
|
+
<td class="qo-muted">
|
|
63
|
+
<% if event[:count] %>count: <%= event[:count] %><% end %>
|
|
64
|
+
<% if event[:duration_ms] %><%= event[:duration_ms] %>ms<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
<td class="qo-request qo-muted">
|
|
67
|
+
<% if event[:controller] || event[:action] %>
|
|
68
|
+
<span class="qo-monospace"><%= [event[:controller], event[:action]].compact.join("#") %></span>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if event[:path] %>
|
|
71
|
+
<br><span class="qo-path"><%= event[:path] %></span>
|
|
72
|
+
<% end %>
|
|
73
|
+
</td>
|
|
74
|
+
<td class="qo-muted"><%= event[:recorded_at]&.strftime("%H:%M:%S") %></td>
|
|
75
|
+
<td class="qo-monospace qo-muted"><%= Array(event[:backtrace]).first %></td>
|
|
76
|
+
</tr>
|
|
77
|
+
<% end %>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
<% end %>
|
|
81
|
+
</turbo-frame>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
|
|
2
|
+
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
3
|
+
pin "query_owl", to: "query_owl/application.js"
|
|
4
|
+
pin "query_owl/table_filter_controller", to: "query_owl/table_filter_controller.js"
|
|
@@ -41,6 +41,19 @@ QueryOwl.configure do |config|
|
|
|
41
41
|
# Controllers to skip — matched against the Rails controller name (e.g. "rails/health").
|
|
42
42
|
# config.ignore_controllers = ["rails/health", "admin/metrics"]
|
|
43
43
|
|
|
44
|
+
# Test helper — opt-in RSpec matchers and Minitest assertions.
|
|
45
|
+
# Add to spec/rails_helper.rb (or test/test_helper.rb for Minitest):
|
|
46
|
+
#
|
|
47
|
+
# require "query_owl/test_helper"
|
|
48
|
+
# RSpec.configure { |c| c.include QueryOwl::TestHelper }
|
|
49
|
+
# # or: class ActiveSupport::TestCase; include QueryOwl::TestHelper; end
|
|
50
|
+
#
|
|
51
|
+
# Then use: expect { }.not_to trigger_n_plus_one
|
|
52
|
+
# expect { }.not_to trigger_slow_query
|
|
53
|
+
# expect { }.not_to trigger_unused_eager_load
|
|
54
|
+
# assert_no_n_plus_one { }
|
|
55
|
+
# assert_no_slow_query { }
|
|
56
|
+
|
|
44
57
|
# Notifiers receive each detected event via #call(event).
|
|
45
58
|
# Defaults to [QueryOwl::Notifiers::Logger] which writes to Rails.logger.
|
|
46
59
|
# Use Console for TTY-aware colorized output (yellow: N+1, red: slow query).
|
data/lib/query_owl/engine.rb
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
require "turbo-rails"
|
|
2
|
+
require "importmap-rails"
|
|
3
|
+
|
|
1
4
|
module QueryOwl
|
|
2
5
|
class Engine < ::Rails::Engine
|
|
3
6
|
isolate_namespace QueryOwl
|
|
4
7
|
config.generators.api_only = true
|
|
5
8
|
|
|
9
|
+
initializer "query_owl.assets" do |app|
|
|
10
|
+
if app.config.respond_to?(:assets)
|
|
11
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "query_owl.importmap", before: "importmap" do |app|
|
|
16
|
+
if app.config.respond_to?(:importmap)
|
|
17
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
18
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
initializer "query_owl.subscribe" do
|
|
7
23
|
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
8
24
|
next unless QueryOwl.config.enabled
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "query_owl"
|
|
2
|
+
|
|
3
|
+
module QueryOwl
|
|
4
|
+
module TestHelper
|
|
5
|
+
# Runs block with QueryOwl's trackers active and returns detected events.
|
|
6
|
+
# Isolated from config.enabled and config.raise_on_n_plus_one.
|
|
7
|
+
def self.capture_events
|
|
8
|
+
QueryTracker.start!
|
|
9
|
+
EagerLoadTracker.start!
|
|
10
|
+
yield
|
|
11
|
+
queries = QueryTracker.stop!
|
|
12
|
+
eager_data = EagerLoadTracker.stop!
|
|
13
|
+
Detector.detect_n_plus_one(queries) +
|
|
14
|
+
Detector.detect_slow_queries(queries) +
|
|
15
|
+
Detector.detect_unused_eager_loads(eager_data)
|
|
16
|
+
rescue
|
|
17
|
+
QueryTracker.stop!
|
|
18
|
+
EagerLoadTracker.stop!
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# RSpec block matchers — use with expect { }.to / not_to
|
|
23
|
+
|
|
24
|
+
def trigger_n_plus_one
|
|
25
|
+
EventTypeMatcher.new(:n_plus_one)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def trigger_slow_query
|
|
29
|
+
EventTypeMatcher.new(:slow_query)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def trigger_unused_eager_load
|
|
33
|
+
EventTypeMatcher.new(:unused_eager_load)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Minitest assertions — call assert_no_n_plus_one { } inside a test method
|
|
37
|
+
|
|
38
|
+
def assert_no_n_plus_one(msg = nil, &block)
|
|
39
|
+
events = QueryOwl::TestHelper.capture_events(&block)
|
|
40
|
+
count = events.count { |e| e[:type] == :n_plus_one }
|
|
41
|
+
assert count.zero?, msg || "Expected no N+1 queries, but #{count} detected"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def assert_no_slow_query(msg = nil, &block)
|
|
45
|
+
events = QueryOwl::TestHelper.capture_events(&block)
|
|
46
|
+
count = events.count { |e| e[:type] == :slow_query }
|
|
47
|
+
assert count.zero?, msg || "Expected no slow queries, but #{count} detected"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class EventTypeMatcher
|
|
51
|
+
def initialize(type)
|
|
52
|
+
@type = type
|
|
53
|
+
@events = []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def matches?(block)
|
|
57
|
+
@events = QueryOwl::TestHelper.capture_events(&block)
|
|
58
|
+
@events.any? { |e| e[:type] == @type }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def does_not_match?(block)
|
|
62
|
+
!matches?(block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def supports_block_expectations?
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def failure_message
|
|
70
|
+
"expected block to trigger #{label} but none were detected"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def failure_message_when_negated
|
|
74
|
+
n = @events.count { |e| e[:type] == @type }
|
|
75
|
+
"expected block not to trigger #{label} but #{n} detected"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def label
|
|
81
|
+
@type.to_s.tr("_", " ")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/query_owl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query_owl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -23,6 +23,34 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: importmap-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: turbo-rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
26
54
|
description: A leaner alternative to Bullet. Detects N+1 queries and slow queries
|
|
27
55
|
in development, logging structured warnings to your Rails logger without the noise.
|
|
28
56
|
email:
|
|
@@ -38,14 +66,18 @@ files:
|
|
|
38
66
|
- app/assets/stylesheets/query_owl/_02_layout.css
|
|
39
67
|
- app/assets/stylesheets/query_owl/_03_table.css
|
|
40
68
|
- app/assets/stylesheets/query_owl/_04_badges.css
|
|
69
|
+
- app/assets/stylesheets/query_owl/_05_filters.css
|
|
41
70
|
- app/controllers/query_owl/application_controller.rb
|
|
42
71
|
- app/controllers/query_owl/slow_queries_controller.rb
|
|
43
72
|
- app/helpers/query_owl/application_helper.rb
|
|
73
|
+
- app/javascript/query_owl/application.js
|
|
74
|
+
- app/javascript/query_owl/table_filter_controller.js
|
|
44
75
|
- app/jobs/query_owl/application_job.rb
|
|
45
76
|
- app/mailers/query_owl/application_mailer.rb
|
|
46
77
|
- app/models/query_owl/application_record.rb
|
|
47
78
|
- app/views/layouts/query_owl/application.html.erb
|
|
48
79
|
- app/views/query_owl/slow_queries/index.html.erb
|
|
80
|
+
- config/importmap.rb
|
|
49
81
|
- config/routes.rb
|
|
50
82
|
- lib/generators/query_owl/install/install_generator.rb
|
|
51
83
|
- lib/generators/query_owl/install/templates/initializer.rb
|
|
@@ -63,6 +95,7 @@ files:
|
|
|
63
95
|
- lib/query_owl/notifiers/stdout.rb
|
|
64
96
|
- lib/query_owl/query_tracker.rb
|
|
65
97
|
- lib/query_owl/request_context.rb
|
|
98
|
+
- lib/query_owl/test_helper.rb
|
|
66
99
|
- lib/query_owl/version.rb
|
|
67
100
|
- lib/tasks/query_owl_tasks.rake
|
|
68
101
|
homepage: https://github.com/eclectic-coding/query_owl
|