rails-profiler 0.33.0 → 0.33.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: 4dc81c1f51aaa36f9a01703962763e6526c95ea44a86aec0f6580a46c6239bfe
4
- data.tar.gz: 1555289065c0d81d6267b21ddb9d3ae2c2c0716e1290f33e30df30590ff4b8c9
3
+ metadata.gz: d632e07d28796ba7902cd20f9466b4c529c77ab76b5627c2b946e6b059b23c1f
4
+ data.tar.gz: 7c91607d3ae24180f6708938a8dfa118ad297b11a6b9749cf155adeef5b81336
5
5
  SHA512:
6
- metadata.gz: 967050b5160cb75653481fd86444696cb81fa84c46aa7faa34064a3c281c0acaa5ec08d4029a8b1aca2525da8ffb5d0033b934025d885a8b53e969faca151293
7
- data.tar.gz: e63d49ccaf002fb7b4911920fc3e279597451fa276d669f557fde971e13e2855cbc347cc16bf254ff2d995b14ba0c18454e929910326a41d0104ee83db49c327
6
+ metadata.gz: a7781894a3b105e730d47d63c0e7a2a15b28a58a51a38fc4662fb193c0665c1cdb8fd7072cadc7926f38a96e6a68a350ae19f16ef2560a119c89355fdbe3f4a6
7
+ data.tar.gz: e626c7d2d344ad58473fdecbbbaf3bfae340faf17eb1dd25ca7f88325ffe87c9a9837fefcdc9b1dea3cdc3541ce46a4156d1d4b17acd3f4b049defd2c08fcd03
data/CHANGELOG.md CHANGED
@@ -19,6 +19,23 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.33.1] - 2026-10-14
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Packaging:** The published gem now contains `app/assets/javascript/profiler-ajax-interceptor.js`,
29
+ the script the toolbar injects to link the `fetch`, `XMLHttpRequest` and Turbo requests of a page
30
+ to that page. It was missing from every published version: with the gem installed from a
31
+ registry, those requests were never linked to their page, and the only trace was the log line
32
+ `ToolbarInjector: could not load the AJAX interceptor script`. A gem used from a git checkout or
33
+ a local path was not affected.
34
+ - **Release:** A version is now published only once both test suites (Ruby 3.3 and 3.4) have
35
+ passed, as written in the `needs` of the `release` and `canary` jobs, and once the built gem
36
+ has been installed and found to carry every file it reads at run time. The suites already held
37
+ publication back before, but only through the order of the pipeline stages.
38
+
22
39
  ## [0.33.0] - 2026-10-14
23
40
 
24
41
  <!-- stamped -->
@@ -0,0 +1,193 @@
1
+ // Profiler AJAX Request Interceptor
2
+ // This script intercepts fetch() and XMLHttpRequest calls to link AJAX requests to their parent page
3
+
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // Get parent token from window variable set by the profiler
8
+ if (!window.__PROFILER_PARENT_TOKEN__) {
9
+ console.warn('[Profiler] Parent token not found, AJAX tracking disabled');
10
+ return;
11
+ }
12
+
13
+ // Check if already intercepted (by Chrome extension or another script)
14
+ if (window.__PROFILER_INTERCEPTOR_ACTIVE__) {
15
+ console.debug('[Profiler] Interceptor already active, skipping');
16
+ return;
17
+ }
18
+ window.__PROFILER_INTERCEPTOR_ACTIVE__ = true;
19
+
20
+ // Save references to originals (they might already be wrapped by extension)
21
+ const originalFetch = window.fetch;
22
+ const originalXHROpen = XMLHttpRequest.prototype.open;
23
+ const originalXHRSend = XMLHttpRequest.prototype.send;
24
+ const pendingXHRRequests = new WeakMap();
25
+ // The fetches Turbo makes, by the AbortSignal it hands to fetch(): { frame: true } when the
26
+ // request loads a <turbo-frame>.
27
+ const turboRequests = new WeakMap();
28
+ const TURBO_STREAM_MIME = 'text/vnd.turbo-stream.html';
29
+
30
+ // The page the sub-requests belong to. Read at each link, not once: a Turbo Drive visit
31
+ // renders a new page, whose inline script sets the token again, without reloading this one.
32
+ function currentParentToken() {
33
+ return window.__PROFILER_PARENT_TOKEN__;
34
+ }
35
+
36
+ function isRequest(input) {
37
+ return typeof Request !== 'undefined' && input instanceof Request;
38
+ }
39
+
40
+ function hasHeader(headers, name) {
41
+ if (!headers) return false;
42
+ if (typeof headers.has === 'function') return headers.has(name);
43
+ const wanted = name.toLowerCase();
44
+ return Object.keys(headers).some(key => key.toLowerCase() === wanted && headers[key] != null && headers[key] !== '');
45
+ }
46
+
47
+ // Turbo (7 and 8) announces each of its requests before calling fetch(), with the options it
48
+ // will pass to it, signal included.
49
+ document.addEventListener('turbo:before-fetch-request', function(event) {
50
+ const options = event.detail && event.detail.fetchOptions;
51
+ if (!options || !options.signal) return;
52
+ turboRequests.set(options.signal, { frame: hasHeader(options.headers, 'Turbo-Frame') });
53
+ });
54
+
55
+ // A Turbo Drive visit, a prefetch on hover, or a form submission answered with a page is the
56
+ // next page, which carries its own toolbar: not a sub-request of this one. A <turbo-frame>
57
+ // load, and any request answered with Turbo Stream actions, update this page: they are.
58
+ function isNavigation(args, response) {
59
+ const init = args[1];
60
+ const signal = (init && init.signal) || (isRequest(args[0]) ? args[0].signal : null);
61
+ const turbo = signal && turboRequests.get(signal);
62
+ if (!turbo || turbo.frame) return false;
63
+
64
+ const contentType = response.headers.get('Content-Type') || '';
65
+ return !contentType.startsWith(TURBO_STREAM_MIME);
66
+ }
67
+
68
+ // Helper function to check if path should be skipped
69
+ function shouldSkipPath(url) {
70
+ try {
71
+ const urlObj = new URL(url, window.location.origin);
72
+ const path = urlObj.pathname;
73
+
74
+ // Skip profiler's own requests
75
+ if (path.startsWith('/_profiler')) {
76
+ return true;
77
+ }
78
+
79
+ return false;
80
+ } catch (e) {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ // Helper function to link child profile to parent
86
+ function linkChildProfile(childToken) {
87
+ const parentToken = currentParentToken();
88
+ if (!childToken || !parentToken) return;
89
+
90
+ console.debug('[Profiler] Linking AJAX request:', { parent: parentToken, child: childToken });
91
+
92
+ // Post to the link API endpoint
93
+ const linkUrl = '/_profiler/api/ajax/link';
94
+ const body = JSON.stringify({
95
+ parent_token: parentToken,
96
+ child_token: childToken
97
+ });
98
+
99
+ // Use original fetch to avoid infinite loop
100
+ originalFetch.call(window, linkUrl, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ 'Accept': 'application/json',
105
+ 'X-Profiler-Request': '1'
106
+ },
107
+ body: body
108
+ }).then(response => {
109
+ if (response.ok) {
110
+ // Refresh toolbar after successful link (with small delay to let backend process)
111
+ setTimeout(() => {
112
+ if (typeof window.__PROFILER_REFRESH_TOOLBAR__ === 'function') {
113
+ window.__PROFILER_REFRESH_TOOLBAR__();
114
+ }
115
+ }, 100);
116
+ }
117
+ }).catch(err => {
118
+ // Fail silently - don't break the page if profiler API fails
119
+ console.debug('[Profiler] Failed to link AJAX request:', err);
120
+ });
121
+ }
122
+
123
+ // Listen for events from Chrome extension (if installed)
124
+ window.addEventListener('__profilerRequest', function(event) {
125
+ const { token } = event.detail;
126
+ if (token) {
127
+ console.debug('[Profiler] Received profiler event from extension:', token);
128
+ linkChildProfile(token);
129
+ }
130
+ });
131
+
132
+ // Override fetch()
133
+ window.fetch = async function(...args) {
134
+ const url = isRequest(args[0]) ? args[0].url : String(args[0]);
135
+
136
+ // Skip profiler paths
137
+ if (shouldSkipPath(url)) {
138
+ return originalFetch.apply(this, args);
139
+ }
140
+
141
+ try {
142
+ const response = await originalFetch.apply(this, args);
143
+
144
+ // Extract profiler token from response headers
145
+ const profilerToken = response.headers.get('X-Profiler-Token');
146
+
147
+ if (profilerToken && !isNavigation(args, response)) {
148
+ linkChildProfile(profilerToken);
149
+ }
150
+
151
+ return response;
152
+ } catch (error) {
153
+ throw error;
154
+ }
155
+ };
156
+
157
+ // Override XMLHttpRequest.open()
158
+ XMLHttpRequest.prototype.open = function(method, url, ...args) {
159
+ const urlString = typeof url === 'string' ? url : url.toString();
160
+
161
+ // Store request metadata
162
+ pendingXHRRequests.set(this, {
163
+ url: urlString,
164
+ method: method,
165
+ timestamp: Date.now()
166
+ });
167
+
168
+ return originalXHROpen.apply(this, [method, url, ...args]);
169
+ };
170
+
171
+ // Override XMLHttpRequest.send()
172
+ XMLHttpRequest.prototype.send = function(...args) {
173
+ const requestData = pendingXHRRequests.get(this);
174
+
175
+ // Skip profiler paths
176
+ if (requestData && shouldSkipPath(requestData.url)) {
177
+ return originalXHRSend.apply(this, args);
178
+ }
179
+
180
+ // Add load event listener to capture response
181
+ this.addEventListener('load', function() {
182
+ const profilerToken = this.getResponseHeader('X-Profiler-Token');
183
+
184
+ if (profilerToken && requestData) {
185
+ linkChildProfile(profilerToken);
186
+ }
187
+ });
188
+
189
+ return originalXHRSend.apply(this, args);
190
+ };
191
+
192
+ console.debug('[Profiler] AJAX interceptor initialized with parent token:', currentParentToken());
193
+ })();
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.33.0"
4
+ VERSION = "0.33.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.0
4
+ version: 0.33.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
@@ -109,6 +109,7 @@ files:
109
109
  - app/assets/builds/profiler-toolbar.js
110
110
  - app/assets/builds/profiler.css
111
111
  - app/assets/builds/profiler.js
112
+ - app/assets/javascript/profiler-ajax-interceptor.js
112
113
  - app/controllers/profiler/api/ajax_controller.rb
113
114
  - app/controllers/profiler/api/cluster_controller.rb
114
115
  - app/controllers/profiler/api/console_controller.rb