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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2654c621778910c1a0b1926398480e21b967f86c63ff45b642e635d80bfebcd0
4
- data.tar.gz: 54245db5c8aa3a64869f3ce18b9c95b662d01ba58a042150ea551f19e1bf7299
3
+ metadata.gz: daca52db3c3f2564dc0e0e51b2271e995db9042ffd82d2b2a2ef25529fc5cbd4
4
+ data.tar.gz: 47cad0afa15471a0fb60dc6366dbadcc5bc480b2cf7b17321198230fd46f8d8b
5
5
  SHA512:
6
- metadata.gz: 5d1a2a46359810b4f5e482097264c6d1d402c4344f5cf29b672d89a9c834c04e81936f07f3111454abf549e12c5a30614e6829c5d62563860de6b625220ce234
7
- data.tar.gz: 41561e881c73aac6ed2ce8b7d3fc4f1a9f83d35fe92f4473713d274978451d9d7cd4ae7fada1af25739d3222457216ae13fdfb015b41bc8cf87116ee8aaa9b1a
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 { color: var(--muted); font-style: italic; margin-top: 1rem; }
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"] } if filters["type"].present?
13
- events = events.select { |e| e[:controller] == filters["controller"] } if filters["controller"].present?
14
- events = events.select { |e| e[:action] == filters["action"] } if filters["action"].present?
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 { @events = events.reverse }
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
- def check_dashboard_enabled
25
- head :forbidden unless QueryOwl.config.dashboard_enabled
26
- end
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,6 @@
1
+ import "@hotwired/turbo"
2
+ import { Application } from "@hotwired/stimulus"
3
+ import TableFilterController from "query_owl/table_filter_controller"
4
+
5
+ const application = Application.start()
6
+ application.register("table-filter", TableFilterController)
@@ -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
+ }
@@ -6,6 +6,7 @@
6
6
  <title>QueryOwl</title>
7
7
  <link rel="icon" href="data:,">
8
8
  <%= inline_styles %>
9
+ <%= javascript_importmap_tags "query_owl" %>
9
10
  </head>
10
11
  <body>
11
12
  <%= yield %>
@@ -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
- <% if @events.empty? %>
7
- <p class="qo-empty">No events detected yet.</p>
8
- <% else %>
9
- <table class="qo-table">
10
- <thead>
11
- <tr>
12
- <th>Type</th>
13
- <th>SQL / Details</th>
14
- <th>Info</th>
15
- <th>Request</th>
16
- <th>Recorded At</th>
17
- <th>Backtrace</th>
18
- </tr>
19
- </thead>
20
- <tbody>
21
- <% @events.each do |event| %>
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
- <td><span class="qo-badge qo-badge--<%= event[:type] %>"><%= event[:type].to_s.tr("_", " ") %></span></td>
24
- <td class="qo-monospace"><%= event[:sql] || "#{event[:model]}##{event[:association]}" %></td>
25
- <td class="qo-muted">
26
- <% if event[:count] %>count: <%= event[:count] %><% end %>
27
- <% if event[:duration_ms] %><%= event[:duration_ms] %>ms<% end %>
28
- </td>
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
- <% end %>
41
- </tbody>
42
- </table>
43
- <% end %>
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>
@@ -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).
@@ -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
@@ -1,3 +1,3 @@
1
1
  module QueryOwl
2
- VERSION = "0.5.0"
2
+ VERSION = "0.7.0"
3
3
  end
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.5.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