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 +4 -4
- data/CHANGELOG.md +17 -0
- data/app/assets/javascript/profiler-ajax-interceptor.js +193 -0
- data/lib/profiler/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d632e07d28796ba7902cd20f9466b4c529c77ab76b5627c2b946e6b059b23c1f
|
|
4
|
+
data.tar.gz: 7c91607d3ae24180f6708938a8dfa118ad297b11a6b9749cf155adeef5b81336
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+
})();
|
data/lib/profiler/version.rb
CHANGED
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.
|
|
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
|