rails_pulse 0.4.0 → 0.4.1

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: 851f283c6c93bde26b1f79d3053a9cb7888381eedb0cd82118fdfbae6abf2a59
4
- data.tar.gz: 622ea59d374bac1a3aa0b1dea374d86537b8bf815c1546b00c1545ee1e6787f7
3
+ metadata.gz: f1a0c55a1386d00790c4bada3da5ed8b34c17e670c7bf1a0c8c55a55c662b16c
4
+ data.tar.gz: 6de45806d2c72656dcc2ef06e61e00a7dd013e826a69a3f12f49cecf9efd360e
5
5
  SHA512:
6
- metadata.gz: f4462e133722e0ef971e3f3c75fe0ea26f23c8167f24d31a8817550c6b91fe1cfd0bf6a35f59d29bd32cc432a9951de77fecdbb09e131fb385ddfe628d6ab660
7
- data.tar.gz: 0c7d9706fc3a128b7c48fc4d09b78a698ff00bdd2e9b110e360c8124d5fccb5e552e4d351da70623f5d392c4c69916dbd39f930e0c1ef62eac2d3832db481cb5
6
+ metadata.gz: a946a339e72c604b0202f28f2e5650f9034619c1737f308cc14a640331a9d25ed60cfa86fc03c2c5a5bc9ab0793ab661844c93bcd1d394d0eedf7984e2fca211
7
+ data.tar.gz: ebb59bb9f9c60c0a9b5ce5ab0a778d08396a841cab960b6f4f4a16a41ca86a38e5ab54e3902b949327b84f5bb6618a7de35366993fded467e8a3b6e5e946ddc6
data/CHANGELOG.md CHANGED
@@ -7,7 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.4.0] - 2026-09-22
10
+ ## [0.4.1] - 2026-09-23
11
+
12
+ ### Added
13
+
14
+ - A test that fails, naming the key, when a default in `RailsPulse::Configuration` and the install template drift apart. (#272)
15
+
16
+ ### Changed
17
+
18
+ - Documented what a tracked request costs, measured against the bounded writer: about 25 microseconds of request-thread overhead per request on SQLite and PostgreSQL. (#275)
19
+ - The release gate and CI now fail if anything under `app/` branches on `Rails.env`, so a code path the test suite cannot reach can no longer ship. (#273)
20
+ - Loosened the `ransack` dependency from `~> 4.0` to `>= 4.0, < 6` so host apps can upgrade to ransack 5 without dropping Rails Pulse.
21
+ - `rake test` now refuses to run when the dummy database was last set up for a different adapter, instead of loading that adapter's `schema.rb` and failing two index tests. (#276)
22
+
23
+ ### Fixed
24
+
25
+ - SQL normalization no longer raises `Regexp::TimeoutError` on long queries, which could fail the whole request when N+1 detection ran during collection.
26
+ - **Metric card headline numbers now honour the global tag filter.** Disabling a tag changed each card's sparkline but not the number beside it, so the two disagreed. Both are now computed over the same summaries. (#274)
11
27
 
12
28
  ## [0.4.0] - 2026-09-22
13
29
 
@@ -62,10 +62,10 @@ module RailsPulse
62
62
  period_start: range_start..now
63
63
  )
64
64
 
65
- # Apply tag filters if available
66
- if respond_to?(:disabled_tags, true) && respond_to?(:show_non_tagged, true)
67
- query = query.with_tag_filters(@disabled_tags, @show_non_tagged)
68
- end
65
+ # The same tag filter the sparkline applies, so the headline number and
66
+ # the sparkline beside it are computed over the same summaries. Cards
67
+ # receive the filter as instance variables; nil means unfiltered.
68
+ query = query.with_tag_filters(@disabled_tags || [], @show_non_tagged != false)
69
69
 
70
70
  # Filter to specific resource if provided
71
71
  query = query.where(summarizable_id: subject_id) if subject_id
@@ -1,5 +1,15 @@
1
1
  module RailsPulse
2
2
  class SqlQueryNormalizer
3
+ # Above this length, skip the regex-based literal scan in favor of a
4
+ # slower-per-char but backtracking-free manual scan. Regex matching here
5
+ # is linear (benchmarked at ~0.4s for an 8,000,000-char literal under a
6
+ # 1s Regexp.timeout), but a host app can configure Regexp.timeout far
7
+ # below that, and very long queries are rare enough that the fast path
8
+ # isn't worth the risk. 100_000 chars leaves generous headroom below
9
+ # where even an aggressive timeout config could plausibly trip — see
10
+ # issue #286.
11
+ LONG_QUERY_THRESHOLD = 100_000
12
+
3
13
  # Smart normalization: preserve table/column names, replace only literal values
4
14
  def self.normalize(query_string)
5
15
  new(query_string).normalize
@@ -72,6 +82,14 @@ module RailsPulse
72
82
  def replace_literal_values(query)
73
83
  normalized = query.dup
74
84
 
85
+ # Replace string literals (single quotes) FIRST so a very long literal
86
+ # collapses to a single "?" before any other regex in this method has
87
+ # to scan across it — see LONG_QUERY_THRESHOLD and issue #286.
88
+ normalized = replace_quoted_literals(normalized, "'")
89
+
90
+ # Replace double-quoted string literals (not protected identifiers)
91
+ normalized = replace_quoted_literals(normalized, '"')
92
+
75
93
  # Replace floating-point numbers FIRST (before integers) to avoid double replacement
76
94
  normalized = normalized.gsub(/(?<![a-zA-Z_])\b\d+\.\d+\b(?![a-zA-Z_])/, "?")
77
95
 
@@ -79,18 +97,78 @@ module RailsPulse
79
97
  # Negative lookbehind/lookahead prevents replacing numbers in table/column names
80
98
  normalized = normalized.gsub(/(?<![a-zA-Z_])\b\d+\b(?![a-zA-Z_])/, "?")
81
99
 
82
- # Replace string literals (single quotes)
83
- normalized = normalized.gsub(/'(?:[^']|'')*'/, "?")
84
-
85
- # Replace double-quoted string literals (not protected identifiers)
86
- normalized = normalized.gsub(/"(?:[^"]|"")*"/, "?")
87
-
88
100
  # Handle boolean literals
89
101
  normalized = normalized.gsub(/\b(true|false)\b/i, "?")
90
102
 
91
103
  normalized
92
104
  end
93
105
 
106
+ # Replaces quote_char-delimited string literals with a single placeholder.
107
+ # Uses a regex for the common case (fast — see benchmarks in
108
+ # LONG_QUERY_THRESHOLD's comment) and falls back to a manual scan for
109
+ # very long queries, where that regex risks Regexp::TimeoutError under
110
+ # an aggressively configured Regexp.timeout — see issue #286.
111
+ def replace_quoted_literals(query, quote_char)
112
+ if query.length < LONG_QUERY_THRESHOLD
113
+ pattern = quote_char == "'" ? /'(?:[^']|'')*'/ : /"(?:[^"]|"")*"/
114
+ query.gsub(pattern, "?")
115
+ else
116
+ scan_quoted_literals(query, quote_char)
117
+ end
118
+ end
119
+
120
+ # Linear, non-regex scan used above LONG_QUERY_THRESHOLD. A quote left
121
+ # unterminated for the rest of the string is left untouched rather than
122
+ # rescanned from every possible start position, which is what would make
123
+ # a naive manual reimplementation quadratic.
124
+ def scan_quoted_literals(query, quote_char)
125
+ result = +""
126
+ i = 0
127
+ len = query.length
128
+
129
+ while i < len
130
+ char = query[i]
131
+
132
+ unless char == quote_char
133
+ result << char
134
+ i += 1
135
+ next
136
+ end
137
+
138
+ close_index = find_closing_quote(query, i + 1, quote_char)
139
+
140
+ if close_index
141
+ result << "?"
142
+ i = close_index + 1
143
+ else
144
+ result << query[i..]
145
+ break
146
+ end
147
+ end
148
+
149
+ result
150
+ end
151
+
152
+ # Returns the index of the unescaped closing quote_char starting the
153
+ # search at `start`, treating a doubled quote_char (e.g. '') as an
154
+ # escaped literal quote rather than a terminator. Returns nil if the
155
+ # string is never closed.
156
+ def find_closing_quote(query, start, quote_char)
157
+ j = start
158
+ len = query.length
159
+
160
+ while j < len
161
+ if query[j] == quote_char
162
+ return j unless query[j + 1] == quote_char
163
+ j += 2
164
+ else
165
+ j += 1
166
+ end
167
+ end
168
+
169
+ nil
170
+ end
171
+
94
172
  def handle_special_constructs(query)
95
173
  normalized = normalize_in_clauses(query)
96
174
  normalized.gsub(/\bBETWEEN\s+\?\s+AND\s+\?/i, "BETWEEN ? AND ?")
@@ -69,7 +69,7 @@
69
69
  </div>
70
70
 
71
71
  <div class="flex items-center gap">
72
- <%= link_to rails_pulse.root_path, class: "flex items-center gap mie-2" do %>
72
+ <%= link_to rails_pulse.root_path, class: "flex items-center gap mie-2", title: "Rails Pulse v#{RailsPulse::VERSION}" do %>
73
73
  <span class="font-bold">Rails Pulse</span>
74
74
  <% end %>
75
75
  <nav class="flex items-center gap text-sm text-subtle show@md" style="--column-gap: 1rem">
@@ -1,3 +1,3 @@
1
1
  module RailsPulse
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_pulse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Harvey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-22 00:00:00.000000000 Z
11
+ date: 2026-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -48,16 +48,22 @@ dependencies:
48
48
  name: ransack
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '4.0'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '6'
54
57
  type: :runtime
55
58
  prerelease: false
56
59
  version_requirements: !ruby/object:Gem::Requirement
57
60
  requirements:
58
- - - "~>"
61
+ - - ">="
59
62
  - !ruby/object:Gem::Version
60
63
  version: '4.0'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '6'
61
67
  - !ruby/object:Gem::Dependency
62
68
  name: sqlite3
63
69
  requirement: !ruby/object:Gem::Requirement
@@ -545,7 +551,7 @@ metadata:
545
551
  bug_tracker_uri: https://github.com/railspulse/rails_pulse/issues
546
552
  rubygems_mfa_required: 'true'
547
553
  post_install_message: |
548
- Rails Pulse 0.4.0 installed.
554
+ Rails Pulse 0.4.1 installed.
549
555
 
550
556
  UPGRADING from any 0.3.x? This release changes route identity and DROPS
551
557
  rails_pulse_routes.method. Back up your database first, then run: