dispatch-rails 0.10.1 → 0.10.3

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: a712af989dbaa59fcd32135400b74a6ddb41058f291d60dce865516e117a50c6
4
- data.tar.gz: 1e1d88267bcffb7177556c6a0dd92c7e9e191356288e647881a1ef04410bbeb3
3
+ metadata.gz: 8dd9b609e2bdb00f81b4b7dc54d47460a9a2d687c51870331459fbbf9019542e
4
+ data.tar.gz: 33a57685d7ae26719a1aba2871e84fc0cce87159bdf741664dc8169badb5ee5f
5
5
  SHA512:
6
- metadata.gz: 891dee4bfc449f10ba4e4478ebd3fdd695d023f1853179563f63ccbbfa0a67c5431151576b403db02520c6132a0bb0ea6c93d64fe4999bde11006d820c39af26
7
- data.tar.gz: f5be0e31f9ecbfce518e7533fc6a483c7bc12bcb5a624aae24a8617666232bf2d7a10f1787367313c7b741bf5ffe88dc07fff8e05bc0805e70012a50c65ef517
6
+ metadata.gz: 6d33ec2cc1b41aeb2f1f563c819f91272e8ea3f983b6c9ca638ca152ac9e532014e8b22a5e66d9b947c11506f189d8e6c4f747b6332e8aca08b5d3b5a6422d5f
7
+ data.tar.gz: 12985b909215d106a6bcda904d031758f276b8f4c8fb766e58f66a6e3672e87d7bdd82951b962389a5bf8877ed4b2706ff88d36aebdb7e1dddd8a195d3b2fa66
data/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable changes to `dispatch-rails` are documented here. The format is based
4
4
  on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.10.3] - 2026-07-08
8
+
9
+ ### Fixed
10
+ - The browser error tracker could **report on its own transport failures**. The
11
+ `send()` fetch had no rejection handler, so a failed report (flaky connection,
12
+ a blocked or CORS-less endpoint response) surfaced as an `unhandledrejection` —
13
+ which the tracker's own listener then reported via `send()`, firing another
14
+ fetch at the same failing endpoint. Hosts saw a `TypeError: Failed to fetch`
15
+ attributed to `error_tracker.js#send`, and a reachable-but-erroring endpoint
16
+ could turn it into a self-amplifying loop. The `fetch` now has a `.catch` that
17
+ swallows transport failures, so the tracker never reports on its own delivery.
18
+
19
+ ## [0.10.2] - 2026-06-18
20
+
21
+ ### Fixed
22
+ - The browser tracker/widget bootstrapped via an **inline** `<script>` (a nonced
23
+ `javascript_tag` that ran `import("/dispatch/…")`). Inline scripts are fragile
24
+ under Turbo + CSP — the nonce attribute is blanked after parse and must be
25
+ re-applied from the `csp-nonce` meta tag on every navigation — so hosts kept
26
+ seeing `script-src-elem blocked inline`. Both loaders are now plain **external
27
+ same-origin** `<script type="module" src="/dispatch/…">` tags, which
28
+ `script-src 'self'` allows outright — no nonce, no `'unsafe-inline'`, no Turbo
29
+ nonce drift. The nonce is still attached when the host generates one, so strict
30
+ nonce-only / `strict-dynamic` policies keep working. (`type="module"` also lets
31
+ the browser de-dupe the load across Turbo navigations via the module map.)
32
+ - Browser CSP/Reporting-API events were attributed to the `/dispatch/reports`
33
+ endpoint the browser POSTs to, collapsing every violation across the app onto
34
+ one bogus URL. CSP reports now carry the violation's `document-uri` as the event
35
+ URL, so the dashboard groups and links them to the page that actually violated.
36
+
7
37
  ## [0.10.1] - 2026-06-18
8
38
 
9
39
  ### Fixed
@@ -156,11 +156,19 @@
156
156
  // Preferred: fetch keepalive (proper CORS + Authorization header).
157
157
  if (window.fetch) {
158
158
  try {
159
+ // The .catch is essential: fetch reports network/CORS failures as a
160
+ // REJECTED PROMISE, not a synchronous throw. Without it, a failed report
161
+ // (flaky connection, blocked endpoint, CORS-less error response) becomes
162
+ // an unhandledrejection — which our own listener below reports via send(),
163
+ // firing another fetch at the same failing endpoint. That self-reporting
164
+ // loop is exactly what "TypeError: Failed to fetch" at send() means.
165
+ // Swallow transport failures silently; the tracker must never report on
166
+ // its own transport.
159
167
  fetch(cfg.endpoint, {
160
168
  method: "POST", keepalive: true, mode: "cors",
161
169
  headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.apiKey },
162
170
  body: body
163
- });
171
+ }).catch(function () { /* transport failed; never self-report */ });
164
172
  return;
165
173
  } catch (e) { /* fall through */ }
166
174
  }
@@ -1,9 +1,9 @@
1
1
  <script type="application/json" id="dispatch-error-config"><%= raw error_config_json.gsub("</", "<\\/") %></script>
2
- <%# nonce: true picks up the host app's CSP nonce; the JSON config island above is non-executable and needs none %>
3
- <%= javascript_tag type: "module", nonce: true do %>
4
- if (!window.__dispatchErrorTrackerLoaded) {
5
- import("/dispatch/error_tracker.js").catch(() => {
6
- console.error("[dispatch-rails] failed to load error_tracker.js ensure the engine assets are mounted");
7
- });
8
- }
9
- <% end %>
2
+ <%# The config island above is a non-executable application/json data block, so CSP
3
+ never applies to it. The tracker itself loads as an EXTERNAL same-origin script
4
+ (served by the engine's AssetMiddleware at this exact path), which script-src
5
+ 'self' allows on its own — no nonce, no 'unsafe-inline', and none of the inline-
6
+ script-under-Turbo nonce drift that an inline loader is prone to. We still attach
7
+ the nonce when the host generates one, so strict nonce-only / strict-dynamic
8
+ policies (where 'self' alone isn't enough) keep working. %>
9
+ <%= tag.script type: "module", src: "/dispatch/error_tracker.js", nonce: content_security_policy_nonce %>
@@ -60,12 +60,10 @@
60
60
  </div>
61
61
  </div>
62
62
 
63
- <%# nonce: true picks up the host app's CSP nonce; omitted when no nonce generator is configured %>
64
- <%= javascript_tag type: "module", nonce: true do %>
65
- if (!window.__dispatchWidgetLoaded) {
66
- window.__dispatchWidgetLoaded = true;
67
- import("/dispatch/widget.js").catch(() => {
68
- console.error("[dispatch-rails] failed to load widget.js make sure the engine assets are mounted");
69
- });
70
- }
71
- <% end %>
63
+ <%# Loaded as an EXTERNAL same-origin module (served by the engine's AssetMiddleware
64
+ at this exact path), so script-src 'self' allows it without a nonce or
65
+ 'unsafe-inline' — no inline loader to be blocked under script-src-elem. type=module
66
+ both resolves widget.js's `import "@hotwired/stimulus"` via the host importmap and
67
+ de-dupes the load across Turbo navigations via the module map. The nonce is
68
+ attached when the host generates one, for strict nonce-only / strict-dynamic CSPs. %>
69
+ <%= tag.script type: "module", src: "/dispatch/widget.js", nonce: content_security_policy_nonce %>
@@ -16,17 +16,23 @@ module Dispatch
16
16
  MAX_PARAMS_BYTES = 8_000
17
17
  SAFE_HEADERS = %w[User-Agent Referer Accept Content-Type Host X-Request-Id].freeze
18
18
 
19
- def self.call(exception, handled:, env: nil, user: nil, tags: {}, level: "error")
20
- new(exception, handled: handled, env: env, user: user, tags: tags, level: level).call
19
+ def self.call(exception, handled:, env: nil, user: nil, tags: {}, level: "error", request_url: nil)
20
+ new(exception, handled: handled, env: env, user: user, tags: tags, level: level,
21
+ request_url: request_url).call
21
22
  end
22
23
 
23
- def initialize(exception, handled:, env: nil, user: nil, tags: {}, level: "error")
24
+ def initialize(exception, handled:, env: nil, user: nil, tags: {}, level: "error", request_url: nil)
24
25
  @exception = exception
25
26
  @handled = handled
26
27
  @env = env
27
28
  @user = user
28
29
  @tags = tags || {}
29
30
  @level = level
31
+ # Override for the event's request URL. A browser report (CSP, NEL, …) is
32
+ # POSTed to /dispatch/reports, so the env URL is that endpoint, not the page
33
+ # that violated the policy — the caller passes the report's document-uri here
34
+ # so the dashboard attributes (and groups) the event to the real page.
35
+ @request_url_override = request_url
30
36
  @source_cache = {}
31
37
  @config = Dispatch::Rails.configuration
32
38
  end
@@ -221,6 +227,8 @@ module Dispatch
221
227
  end
222
228
 
223
229
  def request_url
230
+ return @request_url_override if @request_url_override.present?
231
+
224
232
  scheme = @env["rack.url_scheme"] || "http"
225
233
  host = @env["HTTP_HOST"] || @env["SERVER_NAME"]
226
234
  return nil if host.nil?
@@ -21,7 +21,8 @@ module Dispatch
21
21
  user = resolve_user(config, env, context)
22
22
  tags = merged_tags(config, env, context)
23
23
  event = EventBuilder.call(exception, handled: handled, env: env, user: user,
24
- tags: tags, level: level)
24
+ tags: tags, level: level,
25
+ request_url: context[:request_url])
25
26
  event = config.before_send.call(event) if config.before_send.respond_to?(:call)
26
27
  return if event.nil?
27
28
 
@@ -100,7 +100,13 @@ module Dispatch
100
100
  handled: true, env: env,
101
101
  # report-only disposition is monitoring, not a live block — keep it quieter.
102
102
  level: fields[:disposition].to_s == "report" ? "info" : "warning",
103
- context: { tags: { csp: "true", report_type: "csp-violation" }.merge(fields) }
103
+ context: {
104
+ # Attribute the violation to the page it happened on, not to this
105
+ # /dispatch/reports endpoint the browser POSTed the report to — otherwise
106
+ # every CSP violation across the app collapses onto one bogus URL.
107
+ request_url: fields[:document_uri],
108
+ tags: { csp: "true", report_type: "csp-violation" }.merge(fields)
109
+ }
104
110
  )
105
111
  end
106
112
 
@@ -1,5 +1,5 @@
1
1
  module Dispatch
2
2
  module Rails
3
- VERSION = "0.10.1".freeze
3
+ VERSION = "0.10.3".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dispatch-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dispatch Team