opticore-profiler 1.0.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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/assets/css/debug-message.css +16 -0
- package/dist/assets/css/home-page.css +177 -0
- package/dist/assets/css/profiler-detail.css +215 -0
- package/dist/assets/css/profiler-list.css +131 -0
- package/dist/assets/css/toolbar-standalone.css +213 -0
- package/dist/assets/js/home-particles.js +65 -0
- package/dist/assets/js/profiler-detail.js +77 -0
- package/dist/assets/js/profiler-list.js +76 -0
- package/dist/assets/js/toolbar.js +230 -0
- package/dist/index.cjs +1942 -0
- package/dist/index.d.cts +488 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +1893 -0
- package/dist/utils/translations/message.translation.en.json +190 -0
- package/dist/utils/translations/message.translation.fr.json +190 -0
- package/dist/views/templates/home.njk +120 -0
- package/dist/views/templates/macros/icons.njk +19 -0
- package/dist/views/templates/macros/tables.njk +17 -0
- package/dist/views/templates/message.njk +14 -0
- package/dist/views/templates/panels/configuration.njk +16 -0
- package/dist/views/templates/panels/database.njk +36 -0
- package/dist/views/templates/panels/exception.njk +26 -0
- package/dist/views/templates/panels/logs.njk +44 -0
- package/dist/views/templates/panels/performance.njk +56 -0
- package/dist/views/templates/panels/request.njk +141 -0
- package/dist/views/templates/panels/routes.njk +51 -0
- package/dist/views/templates/panels/routing.njk +22 -0
- package/dist/views/templates/profiler-detail.njk +114 -0
- package/dist/views/templates/profiler-list.njk +163 -0
- package/dist/views/templates/toolbar-wdt.njk +161 -0
- package/package.json +73 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
|
+
body {
|
|
3
|
+
background: transparent;
|
|
4
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
|
5
|
+
font-size: 12px;
|
|
6
|
+
}
|
|
7
|
+
.toolbar {
|
|
8
|
+
position: fixed; bottom: 0; left: 0; right: 0;
|
|
9
|
+
height: 36px;
|
|
10
|
+
background: #1b1b1b;
|
|
11
|
+
border-top: 1px solid #2e2e2e;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: stretch;
|
|
14
|
+
z-index: 99999;
|
|
15
|
+
box-shadow: 0 -2px 8px rgba(0,0,0,.4);
|
|
16
|
+
}
|
|
17
|
+
.tb-badge {
|
|
18
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
19
|
+
padding: 0 10px;
|
|
20
|
+
font-weight: 700; font-size: 12px; color: #fff;
|
|
21
|
+
text-decoration: none; height: 100%;
|
|
22
|
+
transition: opacity .15s;
|
|
23
|
+
}
|
|
24
|
+
.tb-badge:hover { opacity: .85; }
|
|
25
|
+
.tb-badge--s-ok { background: #2e7d32; }
|
|
26
|
+
.tb-badge--s-warn { background: #c0392b; }
|
|
27
|
+
.tb-badge--s-error { background: #c0392b; }
|
|
28
|
+
.tb-badge--s-redirect { background: #1565c0; }
|
|
29
|
+
.tb-block {
|
|
30
|
+
display: flex; align-items: center; gap: 5px;
|
|
31
|
+
padding: 0 10px; color: #ccc; text-decoration: none;
|
|
32
|
+
height: 100%; white-space: nowrap;
|
|
33
|
+
transition: background .15s; cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
.tb-block:hover { background: #252525; color: #fff; }
|
|
36
|
+
.tb-icon { color: #888; display: flex; align-items: center; }
|
|
37
|
+
.tb-block:hover .tb-icon { color: #aaa; }
|
|
38
|
+
.tb-label { font-size: 12px; }
|
|
39
|
+
.tb-underline { border-bottom: 2px solid #e74c3c; }
|
|
40
|
+
.tb-spacer { flex: 1; }
|
|
41
|
+
.tb-brand {
|
|
42
|
+
display: flex; align-items: center; gap: 6px;
|
|
43
|
+
padding: 0 12px; color: #ccc; text-decoration: none;
|
|
44
|
+
border-left: 1px solid #2e2e2e; height: 100%; font-weight: 500;
|
|
45
|
+
}
|
|
46
|
+
.tb-brand:hover { background: #252525; }
|
|
47
|
+
.tb-brand-icon {
|
|
48
|
+
width: 20px; height: 20px; background: #C87A3C;
|
|
49
|
+
border-radius: 4px; display: flex; align-items: center;
|
|
50
|
+
justify-content: center; font-size: 10px; font-weight: 900;
|
|
51
|
+
color: #fff; flex-shrink: 0;
|
|
52
|
+
}
|
|
53
|
+
.tb-version {
|
|
54
|
+
display: flex; align-items: center; padding: 0 10px;
|
|
55
|
+
color: #888; font-size: 11px;
|
|
56
|
+
border-left: 1px solid #2e2e2e; height: 100%; white-space: nowrap;
|
|
57
|
+
}
|
|
58
|
+
.lang-switch--toolbar {
|
|
59
|
+
display: flex; align-items: center; gap: 2px; height: 100%;
|
|
60
|
+
padding: 0 8px; border-left: 1px solid #2e2e2e;
|
|
61
|
+
}
|
|
62
|
+
.lang-switch--toolbar .lang-switch-item {
|
|
63
|
+
padding: 3px 6px; font-size: 10px; font-weight: 700; letter-spacing: .3px;
|
|
64
|
+
color: #777; border-radius: 3px; text-decoration: none;
|
|
65
|
+
}
|
|
66
|
+
.lang-switch--toolbar .lang-switch-item.active { background: #2e2e2e; color: #FAC68E; }
|
|
67
|
+
.lang-switch--toolbar .lang-switch-item:hover:not(.active) { color: #ccc; }
|
|
68
|
+
.tb-collapse {
|
|
69
|
+
display: flex; align-items: center; justify-content: center;
|
|
70
|
+
width: 36px; color: #555; cursor: pointer;
|
|
71
|
+
border: none; background: none;
|
|
72
|
+
border-left: 1px solid #2e2e2e; height: 100%;
|
|
73
|
+
font-size: 13px; transition: background .15s, color .15s;
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
}
|
|
76
|
+
.tb-collapse:hover { background: #333; color: #ccc; }
|
|
77
|
+
.tb-badge-pill {
|
|
78
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
79
|
+
background: #c0392b; color: #fff; border-radius: 10px;
|
|
80
|
+
font-size: 10px; font-weight: 700; min-width: 16px; height: 16px;
|
|
81
|
+
padding: 0 4px; margin-left: 2px;
|
|
82
|
+
}
|
|
83
|
+
.tb-badge-pill--warn { background: #e67e22; }
|
|
84
|
+
.tb-badge-pill--ok { background: #2e7d32; }
|
|
85
|
+
.tb-time-bar {
|
|
86
|
+
width: 100%; height: 2px; background: #e74c3c;
|
|
87
|
+
position: absolute; bottom: 0; left: 0;
|
|
88
|
+
}
|
|
89
|
+
/* HTTP requests tooltip */
|
|
90
|
+
.tb-http-block { position: relative; }
|
|
91
|
+
.tb-http-tooltip {
|
|
92
|
+
display: none;
|
|
93
|
+
position: absolute; bottom: 100%; left: 0;
|
|
94
|
+
background: #1e1e1e; border: 1px solid #333; border-radius: 6px;
|
|
95
|
+
min-width: 500px; z-index: 100000;
|
|
96
|
+
box-shadow: 0 -4px 20px rgba(0,0,0,.7); overflow: hidden;
|
|
97
|
+
/* flush against the trigger block (no gap) so the mouse never leaves the
|
|
98
|
+
hoverable area while moving from the block up into the dropdown —
|
|
99
|
+
a gap here would break :hover before the pointer reaches the content */
|
|
100
|
+
margin-bottom: 0;
|
|
101
|
+
}
|
|
102
|
+
.tb-http-block:hover .tb-http-tooltip { display: block; }
|
|
103
|
+
.tb-tip-header {
|
|
104
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
105
|
+
padding: 8px 12px; background: #161616; border-bottom: 1px solid #333;
|
|
106
|
+
font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .5px;
|
|
107
|
+
}
|
|
108
|
+
.tb-tip-viewall { color: #7c98d3; text-decoration: none; font-size: 11px; }
|
|
109
|
+
.tb-tip-viewall:hover { text-decoration: underline; }
|
|
110
|
+
.tb-tip-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
111
|
+
.tb-tip-table th {
|
|
112
|
+
text-align: left; padding: 5px 10px;
|
|
113
|
+
color: #666; font-weight: 600; font-size: 10px;
|
|
114
|
+
text-transform: uppercase; letter-spacing: .4px;
|
|
115
|
+
border-bottom: 1px solid #2a2a2a; background: #1a1a1a;
|
|
116
|
+
}
|
|
117
|
+
.tb-tip-table td { padding: 5px 10px; color: #bbb; border-bottom: 1px solid #252525; }
|
|
118
|
+
.tb-tip-table tr:last-child td { border-bottom: none; }
|
|
119
|
+
.tb-tip-table tr:hover td { background: #262626; }
|
|
120
|
+
.tip-current td { background: #1e2a1e !important; }
|
|
121
|
+
.tip-method {
|
|
122
|
+
display: inline-block; padding: 1px 6px; border-radius: 3px;
|
|
123
|
+
font-size: 10px; font-weight: 700; min-width: 52px; text-align: center;
|
|
124
|
+
}
|
|
125
|
+
.tip-m-get { background: #1565c0; color: #fff; }
|
|
126
|
+
.tip-m-post { background: #e67e22; color: #fff; }
|
|
127
|
+
.tip-m-put { background: #8e44ad; color: #fff; }
|
|
128
|
+
.tip-m-patch { background: #16a085; color: #fff; }
|
|
129
|
+
.tip-m-delete { background: #c0392b; color: #fff; }
|
|
130
|
+
.tip-m-head,
|
|
131
|
+
.tip-m-options { background: #555; color: #fff; }
|
|
132
|
+
.tip-s-ok { color: #2ecc71; font-weight: 600; }
|
|
133
|
+
.tip-s-redir { color: #3498db; font-weight: 600; }
|
|
134
|
+
.tip-s-err { color: #e74c3c; font-weight: 600; }
|
|
135
|
+
.tip-dur { color: #888; white-space: nowrap; }
|
|
136
|
+
.tip-url-link { color: #ccc; text-decoration: none; }
|
|
137
|
+
.tip-url-link:hover { color: #7c98d3; }
|
|
138
|
+
|
|
139
|
+
/* ── per-block hover tooltip (status, duration, memory, logs, route, server) ── */
|
|
140
|
+
.tb-block-wrap { position: relative; display: flex; align-items: stretch; height: 100%; }
|
|
141
|
+
.tb-tooltip {
|
|
142
|
+
display: none;
|
|
143
|
+
position: absolute; bottom: 100%; left: 0;
|
|
144
|
+
background: #1e1e1e; border: 1px solid #333; border-radius: 6px;
|
|
145
|
+
padding: 10px 0 6px; min-width: 240px; z-index: 100000;
|
|
146
|
+
box-shadow: 0 -4px 20px rgba(0,0,0,.7);
|
|
147
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
|
148
|
+
/* flush against the trigger block (no gap) — see .tb-http-tooltip */
|
|
149
|
+
margin-bottom: 0;
|
|
150
|
+
}
|
|
151
|
+
.tb-block-wrap:hover .tb-tooltip { display: block; }
|
|
152
|
+
/* blocks near the toolbar's right edge (Server, version) — anchor from the
|
|
153
|
+
right so the tooltip grows leftward and stays inside the viewport */
|
|
154
|
+
.tb-tooltip--right { left: auto; right: 0; }
|
|
155
|
+
.tt-row {
|
|
156
|
+
display: flex; align-items: baseline;
|
|
157
|
+
padding: 3px 14px; gap: 8px; line-height: 1.6;
|
|
158
|
+
}
|
|
159
|
+
.tt-label {
|
|
160
|
+
color: #666; font-size: 11px; white-space: nowrap;
|
|
161
|
+
min-width: 90px; flex-shrink: 0;
|
|
162
|
+
}
|
|
163
|
+
.tt-val { color: #d4d4d4; font-size: 12px; word-break: break-all; }
|
|
164
|
+
.tt-val code {
|
|
165
|
+
background: #111; border: 1px solid #2a2a2a; padding: 1px 5px;
|
|
166
|
+
border-radius: 3px; font-size: 11px; color: #aaa;
|
|
167
|
+
}
|
|
168
|
+
.tt-badge {
|
|
169
|
+
display: inline-flex; align-items: center;
|
|
170
|
+
padding: 1px 7px; border-radius: 10px;
|
|
171
|
+
font-size: 10px; font-weight: 700; color: #fff;
|
|
172
|
+
}
|
|
173
|
+
.tt-sep { height: 1px; background: #2a2a2a; margin: 6px 0; }
|
|
174
|
+
.tt-link {
|
|
175
|
+
display: flex; align-items: center; gap: 6px;
|
|
176
|
+
padding: 4px 14px; color: #FAC68E; font-size: 12px;
|
|
177
|
+
text-decoration: none; transition: background .1s;
|
|
178
|
+
}
|
|
179
|
+
.tt-link:hover { background: #252525; color: #FBD4A8; }
|
|
180
|
+
|
|
181
|
+
/* ── collapsed mini pill (bottom-right) ── */
|
|
182
|
+
#wdt-mini {
|
|
183
|
+
position: fixed; bottom: 12px; right: 12px; z-index: 99999;
|
|
184
|
+
display: none; align-items: center;
|
|
185
|
+
background: #1b1b1b; border: 1px solid #333; border-radius: 24px;
|
|
186
|
+
box-shadow: 0 4px 18px rgba(0,0,0,.6); overflow: hidden;
|
|
187
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
|
188
|
+
}
|
|
189
|
+
#wdt-mini-logo {
|
|
190
|
+
display: flex; align-items: center; gap: 7px;
|
|
191
|
+
padding: 0 12px 0 10px; height: 34px;
|
|
192
|
+
border-right: 1px solid #2a2a2a;
|
|
193
|
+
color: #ccc; font-size: 12px; font-weight: 500; text-decoration: none;
|
|
194
|
+
}
|
|
195
|
+
#wdt-mini-logo-icon {
|
|
196
|
+
width: 22px; height: 22px; background: #FAC68E; border-radius: 50%;
|
|
197
|
+
display: flex; align-items: center; justify-content: center;
|
|
198
|
+
font-size: 9px; font-weight: 900; color: #fff; flex-shrink: 0;
|
|
199
|
+
}
|
|
200
|
+
#wdt-mini-status {
|
|
201
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
202
|
+
padding: 0 10px; height: 34px;
|
|
203
|
+
font-size: 12px; font-weight: 700; color: #fff;
|
|
204
|
+
border-right: 1px solid #2a2a2a; min-width: 44px;
|
|
205
|
+
text-decoration: none; transition: opacity .15s;
|
|
206
|
+
}
|
|
207
|
+
#wdt-mini-status:hover { opacity: .85; }
|
|
208
|
+
#wdt-mini-expand {
|
|
209
|
+
display: flex; align-items: center; justify-content: center;
|
|
210
|
+
width: 32px; height: 34px; color: #555; font-size: 13px;
|
|
211
|
+
border: none; background: none; cursor: pointer; transition: color .15s;
|
|
212
|
+
}
|
|
213
|
+
#wdt-mini-expand:hover { color: #ccc; }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var cv = document.getElementById('op-canvas');
|
|
3
|
+
var cx = cv.getContext('2d');
|
|
4
|
+
var W, H;
|
|
5
|
+
|
|
6
|
+
function resize() {
|
|
7
|
+
W = cv.width = window.innerWidth;
|
|
8
|
+
H = cv.height = window.innerHeight;
|
|
9
|
+
}
|
|
10
|
+
resize();
|
|
11
|
+
window.addEventListener('resize', resize);
|
|
12
|
+
|
|
13
|
+
/* palette sable / poussière du désert */
|
|
14
|
+
var COLORS = [
|
|
15
|
+
'#150F04','#150F04','#150F04','#150F04',
|
|
16
|
+
'#150F04','#150F04','#150F04','#150F04'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/* génère une particule */
|
|
20
|
+
function mkParticle() {
|
|
21
|
+
return {
|
|
22
|
+
x: Math.random() * W,
|
|
23
|
+
y: Math.random() * H,
|
|
24
|
+
r: Math.random() * 2.2 + 0.4,
|
|
25
|
+
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
|
26
|
+
alpha: Math.random() * 0.45 + 0.15,
|
|
27
|
+
/* vitesse : chute lente + dérive latérale (vent du désert) */
|
|
28
|
+
vy: Math.random() * 0.55 + 0.18,
|
|
29
|
+
vx: (Math.random() - 0.38) * 0.30,
|
|
30
|
+
/* oscillation sinusoïdale */
|
|
31
|
+
wobble: Math.random() * Math.PI * 2,
|
|
32
|
+
ws: Math.random() * 0.018 + 0.004,
|
|
33
|
+
wa: Math.random() * 0.6 + 0.2,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var N = 220;
|
|
38
|
+
var pts = [];
|
|
39
|
+
for (var i = 0; i < N; i++) pts.push(mkParticle());
|
|
40
|
+
|
|
41
|
+
function tick() {
|
|
42
|
+
cx.clearRect(0, 0, W, H);
|
|
43
|
+
for (var i = 0; i < N; i++) {
|
|
44
|
+
var p = pts[i];
|
|
45
|
+
p.wobble += p.ws;
|
|
46
|
+
p.x += p.vx + Math.sin(p.wobble) * p.wa * 0.25;
|
|
47
|
+
p.y += p.vy;
|
|
48
|
+
|
|
49
|
+
/* recycle en haut quand hors écran */
|
|
50
|
+
if (p.y > H + 6) { p.y = -6; p.x = Math.random() * W; }
|
|
51
|
+
if (p.x > W + 6) { p.x = -6; }
|
|
52
|
+
if (p.x < -6) { p.x = W + 6; }
|
|
53
|
+
|
|
54
|
+
cx.save();
|
|
55
|
+
cx.globalAlpha = p.alpha;
|
|
56
|
+
cx.fillStyle = p.color;
|
|
57
|
+
cx.beginPath();
|
|
58
|
+
cx.arc(p.x, p.y, p.r, 0, 6.283);
|
|
59
|
+
cx.fill();
|
|
60
|
+
cx.restore();
|
|
61
|
+
}
|
|
62
|
+
requestAnimationFrame(tick);
|
|
63
|
+
}
|
|
64
|
+
tick();
|
|
65
|
+
})();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function filterTable(input) {
|
|
2
|
+
const q = input.value.toLowerCase();
|
|
3
|
+
const wrap = input.closest(".table-wrap");
|
|
4
|
+
if (!wrap) return;
|
|
5
|
+
wrap.querySelectorAll("tbody tr").forEach(r => {
|
|
6
|
+
r.style.display = r.textContent.toLowerCase().includes(q) ? "" : "none";
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toggleLogCtx(id, btn) {
|
|
11
|
+
var el = document.getElementById(id);
|
|
12
|
+
if (!el) return;
|
|
13
|
+
var i18n = window.__opticoreProfilerI18n || {};
|
|
14
|
+
var shown = el.style.display === 'block';
|
|
15
|
+
el.style.display = shown ? 'none' : 'block';
|
|
16
|
+
btn.textContent = shown ? (i18n.showContext || 'Show context') : (i18n.hideContext || 'Hide context');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
(function() {
|
|
20
|
+
var tabs = document.querySelectorAll('#log-tabs .log-tab');
|
|
21
|
+
var rows = document.querySelectorAll('#log-tbody .log-row');
|
|
22
|
+
tabs.forEach(function(tab) {
|
|
23
|
+
tab.addEventListener('click', function() {
|
|
24
|
+
var f = this.getAttribute('data-ltab');
|
|
25
|
+
tabs.forEach(function(t) { t.classList.remove('active'); });
|
|
26
|
+
this.classList.add('active');
|
|
27
|
+
rows.forEach(function(row) {
|
|
28
|
+
row.style.display = (f === 'all' || row.getAttribute('data-ltype') === f) ? '' : 'none';
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
document.addEventListener("keydown", e => {
|
|
35
|
+
if (e.key === "Escape") location.href = "/_profiler";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
(function () {
|
|
39
|
+
var tabs = document.querySelectorAll('#rr-tabs .rr-tab');
|
|
40
|
+
var panels = document.querySelectorAll('.rr-tabpanel');
|
|
41
|
+
if (!tabs.length) return;
|
|
42
|
+
tabs.forEach(function (tab) {
|
|
43
|
+
tab.addEventListener('click', function () {
|
|
44
|
+
var target = this.getAttribute('data-rtab');
|
|
45
|
+
tabs.forEach(function (t) { t.classList.remove('active'); });
|
|
46
|
+
this.classList.add('active');
|
|
47
|
+
panels.forEach(function (p) {
|
|
48
|
+
p.style.display = (p.getAttribute('data-rpanel') === target) ? '' : 'none';
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
function copyCurl(btn) {
|
|
55
|
+
var pre = btn.parentElement.querySelector('pre');
|
|
56
|
+
if (!pre) return;
|
|
57
|
+
var text = pre.textContent;
|
|
58
|
+
var done = function () {
|
|
59
|
+
var i18n = window.__opticoreProfilerI18n || {};
|
|
60
|
+
var original = btn.textContent;
|
|
61
|
+
btn.textContent = i18n.copied || 'Copied!';
|
|
62
|
+
setTimeout(function () { btn.textContent = original; }, 1200);
|
|
63
|
+
};
|
|
64
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
65
|
+
navigator.clipboard.writeText(text).then(done, function () {});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
var ta = document.createElement('textarea');
|
|
69
|
+
ta.value = text;
|
|
70
|
+
ta.style.position = 'fixed';
|
|
71
|
+
ta.style.opacity = '0';
|
|
72
|
+
document.body.appendChild(ta);
|
|
73
|
+
ta.select();
|
|
74
|
+
try { document.execCommand('copy'); } catch (e) { /* best-effort fallback */ }
|
|
75
|
+
document.body.removeChild(ta);
|
|
76
|
+
done();
|
|
77
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return String(str == null ? "" : str)
|
|
4
|
+
.replace(/&/g, "&")
|
|
5
|
+
.replace(/</g, "<")
|
|
6
|
+
.replace(/>/g, ">")
|
|
7
|
+
.replace(/"/g, """);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function rowHtml(row) {
|
|
11
|
+
return (
|
|
12
|
+
'<tr class="pl-row" onclick="location.href=\'/_profiler/' + escapeHtml(row.token) + '\'">' +
|
|
13
|
+
'<td class="pl-cell"><span class="pl-status-badge pl-status-badge--' + escapeHtml(row.statusClass) + '">' + row.statusCode + "</span></td>" +
|
|
14
|
+
'<td class="pl-cell">' + escapeHtml(row.method) + "</td>" +
|
|
15
|
+
'<td class="pl-cell pl-cell-url" title="' + escapeHtml(row.url) + '">' + escapeHtml(row.url) + "</td>" +
|
|
16
|
+
'<td class="pl-cell pl-cell-date">' + escapeHtml(row.dateStr) + '<br><span class="pl-cell-time">' + escapeHtml(row.timeStr) + "</span></td>" +
|
|
17
|
+
'<td class="pl-cell"><a href="/_profiler/' + escapeHtml(row.token) + '" onclick="event.stopPropagation()" class="pl-token-link">' + escapeHtml(row.tokenShort) + "</a></td>" +
|
|
18
|
+
"</tr>"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function limitFromUrl() {
|
|
23
|
+
var limit = parseInt(new URLSearchParams(location.search).get("limit") || "10", 10);
|
|
24
|
+
return [10, 25, 50, 100].indexOf(limit) !== -1 ? limit : 10;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Live-updates the results table as the server profiles new requests —
|
|
29
|
+
* replaces the previous `setTimeout(() => location.reload(), 5000)`
|
|
30
|
+
* polling: the page only re-renders when the server actually pushes a
|
|
31
|
+
* new profile over SSE, never on a blind timer.
|
|
32
|
+
*/
|
|
33
|
+
function connectLiveUpdates() {
|
|
34
|
+
if (typeof EventSource === "undefined") return;
|
|
35
|
+
|
|
36
|
+
var table = document.querySelector(".pl-table tbody");
|
|
37
|
+
var heading = document.querySelector(".pl-section-heading");
|
|
38
|
+
var limit = limitFromUrl();
|
|
39
|
+
var count = table ? table.querySelectorAll(".pl-row").length : 0;
|
|
40
|
+
|
|
41
|
+
var es = new EventSource("/_profiler/stream");
|
|
42
|
+
es.addEventListener("profile", function (evt) {
|
|
43
|
+
if (!table) return;
|
|
44
|
+
var row = JSON.parse(evt.data);
|
|
45
|
+
|
|
46
|
+
var emptyRow = table.querySelector(".pl-empty-row");
|
|
47
|
+
if (emptyRow) emptyRow.closest("tr").remove();
|
|
48
|
+
|
|
49
|
+
table.insertAdjacentHTML("afterbegin", rowHtml(row));
|
|
50
|
+
count++;
|
|
51
|
+
|
|
52
|
+
while (table.querySelectorAll(".pl-row").length > limit) {
|
|
53
|
+
table.removeChild(table.lastElementChild);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (heading) {
|
|
57
|
+
var shown = Math.min(count, limit);
|
|
58
|
+
var i18n = window.__opticoreProfilerI18n || {};
|
|
59
|
+
var template = shown === 1
|
|
60
|
+
? (i18n.resultsFoundOne || "{n} result found")
|
|
61
|
+
: (i18n.resultsFoundOther || "{n} results found");
|
|
62
|
+
heading.textContent = template.replace("{n}", shown);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (document.body.dataset.autoreload === "1") {
|
|
68
|
+
connectLiveUpdates();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
document.addEventListener("keydown", e => {
|
|
72
|
+
if (e.key === "r" && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== "INPUT") {
|
|
73
|
+
location.reload();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
})();
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
let ROUTE_PREFIX = window.__opticoreProfilerRoutePrefix || "/_profiler";
|
|
3
|
+
let OWN_TOKEN = window.__opticoreProfilerToken || null;
|
|
4
|
+
let MAX_ROWS = 10;
|
|
5
|
+
// A request reaches addAjaxRow at most once even though two independent
|
|
6
|
+
// sources feed it: the fetch/XHR instrumentation (for calls this page's
|
|
7
|
+
// own JS makes) and the SSE stream (for every request the server
|
|
8
|
+
// profiles, including this page's own — both fire for the same token).
|
|
9
|
+
let seenTokens = {};
|
|
10
|
+
|
|
11
|
+
// collapse the full bar down to the bottom-right mini pill (does NOT close it)
|
|
12
|
+
window.wdtCollapse = function () {
|
|
13
|
+
let bar = document.getElementById("wdt");
|
|
14
|
+
let mini = document.getElementById("wdt-mini");
|
|
15
|
+
if (bar) {
|
|
16
|
+
bar.style.display = "none";
|
|
17
|
+
}
|
|
18
|
+
if (mini) {
|
|
19
|
+
mini.style.display = "flex";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
window.wdtExpand = function () {
|
|
23
|
+
let bar = document.getElementById("wdt");
|
|
24
|
+
let mini = document.getElementById("wdt-mini");
|
|
25
|
+
if (mini) {
|
|
26
|
+
mini.style.display = "none";
|
|
27
|
+
}
|
|
28
|
+
if (bar) {
|
|
29
|
+
bar.style.display = "flex";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function escapeHtml(str) {
|
|
34
|
+
return String(str == null ? "" : str)
|
|
35
|
+
.replace(/&/g, "&")
|
|
36
|
+
.replace(/</g, "<")
|
|
37
|
+
.replace(/>/g, ">")
|
|
38
|
+
.replace(/"/g, """);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function statusClass(code) {
|
|
42
|
+
if (code >= 400) {
|
|
43
|
+
return "tip-s-err";
|
|
44
|
+
}
|
|
45
|
+
if (code >= 300) {
|
|
46
|
+
return "tip-s-redir";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return "tip-s-ok";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fmtDuration(ms) {
|
|
53
|
+
return ms < 1000 ? Math.round(ms) + " ms" : (ms / 1000).toFixed(2) + " s";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function truncateUrl(url, max) {
|
|
57
|
+
max = max || 38;
|
|
58
|
+
return url.length > max ? url.slice(0, max - 1) + "…" : url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Same rule as the server's isApiRequest() (toolbar.view.ts): an HTML
|
|
62
|
+
* response is a page navigation, not a business/API call — keep those
|
|
63
|
+
* out of the "HTTP requests" panel. */
|
|
64
|
+
function isApiContentType(contentType) {
|
|
65
|
+
return !contentType || contentType.toLowerCase().indexOf("text/html") !== 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isProfilerUrl(url) {
|
|
69
|
+
try {
|
|
70
|
+
let path = new URL(url, window.location.origin).pathname;
|
|
71
|
+
return path.indexOf(ROUTE_PREFIX) === 0;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Records one AJAX request the current page made, mirroring what the
|
|
79
|
+
* server already knows about it (X-Debug-Token / X-Debug-Token-Link
|
|
80
|
+
* headers, present on every profiled response) into the toolbar's
|
|
81
|
+
* "HTTP requests" list — client-side, without polling the server.
|
|
82
|
+
*/
|
|
83
|
+
function addAjaxRow(entry) {
|
|
84
|
+
if (seenTokens[entry.token]) return;
|
|
85
|
+
seenTokens[entry.token] = true;
|
|
86
|
+
|
|
87
|
+
let table = document.getElementById("wdt-ajax-table");
|
|
88
|
+
let countEl = document.getElementById("wdt-ajax-count");
|
|
89
|
+
let emptyEl = document.getElementById("wdt-ajax-empty");
|
|
90
|
+
if (!table) return;
|
|
91
|
+
|
|
92
|
+
let tbody = table.querySelector("tbody");
|
|
93
|
+
if (!tbody) return;
|
|
94
|
+
|
|
95
|
+
if (emptyEl) emptyEl.style.display = "none";
|
|
96
|
+
|
|
97
|
+
let row = document.createElement("tr");
|
|
98
|
+
row.innerHTML =
|
|
99
|
+
'<td><span class="tip-method tip-m-' + escapeHtml(entry.method.toLowerCase()) + '">' + escapeHtml(entry.method) + "</span></td>" +
|
|
100
|
+
'<td><a href="' + escapeHtml(ROUTE_PREFIX + "/" + entry.token) + '" class="tip-url-link" title="' + escapeHtml(entry.url) + '">' + escapeHtml(truncateUrl(entry.url)) + "</a></td>" +
|
|
101
|
+
'<td class="' + statusClass(entry.statusCode) + '">' + entry.statusCode + "</td>" +
|
|
102
|
+
'<td class="tip-dur">' + fmtDuration(entry.duration) + "</td>";
|
|
103
|
+
tbody.insertBefore(row, tbody.firstChild);
|
|
104
|
+
|
|
105
|
+
while (tbody.children.length > MAX_ROWS) {
|
|
106
|
+
tbody.removeChild(tbody.lastChild);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let n = tbody.children.length;
|
|
110
|
+
if (countEl) {
|
|
111
|
+
countEl.textContent = n;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The "Last N HTTP Requests" / "View all" header is only rendered
|
|
115
|
+
// server-side when the toolbar's initial snapshot already had a row
|
|
116
|
+
// (see toolbar-wdt.njk) — without this, the very first row added
|
|
117
|
+
// live (e.g. the page loaded with 0 API requests logged yet) would
|
|
118
|
+
// leave the header stuck on the empty-state "HTTP Requests" text,
|
|
119
|
+
// permanently hiding the "View all" link even once rows exist.
|
|
120
|
+
let headerEl = document.getElementById("wdt-ajax-header");
|
|
121
|
+
if (headerEl) {
|
|
122
|
+
let i18n = window.__opticoreProfilerI18n || {};
|
|
123
|
+
let lastNTemplate = i18n.lastNRequests || "Last {n} HTTP Requests";
|
|
124
|
+
let viewAllLabel = i18n.viewAll || "View all →";
|
|
125
|
+
headerEl.innerHTML =
|
|
126
|
+
"<span>" + escapeHtml(lastNTemplate.replace("{n}", n)) + "</span>" +
|
|
127
|
+
'<a href="' + escapeHtml(ROUTE_PREFIX) + '" class="tb-tip-viewall">' + escapeHtml(viewAllLabel) + "</a>";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function fromHeaders(method, url, statusCode, duration, getHeader) {
|
|
132
|
+
let token = getHeader("x-debug-token");
|
|
133
|
+
if (!token || isProfilerUrl(url)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!isApiContentType(getHeader("content-type"))) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
addAjaxRow({ method: method, url: url, token: token, statusCode: statusCode, duration: duration });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function instrumentFetch() {
|
|
143
|
+
if (typeof window.fetch !== "function" || window.fetch.__opticoreProfilerWrapped) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let originalFetch = window.fetch;
|
|
148
|
+
let wrapped = function (input, init) {
|
|
149
|
+
let method = (init && init.method) || (input && input.method) || "GET";
|
|
150
|
+
let url = typeof input === "string" ? input : (input && input.url) || "";
|
|
151
|
+
let start = performance.now();
|
|
152
|
+
|
|
153
|
+
return originalFetch.apply(this, arguments).then(function (response) {
|
|
154
|
+
fromHeaders(method, url || response.url, response.status, performance.now() - start, function (h) {
|
|
155
|
+
return response.headers.get(h);
|
|
156
|
+
});
|
|
157
|
+
return response;
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
wrapped.__opticoreProfilerWrapped = true;
|
|
161
|
+
window.fetch = wrapped;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function instrumentXhr() {
|
|
165
|
+
let proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
|
|
166
|
+
if (!proto || proto.open.__opticoreProfilerWrapped) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let originalOpen = proto.open;
|
|
171
|
+
let originalSend = proto.send;
|
|
172
|
+
|
|
173
|
+
proto.open = function (method, url) {
|
|
174
|
+
this.__opticoreMethod = method;
|
|
175
|
+
this.__opticoreUrl = url;
|
|
176
|
+
return originalOpen.apply(this, arguments);
|
|
177
|
+
};
|
|
178
|
+
proto.open.__opticoreProfilerWrapped = true;
|
|
179
|
+
|
|
180
|
+
proto.send = function () {
|
|
181
|
+
let start = performance.now();
|
|
182
|
+
this.addEventListener("loadend", function () {
|
|
183
|
+
fromHeaders(
|
|
184
|
+
this.__opticoreMethod || "GET",
|
|
185
|
+
this.__opticoreUrl || "",
|
|
186
|
+
this.status,
|
|
187
|
+
performance.now() - start,
|
|
188
|
+
function (h) { return this.getResponseHeader(h); }.bind(this)
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
return originalSend.apply(this, arguments);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Keeps the toolbar's HTTP-requests counter and table in sync with
|
|
197
|
+
* requests the *server* profiles — including ones this page's own JS
|
|
198
|
+
* never made (another tab, curl, Postman...) — by subscribing to the
|
|
199
|
+
* same SSE feed that already powers the profiler list page's live
|
|
200
|
+
* updates, instead of leaving the toolbar frozen at its page-load
|
|
201
|
+
* snapshot until a manual reload.
|
|
202
|
+
*/
|
|
203
|
+
function connectLiveUpdates() {
|
|
204
|
+
if (typeof EventSource === "undefined") {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let es = new EventSource(ROUTE_PREFIX + "/stream");
|
|
209
|
+
es.addEventListener("profile", function (evt) {
|
|
210
|
+
let row = JSON.parse(evt.data);
|
|
211
|
+
if (OWN_TOKEN && row.token === OWN_TOKEN) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!isApiContentType(row.contentType)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
addAjaxRow({
|
|
218
|
+
method: row.method,
|
|
219
|
+
url: row.url,
|
|
220
|
+
token: row.token,
|
|
221
|
+
statusCode: row.statusCode,
|
|
222
|
+
duration: row.duration,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
instrumentFetch();
|
|
228
|
+
instrumentXhr();
|
|
229
|
+
connectLiveUpdates();
|
|
230
|
+
})();
|