anlyx 0.1.2 → 0.1.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.
- package/dist/dev-command.d.ts +25 -0
- package/dist/dev-command.d.ts.map +1 -1
- package/dist/dev-command.js +1365 -14
- package/dist/dev-command.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/init-command.d.ts.map +1 -1
- package/dist/init-command.js +6 -1
- package/dist/init-command.js.map +1 -1
- package/dist/next.d.ts +8 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +16 -0
- package/dist/next.js.map +1 -0
- package/package.json +24 -8
package/dist/dev-command.js
CHANGED
|
@@ -6,7 +6,9 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { scanResultSchema } from "@anlyx/core";
|
|
7
7
|
import { createServer } from "vite";
|
|
8
8
|
import { loadConfig } from "./config-loader.js";
|
|
9
|
+
import { runScanCommand } from "./scan-command.js";
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
11
|
+
const activeLocalUiServers = new Set();
|
|
10
12
|
export async function runDevCommand(options = {}) {
|
|
11
13
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
12
14
|
const dependencies = withDefaultDependencies(options.dependencies);
|
|
@@ -14,24 +16,97 @@ export async function runDevCommand(options = {}) {
|
|
|
14
16
|
cwd,
|
|
15
17
|
...(options.configPath ? { configPath: options.configPath } : {})
|
|
16
18
|
});
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
+
const outputDir = resolve(cwd, options.outputDir ?? ".anlyx");
|
|
20
|
+
const reportDataPath = join(outputDir, "report-data.json");
|
|
21
|
+
const { reportData, scanRan } = await ensureReportData({
|
|
22
|
+
cwd,
|
|
23
|
+
reportDataPath,
|
|
24
|
+
dependencies,
|
|
25
|
+
...(options.configPath ? { configPath: options.configPath } : {}),
|
|
26
|
+
...(options.outputDir ? { outputDir: options.outputDir } : {})
|
|
27
|
+
});
|
|
28
|
+
const frontendStarted = await ensureFrontendDevServer({
|
|
29
|
+
cwd,
|
|
30
|
+
config,
|
|
31
|
+
dependencies
|
|
32
|
+
});
|
|
19
33
|
const port = options.port ?? getConfiguredPort(config);
|
|
20
34
|
const server = await dependencies.createLocalUiServer({
|
|
21
35
|
port,
|
|
22
36
|
reportData,
|
|
23
|
-
viewerRoot: getViewerRoot()
|
|
37
|
+
viewerRoot: getViewerRoot(),
|
|
38
|
+
frontendBaseUrl: config.frontend.baseUrl,
|
|
39
|
+
mode: config.server.mode
|
|
24
40
|
});
|
|
41
|
+
activeLocalUiServers.add(server);
|
|
25
42
|
const shouldOpenBrowser = options.open ?? config.server.openBrowser;
|
|
43
|
+
const browserUrl = config.server.mode === "inject" ? config.frontend.baseUrl : server.url;
|
|
26
44
|
if (shouldOpenBrowser) {
|
|
27
|
-
await dependencies.openBrowser(
|
|
45
|
+
await dependencies.openBrowser(browserUrl);
|
|
28
46
|
}
|
|
29
47
|
return {
|
|
30
48
|
url: server.url,
|
|
31
49
|
port,
|
|
32
|
-
reportDataPath
|
|
50
|
+
reportDataPath,
|
|
51
|
+
mode: config.server.mode,
|
|
52
|
+
frontendStarted,
|
|
53
|
+
scanRan,
|
|
54
|
+
...(config.server.mode === "inject" ? { frontendUrl: config.frontend.baseUrl } : {}),
|
|
55
|
+
...(config.server.mode === "inject" ? { scriptTag: getOverlayScriptTag(server.url) } : {})
|
|
33
56
|
};
|
|
34
57
|
}
|
|
58
|
+
export async function closeActiveLocalUiServers() {
|
|
59
|
+
const servers = Array.from(activeLocalUiServers);
|
|
60
|
+
activeLocalUiServers.clear();
|
|
61
|
+
await Promise.all(servers.map(async (server) => {
|
|
62
|
+
await server.close?.();
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
async function ensureReportData(options) {
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
reportData: await options.dependencies.readReportData(options.reportDataPath),
|
|
69
|
+
scanRan: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!isMissingReportDataError(error)) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await options.dependencies.runScanCommand({
|
|
79
|
+
cwd: options.cwd,
|
|
80
|
+
...(options.configPath ? { configPath: options.configPath } : {}),
|
|
81
|
+
...(options.outputDir ? { outputDir: options.outputDir } : {}),
|
|
82
|
+
skipCapture: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`Automatic scan failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
reportData: await options.dependencies.readReportData(options.reportDataPath),
|
|
90
|
+
scanRan: true
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function ensureFrontendDevServer(options) {
|
|
94
|
+
const command = options.config.dev?.command;
|
|
95
|
+
if (!command) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (await options.dependencies.isFrontendReachable(options.config.frontend.baseUrl)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
options.dependencies.startFrontendDevServer({
|
|
102
|
+
command,
|
|
103
|
+
cwd: options.cwd
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function isMissingReportDataError(error) {
|
|
108
|
+
return error instanceof Error && /Run "anlyx scan" first/.test(error.message);
|
|
109
|
+
}
|
|
35
110
|
export async function readReportData(path) {
|
|
36
111
|
let content;
|
|
37
112
|
try {
|
|
@@ -71,7 +146,7 @@ export async function createLocalUiServer(options) {
|
|
|
71
146
|
port: options.port,
|
|
72
147
|
strictPort: true
|
|
73
148
|
},
|
|
74
|
-
plugins: [
|
|
149
|
+
plugins: [createAnlyxDevPlugin(options)]
|
|
75
150
|
});
|
|
76
151
|
await viteServer.listen();
|
|
77
152
|
return {
|
|
@@ -87,33 +162,1309 @@ export async function openBrowser(url) {
|
|
|
87
162
|
});
|
|
88
163
|
child.unref();
|
|
89
164
|
}
|
|
90
|
-
function
|
|
165
|
+
export async function isFrontendReachable(url) {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const timeout = setTimeout(() => controller.abort(), 1200);
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(url, {
|
|
170
|
+
method: "GET",
|
|
171
|
+
signal: controller.signal
|
|
172
|
+
});
|
|
173
|
+
return response.status < 500;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function startFrontendDevServer(options) {
|
|
183
|
+
const child = spawn(options.command, {
|
|
184
|
+
cwd: options.cwd,
|
|
185
|
+
shell: true,
|
|
186
|
+
stdio: "inherit"
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
stop: () => {
|
|
190
|
+
if (!child.killed) {
|
|
191
|
+
child.kill();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function createAnlyxDevPlugin(options) {
|
|
91
197
|
return {
|
|
92
|
-
name: "anlyx-
|
|
198
|
+
name: "anlyx-dev-runtime",
|
|
93
199
|
configureServer(server) {
|
|
94
200
|
server.middlewares.use((request, response, next) => {
|
|
95
|
-
if (request.method === "GET" && request.url === "/") {
|
|
201
|
+
if (request.method === "GET" && options.mode === "viewer" && request.url === "/") {
|
|
96
202
|
request.url = "/viewer.html";
|
|
97
203
|
}
|
|
98
204
|
next();
|
|
99
205
|
});
|
|
100
|
-
server.middlewares.use(
|
|
101
|
-
|
|
206
|
+
server.middlewares.use(async (request, response, next) => {
|
|
207
|
+
const requestUrl = request.url ?? "/";
|
|
208
|
+
if (request.method === "OPTIONS" && isAnlyxPath(requestUrl)) {
|
|
209
|
+
response.statusCode = 204;
|
|
210
|
+
setCorsHeaders(response);
|
|
211
|
+
response.end();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (request.method === "GET" && isReportDataPath(requestUrl)) {
|
|
215
|
+
sendJson(response, options.reportData);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (request.method === "GET" && requestUrl === "/_anlyx/overlay.js") {
|
|
219
|
+
response.statusCode = 200;
|
|
220
|
+
setCorsHeaders(response);
|
|
221
|
+
response.setHeader("content-type", "application/javascript; charset=utf-8");
|
|
222
|
+
response.end(getOverlayClientScript());
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (request.method === "GET" && requestUrl === "/_anlyx/overlay-ui.js") {
|
|
226
|
+
await sendRuntimeAsset(response, join(options.viewerRoot, "../overlay/overlay-ui.js"), "application/javascript; charset=utf-8");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (request.method === "GET" && requestUrl === "/_anlyx/overlay-ui.css") {
|
|
230
|
+
await sendRuntimeAsset(response, join(options.viewerRoot, "../overlay/overlay-ui.css"), "text/css; charset=utf-8");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (request.method === "GET" && options.mode === "inject" && requestUrl === "/") {
|
|
234
|
+
response.statusCode = 200;
|
|
235
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
236
|
+
response.end(getInjectModeHtml(options.frontendBaseUrl, getOverlayScriptTag(getServerUrl(options.port))));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (request.method === "GET" && isStandaloneViewerPath(requestUrl)) {
|
|
240
|
+
request.url = "/viewer.html";
|
|
102
241
|
next();
|
|
103
242
|
return;
|
|
104
243
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
244
|
+
if (options.mode === "viewer" || options.mode === "inject") {
|
|
245
|
+
next();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (isAnlyxPath(requestUrl)) {
|
|
249
|
+
response.statusCode = 404;
|
|
250
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
251
|
+
response.end("Anlyx runtime asset not found.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await proxyToFrontend(request, response, options.frontendBaseUrl);
|
|
108
255
|
});
|
|
109
256
|
}
|
|
110
257
|
};
|
|
111
258
|
}
|
|
259
|
+
function isReportDataPath(path) {
|
|
260
|
+
return path === "/_anlyx/report-data" || path === "/api/report-data";
|
|
261
|
+
}
|
|
262
|
+
function isStandaloneViewerPath(path) {
|
|
263
|
+
return path === "/_anlyx/viewer" || path === "/_anlyx/viewer.html";
|
|
264
|
+
}
|
|
265
|
+
function isAnlyxPath(path) {
|
|
266
|
+
return path.startsWith("/_anlyx/");
|
|
267
|
+
}
|
|
268
|
+
function sendJson(response, value) {
|
|
269
|
+
response.statusCode = 200;
|
|
270
|
+
setCorsHeaders(response);
|
|
271
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
272
|
+
response.end(JSON.stringify(value));
|
|
273
|
+
}
|
|
274
|
+
async function sendRuntimeAsset(response, path, contentType) {
|
|
275
|
+
try {
|
|
276
|
+
const content = await readFile(path);
|
|
277
|
+
response.statusCode = 200;
|
|
278
|
+
setCorsHeaders(response);
|
|
279
|
+
response.setHeader("content-type", contentType);
|
|
280
|
+
response.end(content);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
response.statusCode = 404;
|
|
284
|
+
setCorsHeaders(response);
|
|
285
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
286
|
+
response.end("Anlyx runtime asset not found.");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function setCorsHeaders(response) {
|
|
290
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
291
|
+
response.setHeader("access-control-allow-methods", "GET, OPTIONS");
|
|
292
|
+
response.setHeader("access-control-allow-headers", "content-type");
|
|
293
|
+
}
|
|
294
|
+
function getServerUrl(port) {
|
|
295
|
+
return `http://localhost:${port}`;
|
|
296
|
+
}
|
|
297
|
+
async function proxyToFrontend(request, response, frontendBaseUrl) {
|
|
298
|
+
const targetUrl = buildProxyTargetUrl(frontendBaseUrl, request.url ?? "/");
|
|
299
|
+
const method = request.method ?? "GET";
|
|
300
|
+
try {
|
|
301
|
+
const requestInit = {
|
|
302
|
+
method,
|
|
303
|
+
headers: getProxyRequestHeaders(request),
|
|
304
|
+
redirect: "manual"
|
|
305
|
+
};
|
|
306
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
307
|
+
const body = await readRequestBody(request);
|
|
308
|
+
if (body !== undefined) {
|
|
309
|
+
requestInit.body = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const upstream = await fetch(targetUrl, requestInit);
|
|
313
|
+
response.statusCode = upstream.status;
|
|
314
|
+
response.statusMessage = upstream.statusText;
|
|
315
|
+
upstream.headers.forEach((value, key) => {
|
|
316
|
+
if (!shouldOmitProxyResponseHeader(key)) {
|
|
317
|
+
response.setHeader(key, value);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
321
|
+
if (method === "GET" && contentType.includes("text/html")) {
|
|
322
|
+
const html = await upstream.text();
|
|
323
|
+
response.setHeader("content-type", contentType);
|
|
324
|
+
response.end(injectOverlayScript(html));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const body = Buffer.from(await upstream.arrayBuffer());
|
|
328
|
+
response.end(body);
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
response.statusCode = 502;
|
|
332
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
333
|
+
response.end(getProxyErrorHtml(frontendBaseUrl, error));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
export function buildProxyTargetUrl(frontendBaseUrl, requestUrl) {
|
|
337
|
+
const base = new URL(frontendBaseUrl);
|
|
338
|
+
const target = new URL(requestUrl, base);
|
|
339
|
+
target.protocol = base.protocol;
|
|
340
|
+
target.host = base.host;
|
|
341
|
+
return target.toString();
|
|
342
|
+
}
|
|
343
|
+
export function injectOverlayScript(html) {
|
|
344
|
+
if (html.includes("/_anlyx/overlay.js")) {
|
|
345
|
+
return html;
|
|
346
|
+
}
|
|
347
|
+
const script = '<script src="/_anlyx/overlay.js" defer></script>';
|
|
348
|
+
if (html.includes("</body>")) {
|
|
349
|
+
return html.replace("</body>", `${script}</body>`);
|
|
350
|
+
}
|
|
351
|
+
return `${html}${script}`;
|
|
352
|
+
}
|
|
353
|
+
export function getOverlayScriptTag(serverUrl) {
|
|
354
|
+
return `<script src="${serverUrl}/_anlyx/overlay.js" defer></script>`;
|
|
355
|
+
}
|
|
356
|
+
function getInjectModeHtml(frontendBaseUrl, scriptTag) {
|
|
357
|
+
return `<!doctype html>
|
|
358
|
+
<html lang="en">
|
|
359
|
+
<head>
|
|
360
|
+
<meta charset="utf-8" />
|
|
361
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
362
|
+
<title>Anlyx Inject Mode</title>
|
|
363
|
+
<style>
|
|
364
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f8fafc; color: #0f172a; }
|
|
365
|
+
main { width: min(760px, calc(100vw - 32px)); border: 1px solid #dbe4f0; border-radius: 16px; background: white; box-shadow: 0 18px 60px rgba(15, 23, 42, 0.12); padding: 28px; }
|
|
366
|
+
h1 { margin: 0 0 10px; font-size: 22px; line-height: 1.25; }
|
|
367
|
+
p { margin: 8px 0; color: #475569; line-height: 1.55; }
|
|
368
|
+
pre { overflow: auto; border-radius: 12px; background: #0f172a; color: #e2e8f0; padding: 14px; font-size: 13px; line-height: 1.5; }
|
|
369
|
+
a { color: #2563eb; font-weight: 800; text-decoration: none; }
|
|
370
|
+
</style>
|
|
371
|
+
</head>
|
|
372
|
+
<body>
|
|
373
|
+
<main>
|
|
374
|
+
<h1>Anlyx runtime is ready</h1>
|
|
375
|
+
<p>Open your real frontend at <a href="${escapeHtml(frontendBaseUrl)}">${escapeHtml(frontendBaseUrl)}</a> and inject this local-only script into the app during development.</p>
|
|
376
|
+
<pre>${escapeHtml(scriptTag)}</pre>
|
|
377
|
+
<p>The standalone debug viewer is still available at <a href="/_anlyx/viewer">/_anlyx/viewer</a>.</p>
|
|
378
|
+
</main>
|
|
379
|
+
</body>
|
|
380
|
+
</html>`;
|
|
381
|
+
}
|
|
382
|
+
function getProxyRequestHeaders(request) {
|
|
383
|
+
const headers = new Headers();
|
|
384
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
385
|
+
if (value === undefined || shouldOmitProxyRequestHeader(key)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (Array.isArray(value)) {
|
|
389
|
+
for (const item of value) {
|
|
390
|
+
headers.append(key, item);
|
|
391
|
+
}
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
headers.set(key, value);
|
|
395
|
+
}
|
|
396
|
+
return headers;
|
|
397
|
+
}
|
|
398
|
+
function shouldOmitProxyRequestHeader(key) {
|
|
399
|
+
return ["host", "connection", "content-length"].includes(key.toLowerCase());
|
|
400
|
+
}
|
|
401
|
+
function shouldOmitProxyResponseHeader(key) {
|
|
402
|
+
return ["connection", "content-encoding", "content-length", "transfer-encoding"].includes(key.toLowerCase());
|
|
403
|
+
}
|
|
404
|
+
function readRequestBody(request) {
|
|
405
|
+
return new Promise((resolveBody, reject) => {
|
|
406
|
+
const chunks = [];
|
|
407
|
+
request.on("data", (chunk) => {
|
|
408
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
409
|
+
});
|
|
410
|
+
request.on("end", () => {
|
|
411
|
+
resolveBody(chunks.length > 0 ? Buffer.concat(chunks) : undefined);
|
|
412
|
+
});
|
|
413
|
+
request.on("error", reject);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function getProxyErrorHtml(frontendBaseUrl, error) {
|
|
417
|
+
const message = error instanceof Error ? error.message : "Unknown proxy error";
|
|
418
|
+
return `<!doctype html>
|
|
419
|
+
<html lang="en">
|
|
420
|
+
<head>
|
|
421
|
+
<meta charset="utf-8" />
|
|
422
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
423
|
+
<title>Anlyx proxy error</title>
|
|
424
|
+
<style>
|
|
425
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f8fafc; color: #0f172a; }
|
|
426
|
+
main { width: min(560px, calc(100vw - 32px)); border: 1px solid #dbe4f0; border-radius: 16px; background: white; box-shadow: 0 18px 60px rgba(15, 23, 42, 0.12); padding: 28px; }
|
|
427
|
+
h1 { margin: 0 0 10px; font-size: 22px; line-height: 1.25; }
|
|
428
|
+
p { margin: 8px 0; color: #475569; line-height: 1.55; }
|
|
429
|
+
code { border-radius: 8px; background: #eef4ff; color: #1d4ed8; padding: 2px 6px; }
|
|
430
|
+
</style>
|
|
431
|
+
</head>
|
|
432
|
+
<body>
|
|
433
|
+
<main>
|
|
434
|
+
<h1>Anlyx could not reach the frontend app</h1>
|
|
435
|
+
<p>Overlay Mode proxies your configured frontend at <code>${escapeHtml(frontendBaseUrl)}</code>.</p>
|
|
436
|
+
<p>Start the frontend dev server, then refresh this page. The standalone viewer is still available at <code>/_anlyx/viewer</code>.</p>
|
|
437
|
+
<p>${escapeHtml(message)}</p>
|
|
438
|
+
</main>
|
|
439
|
+
</body>
|
|
440
|
+
</html>`;
|
|
441
|
+
}
|
|
442
|
+
function escapeHtml(value) {
|
|
443
|
+
return value
|
|
444
|
+
.replace(/&/g, "&")
|
|
445
|
+
.replace(/</g, "<")
|
|
446
|
+
.replace(/>/g, ">")
|
|
447
|
+
.replace(/"/g, """)
|
|
448
|
+
.replace(/'/g, "'");
|
|
449
|
+
}
|
|
450
|
+
export function getOverlayClientScript() {
|
|
451
|
+
return String.raw `
|
|
452
|
+
(() => {
|
|
453
|
+
if (window.__ANLYX_OVERLAY_INSTALLED__) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
window.__ANLYX_OVERLAY_INSTALLED__ = true;
|
|
457
|
+
|
|
458
|
+
const state = {
|
|
459
|
+
report: null,
|
|
460
|
+
events: [],
|
|
461
|
+
actions: [],
|
|
462
|
+
selectedEventId: null,
|
|
463
|
+
open: false,
|
|
464
|
+
loadError: null
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
let drawer = null;
|
|
468
|
+
let body = null;
|
|
469
|
+
let launcher = null;
|
|
470
|
+
let overlayUiReady = false;
|
|
471
|
+
let overlayUiLoading = false;
|
|
472
|
+
let overlayRootGuardInstalled = false;
|
|
473
|
+
let overlayRootRestoreScheduled = false;
|
|
474
|
+
let overlayInfrastructureInstalled = false;
|
|
475
|
+
const endpointRegexCache = new Map();
|
|
476
|
+
const currentScript = document.currentScript;
|
|
477
|
+
const runtimeBaseUrl = currentScript && currentScript.src ? new URL(currentScript.src).origin : window.location.origin;
|
|
478
|
+
const ANLYX_PENDING_ACTION_KEY = "__anlyx_pending_action__";
|
|
479
|
+
const ANLYX_DRAWER_SETTINGS_KEY = "__anlyx_drawer_settings__";
|
|
480
|
+
const ANLYX_LAUNCHER_SETTINGS_KEY = "__anlyx_launcher_settings__";
|
|
481
|
+
const drawerSettings = Object.assign({
|
|
482
|
+
width: 600,
|
|
483
|
+
height: 760,
|
|
484
|
+
x: null,
|
|
485
|
+
y: 12,
|
|
486
|
+
opacity: 0.98,
|
|
487
|
+
language: "en"
|
|
488
|
+
}, restoreDrawerSettings());
|
|
489
|
+
const launcherSettings = Object.assign({
|
|
490
|
+
x: null,
|
|
491
|
+
y: null,
|
|
492
|
+
expandedUntil: 0
|
|
493
|
+
}, restoreLauncherSettings());
|
|
494
|
+
|
|
495
|
+
scheduleOverlayMount();
|
|
496
|
+
|
|
497
|
+
function scheduleOverlayMount() {
|
|
498
|
+
const mount = () => window.setTimeout(mountOverlayUi, 0);
|
|
499
|
+
|
|
500
|
+
if (document.body) {
|
|
501
|
+
mount();
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (document.readyState === "loading") {
|
|
506
|
+
document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
mount();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function mountOverlayUi() {
|
|
514
|
+
const existingRoot = document.getElementById("anlyx-overlay-root");
|
|
515
|
+
if (existingRoot) {
|
|
516
|
+
launcher = existingRoot.querySelector(".anlyx-fab");
|
|
517
|
+
drawer = existingRoot.querySelector(".anlyx-drawer");
|
|
518
|
+
body = existingRoot.querySelector(".anlyx-body");
|
|
519
|
+
if (drawer && body) {
|
|
520
|
+
installOverlayRootGuard();
|
|
521
|
+
applyLauncherSettings();
|
|
522
|
+
render();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
existingRoot.remove();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!document.body) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!document.querySelector("style[data-anlyx-overlay-base]")) {
|
|
533
|
+
const style = document.createElement("style");
|
|
534
|
+
style.setAttribute("data-anlyx-overlay-base", "true");
|
|
535
|
+
style.textContent = ${"`"}
|
|
536
|
+
#anlyx-overlay-root { position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #0f172a; }
|
|
537
|
+
.anlyx-fab { pointer-events: auto; position: absolute; left: auto; top: auto; right: 18px; bottom: 18px; display: inline-flex; align-items: center; justify-content: flex-start; gap: 3px; width: 38px; height: 38px; min-width: 38px; max-width: 38px; padding: 0; overflow: hidden; border: 1px solid rgba(37, 99, 235, .20); border-radius: 999px; background: rgba(37, 99, 235, .72); color: white; font-weight: 850; font-size: 12px; box-shadow: 0 14px 36px rgba(37, 99, 235, 0.20); cursor: grab; opacity: .72; backdrop-filter: blur(10px); transition: width 160ms ease, max-width 160ms ease, opacity 160ms ease, background 160ms ease, box-shadow 160ms ease, transform 160ms ease; }
|
|
538
|
+
.anlyx-fab:hover, .anlyx-fab:focus-visible, .anlyx-fab[data-expanded="true"] { width: 86px; max-width: 86px; opacity: .96; background: rgba(37, 99, 235, .96); box-shadow: 0 18px 46px rgba(37, 99, 235, 0.26); transform: translate(-48px, -1px); }
|
|
539
|
+
.anlyx-fab:active { cursor: grabbing; transform: translateY(0); }
|
|
540
|
+
.anlyx-fab__mark { width: 38px; height: 38px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 38px; }
|
|
541
|
+
.anlyx-fab__mark svg { width: 22px; height: 22px; display: block; filter: drop-shadow(0 1px 1px rgba(15, 23, 42, .16)); }
|
|
542
|
+
.anlyx-fab__label { padding-right: 10px; white-space: nowrap; line-height: 1; letter-spacing: 0; opacity: 0; transform: translateX(-4px); transition: opacity 140ms ease, transform 140ms ease; }
|
|
543
|
+
.anlyx-fab:hover .anlyx-fab__label, .anlyx-fab:focus-visible .anlyx-fab__label, .anlyx-fab[data-expanded="true"] .anlyx-fab__label { opacity: 1; transform: translateX(0); }
|
|
544
|
+
.anlyx-drawer { pointer-events: auto; position: absolute; top: 12px; left: auto; right: auto; width: 600px; height: min(760px, calc(100vh - 24px)); min-width: 420px; min-height: 420px; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); border: 1px solid rgba(15, 23, 42, .12); border-radius: 18px; background: rgba(248, 250, 252, .98); box-shadow: 0 24px 80px rgba(15, 23, 42, 0.22); overflow: hidden; display: none; }
|
|
545
|
+
.anlyx-drawer[data-open="true"] { display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
546
|
+
.anlyx-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 12px 14px; border-bottom: 1px solid rgba(15, 23, 42, .08); background: rgba(255,255,255,.88); }
|
|
547
|
+
.anlyx-drag-handle { min-width: 0; cursor: grab; user-select: none; }
|
|
548
|
+
.anlyx-drag-handle:active { cursor: grabbing; }
|
|
549
|
+
.anlyx-title { margin: 0; font-size: 15px; line-height: 1.2; font-weight: 900; letter-spacing: 0; }
|
|
550
|
+
.anlyx-subtitle { margin: 3px 0 0; font-size: 11px; color: #64748b; font-weight: 650; }
|
|
551
|
+
.anlyx-shell-controls { display: flex; align-items: center; gap: 7px; }
|
|
552
|
+
.anlyx-shell-field { display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 8px; border: 1px solid #e2e8f0; border-radius: 10px; background: rgba(255,255,255,.9); color: #475569; font-size: 10px; font-weight: 800; white-space: nowrap; }
|
|
553
|
+
.anlyx-opacity-control { width: 70px; accent-color: #2563eb; }
|
|
554
|
+
.anlyx-language-control { width: 58px; border: 0; outline: 0; background: transparent; color: #0f172a; font-size: 10px; font-weight: 900; }
|
|
555
|
+
.anlyx-close { border: 1px solid #e2e8f0; background: #fff; border-radius: 10px; width: 32px; height: 32px; cursor: pointer; font-size: 17px; line-height: 1; color: #0f172a; box-shadow: 0 1px 2px rgba(15, 23, 42, .06); }
|
|
556
|
+
.anlyx-body { overflow: auto; padding: 12px; background: #f8fafc; }
|
|
557
|
+
.anlyx-resize-handle { position: absolute; left: 0; bottom: 0; width: 22px; height: 22px; cursor: nesw-resize; opacity: .62; }
|
|
558
|
+
.anlyx-resize-handle::before { content: ""; position: absolute; left: 6px; bottom: 6px; width: 10px; height: 10px; border-left: 2px solid #94a3b8; border-bottom: 2px solid #94a3b8; border-radius: 0 0 0 3px; }
|
|
559
|
+
.anlyx-section { border: 1px solid rgba(15, 23, 42, .08); border-radius: 14px; background: #fff; margin-bottom: 10px; overflow: hidden; box-shadow: 0 1px 2px rgba(15, 23, 42, .04); }
|
|
560
|
+
.anlyx-section-title { margin: 0; padding: 10px 12px; font-size: 10px; text-transform: uppercase; color: #64748b; letter-spacing: .08em; border-bottom: 1px solid #eef2f7; font-weight: 900; }
|
|
561
|
+
.anlyx-empty { padding: 18px 12px; color: #667085; font-size: 13px; line-height: 1.5; }
|
|
562
|
+
@media (max-width: 700px) {
|
|
563
|
+
.anlyx-drawer { min-width: 0; width: calc(100vw - 16px); height: calc(100vh - 16px); border-radius: 14px; }
|
|
564
|
+
.anlyx-head { grid-template-columns: 1fr; }
|
|
565
|
+
.anlyx-shell-controls { justify-content: space-between; }
|
|
566
|
+
}
|
|
567
|
+
${"`"};
|
|
568
|
+
document.head.appendChild(style);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const root = document.createElement("div");
|
|
572
|
+
root.id = "anlyx-overlay-root";
|
|
573
|
+
root.innerHTML = ${"`"}
|
|
574
|
+
<button class="anlyx-fab" type="button" aria-label="Open Anlyx" title="Open Anlyx">
|
|
575
|
+
<span class="anlyx-fab__mark" aria-hidden="true">
|
|
576
|
+
<svg viewBox="0 0 32 32" role="img" focusable="false">
|
|
577
|
+
<circle cx="8" cy="7" r="4.3" fill="none" stroke="currentColor" stroke-width="2.7" />
|
|
578
|
+
<path d="M12.4 7h5.2c2.2 0 4 1.8 4 4v3.7" fill="none" stroke="currentColor" stroke-width="2.7" stroke-linecap="round" />
|
|
579
|
+
<path d="M20.2 17.3h-5.6c-2.2 0-4 1.8-4 4v3.1h9.1" fill="none" stroke="currentColor" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round" />
|
|
580
|
+
<rect x="20.2" y="21.2" width="6.4" height="6.4" rx="1.8" fill="none" stroke="currentColor" stroke-width="2.7" />
|
|
581
|
+
<rect x="15" y="13.1" width="5.2" height="5.2" rx="1" fill="#f59e0b" transform="rotate(45 17.6 15.7)" />
|
|
582
|
+
</svg>
|
|
583
|
+
</span>
|
|
584
|
+
<span class="anlyx-fab__label">Anlyx</span>
|
|
585
|
+
</button>
|
|
586
|
+
<aside class="anlyx-drawer" aria-label="Anlyx flow drawer">
|
|
587
|
+
<div class="anlyx-head">
|
|
588
|
+
<div class="anlyx-drag-handle" data-anlyx-label="Move Anlyx drawer">
|
|
589
|
+
<h2 class="anlyx-title">Anlyx Flow Drawer</h2>
|
|
590
|
+
<p class="anlyx-subtitle">Click the real app and inspect the API flow.</p>
|
|
591
|
+
</div>
|
|
592
|
+
<div class="anlyx-shell-controls">
|
|
593
|
+
<label class="anlyx-shell-field">
|
|
594
|
+
<span class="anlyx-opacity-label">Opacity</span>
|
|
595
|
+
<input class="anlyx-opacity-control" type="range" min="70" max="100" step="5" aria-label="Anlyx opacity" />
|
|
596
|
+
</label>
|
|
597
|
+
<label class="anlyx-shell-field">
|
|
598
|
+
<span class="anlyx-language-label">Lang</span>
|
|
599
|
+
<select class="anlyx-language-control" aria-label="Anlyx language">
|
|
600
|
+
<option value="en">EN</option>
|
|
601
|
+
<option value="ko">KO</option>
|
|
602
|
+
</select>
|
|
603
|
+
</label>
|
|
604
|
+
<button class="anlyx-close" type="button" aria-label="Close Anlyx">×</button>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
<div class="anlyx-body"></div>
|
|
608
|
+
<div class="anlyx-resize-handle" role="separator" aria-label="Resize Anlyx drawer"></div>
|
|
609
|
+
</aside>
|
|
610
|
+
${"`"};
|
|
611
|
+
document.body.appendChild(root);
|
|
612
|
+
|
|
613
|
+
const button = root.querySelector(".anlyx-fab");
|
|
614
|
+
launcher = button;
|
|
615
|
+
drawer = root.querySelector(".anlyx-drawer");
|
|
616
|
+
body = root.querySelector(".anlyx-body");
|
|
617
|
+
const closeButton = root.querySelector(".anlyx-close");
|
|
618
|
+
const opacityControl = root.querySelector(".anlyx-opacity-control");
|
|
619
|
+
const languageControl = root.querySelector(".anlyx-language-control");
|
|
620
|
+
const dragHandle = root.querySelector(".anlyx-drag-handle");
|
|
621
|
+
const resizeHandle = root.querySelector(".anlyx-resize-handle");
|
|
622
|
+
|
|
623
|
+
installLauncherDrag(button);
|
|
624
|
+
button.addEventListener("click", () => {
|
|
625
|
+
if (button.__anlyxSuppressClick) {
|
|
626
|
+
button.__anlyxSuppressClick = false;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
state.open = !state.open;
|
|
630
|
+
render();
|
|
631
|
+
});
|
|
632
|
+
closeButton.addEventListener("click", () => {
|
|
633
|
+
state.open = false;
|
|
634
|
+
render();
|
|
635
|
+
});
|
|
636
|
+
if (opacityControl) {
|
|
637
|
+
opacityControl.addEventListener("input", () => {
|
|
638
|
+
drawerSettings.opacity = Number(opacityControl.value || 98) / 100;
|
|
639
|
+
applyDrawerSettings();
|
|
640
|
+
persistDrawerSettings();
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (languageControl) {
|
|
644
|
+
languageControl.addEventListener("change", () => {
|
|
645
|
+
drawerSettings.language = languageControl.value === "ko" ? "ko" : "en";
|
|
646
|
+
applyDrawerSettings();
|
|
647
|
+
persistDrawerSettings();
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
installDrawerDrag(dragHandle);
|
|
651
|
+
installDrawerResize(resizeHandle);
|
|
652
|
+
applyDrawerSettings();
|
|
653
|
+
applyLauncherSettings();
|
|
654
|
+
|
|
655
|
+
installOverlayRootGuard();
|
|
656
|
+
|
|
657
|
+
if (!overlayInfrastructureInstalled) {
|
|
658
|
+
overlayInfrastructureInstalled = true;
|
|
659
|
+
restorePendingAction();
|
|
660
|
+
installUserActionTracker(root);
|
|
661
|
+
installFetchInterceptor();
|
|
662
|
+
installXhrInterceptor();
|
|
663
|
+
loadReport();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
render();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function applyDrawerSettings() {
|
|
670
|
+
if (!drawer) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const viewportWidth = window.innerWidth || 1280;
|
|
674
|
+
const viewportHeight = window.innerHeight || 800;
|
|
675
|
+
const width = clamp(Number(drawerSettings.width) || 600, Math.min(420, viewportWidth - 16), viewportWidth - 16);
|
|
676
|
+
const height = clamp(Number(drawerSettings.height) || 760, Math.min(420, viewportHeight - 16), viewportHeight - 16);
|
|
677
|
+
const defaultX = viewportWidth - width - 12;
|
|
678
|
+
const x = clamp(drawerSettings.x === null ? defaultX : Number(drawerSettings.x), 8, viewportWidth - width - 8);
|
|
679
|
+
const y = clamp(Number(drawerSettings.y) || 12, 8, viewportHeight - height - 8);
|
|
680
|
+
drawerSettings.width = Math.round(width);
|
|
681
|
+
drawerSettings.height = Math.round(height);
|
|
682
|
+
drawerSettings.x = Math.round(x);
|
|
683
|
+
drawerSettings.y = Math.round(y);
|
|
684
|
+
drawerSettings.opacity = clamp(Number(drawerSettings.opacity) || 0.98, 0.7, 1);
|
|
685
|
+
drawer.style.width = drawerSettings.width + "px";
|
|
686
|
+
drawer.style.height = drawerSettings.height + "px";
|
|
687
|
+
drawer.style.left = drawerSettings.x + "px";
|
|
688
|
+
drawer.style.top = drawerSettings.y + "px";
|
|
689
|
+
drawer.style.opacity = String(drawerSettings.opacity);
|
|
690
|
+
|
|
691
|
+
const opacityControl = document.querySelector("#anlyx-overlay-root .anlyx-opacity-control");
|
|
692
|
+
const languageControl = document.querySelector("#anlyx-overlay-root .anlyx-language-control");
|
|
693
|
+
if (opacityControl) {
|
|
694
|
+
opacityControl.value = String(Math.round(drawerSettings.opacity * 100));
|
|
695
|
+
}
|
|
696
|
+
if (languageControl) {
|
|
697
|
+
languageControl.value = drawerSettings.language === "ko" ? "ko" : "en";
|
|
698
|
+
}
|
|
699
|
+
applyDrawerLanguage();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function applyLauncherSettings() {
|
|
703
|
+
if (!launcher) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const viewportWidth = window.innerWidth || 1280;
|
|
707
|
+
const viewportHeight = window.innerHeight || 800;
|
|
708
|
+
const width = 38;
|
|
709
|
+
const height = 38;
|
|
710
|
+
const defaultX = viewportWidth - width - 18;
|
|
711
|
+
const defaultY = viewportHeight - height - 18;
|
|
712
|
+
const x = clamp(launcherSettings.x === null ? defaultX : Number(launcherSettings.x), 8, viewportWidth - width - 8);
|
|
713
|
+
const y = clamp(launcherSettings.y === null ? defaultY : Number(launcherSettings.y), 8, viewportHeight - height - 8);
|
|
714
|
+
launcherSettings.x = Math.round(x);
|
|
715
|
+
launcherSettings.y = Math.round(y);
|
|
716
|
+
launcher.style.left = launcherSettings.x + "px";
|
|
717
|
+
launcher.style.top = launcherSettings.y + "px";
|
|
718
|
+
launcher.style.right = "auto";
|
|
719
|
+
launcher.style.bottom = "auto";
|
|
720
|
+
launcher.dataset.expanded = Date.now() < Number(launcherSettings.expandedUntil || 0) ? "true" : "false";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function applyDrawerLanguage() {
|
|
724
|
+
const isKo = drawerSettings.language === "ko";
|
|
725
|
+
const title = document.querySelector("#anlyx-overlay-root .anlyx-title");
|
|
726
|
+
const subtitle = document.querySelector("#anlyx-overlay-root .anlyx-subtitle");
|
|
727
|
+
const opacityLabel = document.querySelector("#anlyx-overlay-root .anlyx-opacity-label");
|
|
728
|
+
const languageLabel = document.querySelector("#anlyx-overlay-root .anlyx-language-label");
|
|
729
|
+
if (title) {
|
|
730
|
+
title.textContent = isKo ? "Anlyx 플로우 드로어" : "Anlyx Flow Drawer";
|
|
731
|
+
}
|
|
732
|
+
if (subtitle) {
|
|
733
|
+
subtitle.textContent = isKo ? "앱을 그대로 사용하며 API 흐름을 확인하세요." : "Click the real app and inspect the API flow.";
|
|
734
|
+
}
|
|
735
|
+
if (opacityLabel) {
|
|
736
|
+
opacityLabel.textContent = isKo ? "투명도" : "Opacity";
|
|
737
|
+
}
|
|
738
|
+
if (languageLabel) {
|
|
739
|
+
languageLabel.textContent = isKo ? "언어" : "Lang";
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function installDrawerDrag(handle) {
|
|
744
|
+
if (!handle || !drawer) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
748
|
+
if (event.button !== 0) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const startX = event.clientX;
|
|
752
|
+
const startY = event.clientY;
|
|
753
|
+
const initialX = Number(drawerSettings.x) || drawer.getBoundingClientRect().left;
|
|
754
|
+
const initialY = Number(drawerSettings.y) || drawer.getBoundingClientRect().top;
|
|
755
|
+
const onMove = (moveEvent) => {
|
|
756
|
+
drawerSettings.x = initialX + moveEvent.clientX - startX;
|
|
757
|
+
drawerSettings.y = initialY + moveEvent.clientY - startY;
|
|
758
|
+
applyDrawerSettings();
|
|
759
|
+
};
|
|
760
|
+
const onUp = () => {
|
|
761
|
+
window.removeEventListener("pointermove", onMove);
|
|
762
|
+
window.removeEventListener("pointerup", onUp);
|
|
763
|
+
persistDrawerSettings();
|
|
764
|
+
};
|
|
765
|
+
window.addEventListener("pointermove", onMove);
|
|
766
|
+
window.addEventListener("pointerup", onUp, { once: true });
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function installDrawerResize(handle) {
|
|
771
|
+
if (!handle || !drawer) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
775
|
+
if (event.button !== 0) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
event.preventDefault();
|
|
779
|
+
const startX = event.clientX;
|
|
780
|
+
const startY = event.clientY;
|
|
781
|
+
const initialX = Number(drawerSettings.x) || drawer.getBoundingClientRect().left;
|
|
782
|
+
const initialWidth = Number(drawerSettings.width) || drawer.getBoundingClientRect().width;
|
|
783
|
+
const initialHeight = Number(drawerSettings.height) || drawer.getBoundingClientRect().height;
|
|
784
|
+
const onMove = (moveEvent) => {
|
|
785
|
+
const width = initialWidth - (moveEvent.clientX - startX);
|
|
786
|
+
drawerSettings.width = width;
|
|
787
|
+
drawerSettings.height = initialHeight + moveEvent.clientY - startY;
|
|
788
|
+
drawerSettings.x = initialX + initialWidth - width;
|
|
789
|
+
applyDrawerSettings();
|
|
790
|
+
};
|
|
791
|
+
const onUp = () => {
|
|
792
|
+
window.removeEventListener("pointermove", onMove);
|
|
793
|
+
window.removeEventListener("pointerup", onUp);
|
|
794
|
+
persistDrawerSettings();
|
|
795
|
+
};
|
|
796
|
+
window.addEventListener("pointermove", onMove);
|
|
797
|
+
window.addEventListener("pointerup", onUp, { once: true });
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function installLauncherDrag(button) {
|
|
802
|
+
if (!button) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
button.addEventListener("pointerdown", (event) => {
|
|
806
|
+
if (event.button !== 0) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const startX = event.clientX;
|
|
810
|
+
const startY = event.clientY;
|
|
811
|
+
const initialX = launcherSettings.x === null ? button.getBoundingClientRect().left : Number(launcherSettings.x);
|
|
812
|
+
const initialY = launcherSettings.y === null ? button.getBoundingClientRect().top : Number(launcherSettings.y);
|
|
813
|
+
let dragged = false;
|
|
814
|
+
const onMove = (moveEvent) => {
|
|
815
|
+
const deltaX = moveEvent.clientX - startX;
|
|
816
|
+
const deltaY = moveEvent.clientY - startY;
|
|
817
|
+
if (Math.abs(deltaX) + Math.abs(deltaY) > 4) {
|
|
818
|
+
dragged = true;
|
|
819
|
+
}
|
|
820
|
+
if (!dragged) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
launcherSettings.x = initialX + deltaX;
|
|
824
|
+
launcherSettings.y = initialY + deltaY;
|
|
825
|
+
applyLauncherSettings();
|
|
826
|
+
};
|
|
827
|
+
const onUp = () => {
|
|
828
|
+
window.removeEventListener("pointermove", onMove);
|
|
829
|
+
window.removeEventListener("pointerup", onUp);
|
|
830
|
+
if (dragged) {
|
|
831
|
+
button.__anlyxSuppressClick = true;
|
|
832
|
+
persistLauncherSettings();
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
window.addEventListener("pointermove", onMove);
|
|
836
|
+
window.addEventListener("pointerup", onUp, { once: true });
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function restoreDrawerSettings() {
|
|
841
|
+
try {
|
|
842
|
+
const raw = window.localStorage && window.localStorage.getItem(ANLYX_DRAWER_SETTINGS_KEY);
|
|
843
|
+
return raw ? JSON.parse(raw) : {};
|
|
844
|
+
} catch {
|
|
845
|
+
return {};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function restoreLauncherSettings() {
|
|
850
|
+
try {
|
|
851
|
+
const raw = window.localStorage && window.localStorage.getItem(ANLYX_LAUNCHER_SETTINGS_KEY);
|
|
852
|
+
return raw ? JSON.parse(raw) : {};
|
|
853
|
+
} catch {
|
|
854
|
+
return {};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function persistDrawerSettings() {
|
|
859
|
+
try {
|
|
860
|
+
if (window.localStorage) {
|
|
861
|
+
window.localStorage.setItem(ANLYX_DRAWER_SETTINGS_KEY, JSON.stringify(drawerSettings));
|
|
862
|
+
}
|
|
863
|
+
} catch {
|
|
864
|
+
// Ignore storage failures in strict browser privacy modes.
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function persistLauncherSettings() {
|
|
869
|
+
try {
|
|
870
|
+
if (window.localStorage) {
|
|
871
|
+
window.localStorage.setItem(ANLYX_LAUNCHER_SETTINGS_KEY, JSON.stringify({
|
|
872
|
+
x: launcherSettings.x,
|
|
873
|
+
y: launcherSettings.y
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
} catch {
|
|
877
|
+
// Ignore storage failures in strict browser privacy modes.
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function clamp(value, min, max) {
|
|
882
|
+
return Math.min(Math.max(value, min), max);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function installOverlayRootGuard() {
|
|
886
|
+
if (overlayRootGuardInstalled || !document.body || !window.MutationObserver) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
overlayRootGuardInstalled = true;
|
|
890
|
+
const observer = new MutationObserver(() => {
|
|
891
|
+
if (!document.body || document.getElementById("anlyx-overlay-root") || overlayRootRestoreScheduled) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
overlayRootRestoreScheduled = true;
|
|
895
|
+
window.setTimeout(() => {
|
|
896
|
+
overlayRootRestoreScheduled = false;
|
|
897
|
+
mountOverlayUi();
|
|
898
|
+
}, 50);
|
|
899
|
+
});
|
|
900
|
+
observer.observe(document.body, { childList: true });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function loadOverlayUiAssets() {
|
|
904
|
+
if (!document.querySelector("link[data-anlyx-overlay-ui]")) {
|
|
905
|
+
const link = document.createElement("link");
|
|
906
|
+
link.rel = "stylesheet";
|
|
907
|
+
link.href = runtimeBaseUrl + "/_anlyx/overlay-ui.css";
|
|
908
|
+
link.setAttribute("data-anlyx-overlay-ui", "true");
|
|
909
|
+
document.head.appendChild(link);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (window.__ANLYX_RENDER_FLOW_DRAWER__) {
|
|
913
|
+
overlayUiReady = true;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (overlayUiLoading) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
overlayUiLoading = true;
|
|
922
|
+
const script = document.createElement("script");
|
|
923
|
+
script.src = runtimeBaseUrl + "/_anlyx/overlay-ui.js";
|
|
924
|
+
script.defer = true;
|
|
925
|
+
script.onload = () => {
|
|
926
|
+
overlayUiReady = true;
|
|
927
|
+
render();
|
|
928
|
+
};
|
|
929
|
+
script.onerror = () => {
|
|
930
|
+
overlayUiReady = false;
|
|
931
|
+
render();
|
|
932
|
+
};
|
|
933
|
+
document.head.appendChild(script);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function loadReport() {
|
|
937
|
+
try {
|
|
938
|
+
const response = await fetch(runtimeBaseUrl + "/_anlyx/report-data");
|
|
939
|
+
if (!response.ok) {
|
|
940
|
+
throw new Error("Report data request failed with status " + response.status);
|
|
941
|
+
}
|
|
942
|
+
state.report = await response.json();
|
|
943
|
+
render();
|
|
944
|
+
} catch (error) {
|
|
945
|
+
state.loadError = error instanceof Error ? error.message : "Failed to load report data";
|
|
946
|
+
render();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function installFetchInterceptor() {
|
|
951
|
+
const originalFetch = window.fetch;
|
|
952
|
+
window.fetch = async function anlyxFetch(input, init) {
|
|
953
|
+
const method = ((init && init.method) || (input && input.method) || "GET").toUpperCase();
|
|
954
|
+
const url = typeof input === "string" ? input : input && input.url;
|
|
955
|
+
const startedAt = performance.now();
|
|
956
|
+
try {
|
|
957
|
+
const response = await originalFetch.apply(this, arguments);
|
|
958
|
+
if (shouldTrackRequestUrl(url)) {
|
|
959
|
+
scheduleApiEventRecord({ method, url, status: response.status, durationMs: performance.now() - startedAt, startedAt });
|
|
960
|
+
}
|
|
961
|
+
return response;
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (shouldTrackRequestUrl(url)) {
|
|
964
|
+
scheduleApiEventRecord({ method, url, status: "failed", durationMs: performance.now() - startedAt, startedAt });
|
|
965
|
+
}
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function installUserActionTracker(root) {
|
|
972
|
+
document.addEventListener("pointerdown", (event) => captureUserAction(event, root), true);
|
|
973
|
+
document.addEventListener("click", (event) => captureUserAction(event, root), true);
|
|
974
|
+
document.addEventListener("submit", (event) => captureUserAction(event, root), true);
|
|
975
|
+
document.addEventListener("keydown", (event) => {
|
|
976
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
977
|
+
captureUserAction(event, root);
|
|
978
|
+
}
|
|
979
|
+
}, true);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function captureUserAction(event, root) {
|
|
983
|
+
const target = getActionTarget(event.target);
|
|
984
|
+
if (!target || root.contains(target)) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const action = {
|
|
989
|
+
id: String(Date.now()) + "-" + Math.random().toString(36).slice(2),
|
|
990
|
+
type: getActionType(event, target),
|
|
991
|
+
label: getActionLabel(target),
|
|
992
|
+
selector: getElementPath(target),
|
|
993
|
+
at: performance.now(),
|
|
994
|
+
capturedAt: Date.now()
|
|
995
|
+
};
|
|
996
|
+
rememberAction(action);
|
|
997
|
+
persistPendingAction(action);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function rememberAction(action) {
|
|
1001
|
+
state.actions = [action].concat(state.actions).slice(0, 20);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function persistPendingAction(action) {
|
|
1005
|
+
try {
|
|
1006
|
+
window.sessionStorage.setItem(ANLYX_PENDING_ACTION_KEY, JSON.stringify(action));
|
|
1007
|
+
} catch {
|
|
1008
|
+
// Ignore storage failures in strict browser privacy modes.
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function restorePendingAction() {
|
|
1013
|
+
try {
|
|
1014
|
+
const raw = window.sessionStorage.getItem(ANLYX_PENDING_ACTION_KEY);
|
|
1015
|
+
if (!raw) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const action = JSON.parse(raw);
|
|
1019
|
+
if (isFreshAction(action)) {
|
|
1020
|
+
rememberAction(Object.assign({}, action, { at: performance.now() }));
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
window.sessionStorage.removeItem(ANLYX_PENDING_ACTION_KEY);
|
|
1024
|
+
} catch {
|
|
1025
|
+
try {
|
|
1026
|
+
window.sessionStorage.removeItem(ANLYX_PENDING_ACTION_KEY);
|
|
1027
|
+
} catch {
|
|
1028
|
+
// Ignore storage cleanup failures.
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function isFreshAction(action) {
|
|
1034
|
+
return action && action.capturedAt && Date.now() - action.capturedAt <= 8000;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getActionTarget(target) {
|
|
1038
|
+
if (!target || !target.closest) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
return target.closest("[data-anlyx-label], button, a, input, select, textarea, label, summary, [role='button'], [role='link'], [role='menuitem'], [role='tab']");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function getActionType(event, target) {
|
|
1045
|
+
if (event.type === "submit") {
|
|
1046
|
+
return "Submitted";
|
|
1047
|
+
}
|
|
1048
|
+
if (event.type === "keydown") {
|
|
1049
|
+
return "Pressed";
|
|
1050
|
+
}
|
|
1051
|
+
if (target.tagName === "A" || target.getAttribute("role") === "link") {
|
|
1052
|
+
return "Opened";
|
|
1053
|
+
}
|
|
1054
|
+
return "Clicked";
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function getActionLabel(target) {
|
|
1058
|
+
const label =
|
|
1059
|
+
target.getAttribute("data-anlyx-label") ||
|
|
1060
|
+
target.getAttribute("aria-label") ||
|
|
1061
|
+
target.getAttribute("title") ||
|
|
1062
|
+
target.getAttribute("placeholder") ||
|
|
1063
|
+
target.name ||
|
|
1064
|
+
target.textContent ||
|
|
1065
|
+
target.value ||
|
|
1066
|
+
getElementPath(target);
|
|
1067
|
+
return compactLabel(label);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function compactLabel(value) {
|
|
1071
|
+
const label = String(value || "").replace(/\s+/g, " ").trim();
|
|
1072
|
+
if (!label) {
|
|
1073
|
+
return "unnamed element";
|
|
1074
|
+
}
|
|
1075
|
+
return label.length > 80 ? label.slice(0, 77) + "..." : label;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function getElementPath(target) {
|
|
1079
|
+
const tag = String(target.tagName || "element").toLowerCase();
|
|
1080
|
+
if (target.id) {
|
|
1081
|
+
return tag + "#" + target.id;
|
|
1082
|
+
}
|
|
1083
|
+
const testId = target.getAttribute("data-testid");
|
|
1084
|
+
if (testId) {
|
|
1085
|
+
return tag + "[data-testid='" + testId + "']";
|
|
1086
|
+
}
|
|
1087
|
+
const className = String(target.className || "").split(/\s+/).filter(Boolean).slice(0, 2).join(".");
|
|
1088
|
+
return className ? tag + "." + className : tag;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function installXhrInterceptor() {
|
|
1092
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
1093
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
1094
|
+
|
|
1095
|
+
XMLHttpRequest.prototype.open = function anlyxOpen(method, url) {
|
|
1096
|
+
this.__anlyxRequest = { method: String(method || "GET").toUpperCase(), url: String(url || ""), startedAt: 0 };
|
|
1097
|
+
return originalOpen.apply(this, arguments);
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
XMLHttpRequest.prototype.send = function anlyxSend() {
|
|
1101
|
+
const request = this.__anlyxRequest;
|
|
1102
|
+
if (request) {
|
|
1103
|
+
request.startedAt = performance.now();
|
|
1104
|
+
this.addEventListener("loadend", () => {
|
|
1105
|
+
if (shouldTrackRequestUrl(request.url)) {
|
|
1106
|
+
scheduleApiEventRecord({
|
|
1107
|
+
method: request.method,
|
|
1108
|
+
url: request.url,
|
|
1109
|
+
status: this.status || "unknown",
|
|
1110
|
+
durationMs: performance.now() - request.startedAt,
|
|
1111
|
+
startedAt: request.startedAt
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
return originalSend.apply(this, arguments);
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function scheduleApiEventRecord(event) {
|
|
1121
|
+
window.setTimeout(() => recordApiEvent(event), 0);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function recordApiEvent(event) {
|
|
1125
|
+
const normalized = normalizeUrl(event.url);
|
|
1126
|
+
if (!normalized || shouldIgnoreRequest(normalized)) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const matched = matchEndpoint(event.method, normalized.pathname);
|
|
1131
|
+
const passive = isPassiveRequest(event.method, normalized.pathname);
|
|
1132
|
+
const triggeredBy = passive ? null : findActionForRequest(event.startedAt);
|
|
1133
|
+
const item = {
|
|
1134
|
+
id: String(Date.now()) + "-" + Math.random().toString(36).slice(2),
|
|
1135
|
+
method: event.method,
|
|
1136
|
+
path: normalized.pathname,
|
|
1137
|
+
status: event.status,
|
|
1138
|
+
durationMs: Math.round(event.durationMs),
|
|
1139
|
+
count: 1,
|
|
1140
|
+
lastSeenAt: Date.now(),
|
|
1141
|
+
triggeredBy,
|
|
1142
|
+
source: triggeredBy ? "action" : classifyApiEventSource(normalized.pathname),
|
|
1143
|
+
matchedEndpoint: matched.endpoint,
|
|
1144
|
+
matchedFlow: matched.flow,
|
|
1145
|
+
matchedPages: matched.pages
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
const existingIndex = findExistingEventIndex(item);
|
|
1149
|
+
if (existingIndex >= 0) {
|
|
1150
|
+
const existing = state.events[existingIndex];
|
|
1151
|
+
const updated = Object.assign({}, existing, {
|
|
1152
|
+
status: item.status,
|
|
1153
|
+
durationMs: item.durationMs,
|
|
1154
|
+
count: (existing.count || 1) + 1,
|
|
1155
|
+
lastSeenAt: item.lastSeenAt,
|
|
1156
|
+
triggeredBy: item.triggeredBy || existing.triggeredBy,
|
|
1157
|
+
source: item.triggeredBy ? "action" : item.source,
|
|
1158
|
+
matchedEndpoint: item.matchedEndpoint,
|
|
1159
|
+
matchedFlow: item.matchedFlow,
|
|
1160
|
+
matchedPages: item.matchedPages
|
|
1161
|
+
});
|
|
1162
|
+
state.events = [updated].concat(state.events.filter((_, index) => index !== existingIndex)).slice(0, 12);
|
|
1163
|
+
if (shouldAutoFocusEvent(updated)) {
|
|
1164
|
+
brieflyExpandLauncher();
|
|
1165
|
+
state.selectedEventId = updated.id;
|
|
1166
|
+
state.open = true;
|
|
1167
|
+
}
|
|
1168
|
+
render();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
state.events = [item].concat(state.events).slice(0, 12);
|
|
1173
|
+
if (shouldAutoFocusEvent(item)) {
|
|
1174
|
+
brieflyExpandLauncher();
|
|
1175
|
+
state.selectedEventId = item.id;
|
|
1176
|
+
state.open = true;
|
|
1177
|
+
}
|
|
1178
|
+
render();
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function shouldAutoFocusEvent(item) {
|
|
1182
|
+
return Boolean(item && item.triggeredBy);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function brieflyExpandLauncher() {
|
|
1186
|
+
launcherSettings.expandedUntil = Date.now() + 2600;
|
|
1187
|
+
applyLauncherSettings();
|
|
1188
|
+
window.setTimeout(applyLauncherSettings, 2700);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function classifyApiEventSource(pathname) {
|
|
1192
|
+
if (isHealthOrPollingPath(pathname)) {
|
|
1193
|
+
return "health";
|
|
1194
|
+
}
|
|
1195
|
+
return "background";
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function isPassiveRequest(method, pathname) {
|
|
1199
|
+
const normalizedMethod = String(method || "GET").toUpperCase();
|
|
1200
|
+
const segments = getPathSegments(pathname);
|
|
1201
|
+
if (isHealthOrPollingPath(pathname)) {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
if (segments.some((segment) => {
|
|
1205
|
+
return segment === "page-views" ||
|
|
1206
|
+
segment === "analytics" ||
|
|
1207
|
+
segment === "telemetry" ||
|
|
1208
|
+
segment === "events" ||
|
|
1209
|
+
segment === "metrics";
|
|
1210
|
+
})) {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (isAutomaticSupportPath(normalizedMethod, segments)) {
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function isAutomaticSupportPath(method, segments) {
|
|
1220
|
+
if (method === "GET" && isSessionProbePath(segments)) {
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
if (segments.includes("csrf") || segments.includes("xsrf")) {
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
if (!segments.includes("auth")) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
const last = segments[segments.length - 1] || "";
|
|
1230
|
+
return last === "session" ||
|
|
1231
|
+
last === "refresh" ||
|
|
1232
|
+
last === "token" ||
|
|
1233
|
+
last === "csrf" ||
|
|
1234
|
+
last === "status";
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function isSessionProbePath(segments) {
|
|
1238
|
+
const last = segments[segments.length - 1] || "";
|
|
1239
|
+
if (last === "me" || last === "session" || last === "profile" || last === "current-user") {
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
return segments.includes("saved-benefits") ||
|
|
1243
|
+
segments.includes("saved-items") ||
|
|
1244
|
+
segments.includes("bookmarks") ||
|
|
1245
|
+
segments.includes("favorites");
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function isHealthOrPollingPath(pathname) {
|
|
1249
|
+
const segments = getPathSegments(pathname);
|
|
1250
|
+
return segments.some((segment) => {
|
|
1251
|
+
return segment === "health" ||
|
|
1252
|
+
segment === "healthz" ||
|
|
1253
|
+
segment === "ready" ||
|
|
1254
|
+
segment === "readyz" ||
|
|
1255
|
+
segment === "live" ||
|
|
1256
|
+
segment === "livez" ||
|
|
1257
|
+
segment === "ping" ||
|
|
1258
|
+
segment === "metrics" ||
|
|
1259
|
+
segment === "poll" ||
|
|
1260
|
+
segment === "polling";
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function getPathSegments(pathname) {
|
|
1265
|
+
return String(pathname || "").toLowerCase().split("/").filter(Boolean);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function findExistingEventIndex(item) {
|
|
1269
|
+
return state.events.findIndex((event) => {
|
|
1270
|
+
return event.method === item.method &&
|
|
1271
|
+
event.path === item.path &&
|
|
1272
|
+
String(event.status) === String(item.status) &&
|
|
1273
|
+
getEndpointId(event.matchedEndpoint) === getEndpointId(item.matchedEndpoint);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function getEndpointId(endpoint) {
|
|
1278
|
+
return endpoint && endpoint.id ? endpoint.id : "";
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function findActionForRequest(startedAt) {
|
|
1282
|
+
const requestStartedAt = Number(startedAt || performance.now());
|
|
1283
|
+
return state.actions.find((action) => {
|
|
1284
|
+
const age = requestStartedAt - action.at;
|
|
1285
|
+
if (age >= -50 && age <= 3000) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
return Date.now() - action.capturedAt <= 8000;
|
|
1289
|
+
}) || null;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function normalizeUrl(value) {
|
|
1293
|
+
if (!value) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
return new URL(String(value), window.location.href);
|
|
1298
|
+
} catch {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function shouldIgnoreRequest(url) {
|
|
1304
|
+
if (
|
|
1305
|
+
url.pathname.startsWith("/_anlyx/") ||
|
|
1306
|
+
url.pathname.startsWith("/@vite/") ||
|
|
1307
|
+
url.pathname.startsWith("/_next/") ||
|
|
1308
|
+
url.pathname.startsWith("/getconfig/") ||
|
|
1309
|
+
url.pathname.includes("hot-update") ||
|
|
1310
|
+
url.pathname === "/favicon.ico"
|
|
1311
|
+
) {
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
return /\.(css|js|map|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf)$/i.test(url.pathname);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function shouldTrackRequestUrl(value) {
|
|
1318
|
+
const normalized = normalizeUrl(value);
|
|
1319
|
+
return Boolean(normalized && !shouldIgnoreRequest(normalized));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function matchEndpoint(method, path) {
|
|
1323
|
+
const report = state.report || {};
|
|
1324
|
+
const endpoints = Array.isArray(report.endpoints) ? report.endpoints : [];
|
|
1325
|
+
const endpoint = endpoints.find((candidate) => {
|
|
1326
|
+
return String(candidate.method || "").toUpperCase() === method && endpointPathToRegex(candidate.path).test(path);
|
|
1327
|
+
});
|
|
1328
|
+
const flows = Array.isArray(report.flows) ? report.flows : [];
|
|
1329
|
+
const pages = Array.isArray(report.pages) ? report.pages : [];
|
|
1330
|
+
return {
|
|
1331
|
+
endpoint,
|
|
1332
|
+
flow: endpoint ? flows.find((flow) => flow.endpointId === endpoint.id) : null,
|
|
1333
|
+
pages: endpoint
|
|
1334
|
+
? pages.filter((page) => Array.isArray(page.apiCalls) && page.apiCalls.some((call) => call.endpointId === endpoint.id))
|
|
1335
|
+
: []
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function endpointPathToRegex(path) {
|
|
1340
|
+
const key = String(path || "");
|
|
1341
|
+
const cached = endpointRegexCache.get(key);
|
|
1342
|
+
if (cached) {
|
|
1343
|
+
return cached;
|
|
1344
|
+
}
|
|
1345
|
+
const escaped = String(path || "")
|
|
1346
|
+
.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")
|
|
1347
|
+
.replace(/\\\{[^/]+\\\}/g, "[^/]+");
|
|
1348
|
+
const regex = new RegExp("^" + escaped + "$");
|
|
1349
|
+
endpointRegexCache.set(key, regex);
|
|
1350
|
+
return regex;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function render() {
|
|
1354
|
+
if (!drawer || !body) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
drawer.dataset.open = state.open ? "true" : "false";
|
|
1359
|
+
if (!state.open) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const selected = state.events.find((event) => event.id === state.selectedEventId) || null;
|
|
1364
|
+
renderReactDrawer(selected, getLatestAction());
|
|
1365
|
+
|
|
1366
|
+
installEventSelectionHandler();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function installEventSelectionHandler() {
|
|
1370
|
+
if (!body || body.dataset.eventSelectionBound === "true") {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
body.dataset.eventSelectionBound = "true";
|
|
1374
|
+
const selectEventFromTarget = (target) => {
|
|
1375
|
+
const row = target && target.closest ? target.closest("[data-event-id]") : null;
|
|
1376
|
+
if (!row) {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
state.selectedEventId = row.getAttribute("data-event-id");
|
|
1380
|
+
state.open = true;
|
|
1381
|
+
render();
|
|
1382
|
+
return true;
|
|
1383
|
+
};
|
|
1384
|
+
body.addEventListener("click", (event) => {
|
|
1385
|
+
selectEventFromTarget(event.target);
|
|
1386
|
+
});
|
|
1387
|
+
body.addEventListener("keydown", (event) => {
|
|
1388
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (selectEventFromTarget(event.target)) {
|
|
1392
|
+
event.preventDefault();
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function getLatestAction() {
|
|
1398
|
+
return state.actions.find((action) => isFreshAction(action)) || null;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function getScannedHints() {
|
|
1402
|
+
const report = state.report || {};
|
|
1403
|
+
const pages = Array.isArray(report.pages) ? report.pages : [];
|
|
1404
|
+
const endpoints = Array.isArray(report.endpoints) ? report.endpoints : [];
|
|
1405
|
+
const pathname = window.location && window.location.pathname ? window.location.pathname : "/";
|
|
1406
|
+
const matchingPages = pages.filter((page) => {
|
|
1407
|
+
return page && page.route && routeToRegex(page.route).test(pathname);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
return matchingPages.flatMap((page) => {
|
|
1411
|
+
const apiCalls = Array.isArray(page.apiCalls) ? page.apiCalls : [];
|
|
1412
|
+
return apiCalls.map((apiCall) => {
|
|
1413
|
+
const endpoint = apiCall.endpointId
|
|
1414
|
+
? endpoints.find((candidate) => candidate && candidate.id === apiCall.endpointId)
|
|
1415
|
+
: null;
|
|
1416
|
+
const method = String(apiCall.method || (endpoint && endpoint.method) || "GET").toUpperCase();
|
|
1417
|
+
const path = String(apiCall.path || (endpoint && endpoint.path) || "");
|
|
1418
|
+
return {
|
|
1419
|
+
pageRoute: page.route,
|
|
1420
|
+
pageFilePath: page.filePath,
|
|
1421
|
+
method,
|
|
1422
|
+
path,
|
|
1423
|
+
endpointId: apiCall.endpointId || (endpoint && endpoint.id),
|
|
1424
|
+
endpointLabel: endpoint ? String(endpoint.method || method).toUpperCase() + " " + endpoint.path : method + " " + path,
|
|
1425
|
+
evidence: page.captureStatus === "success" ? "capture" : "scanned-page"
|
|
1426
|
+
};
|
|
1427
|
+
});
|
|
1428
|
+
}).filter((hint) => hint.path).slice(0, 4);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function routeToRegex(route) {
|
|
1432
|
+
const escaped = String(route || "/")
|
|
1433
|
+
.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")
|
|
1434
|
+
.replace(/\\\[\\\.\\\.\\\.[^\]]+\\\]/g, ".+")
|
|
1435
|
+
.replace(/\\\[\\\[\\\.\\\.\\\.[^\]]+\\\]\\\]/g, ".*")
|
|
1436
|
+
.replace(/\\\[[^\]]+\\\]/g, "[^/]+");
|
|
1437
|
+
return new RegExp("^" + escaped + "/?$");
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function renderReactDrawer(selected, latestAction) {
|
|
1441
|
+
loadOverlayUiAssets();
|
|
1442
|
+
|
|
1443
|
+
if (!window.__ANLYX_RENDER_FLOW_DRAWER__) {
|
|
1444
|
+
body.innerHTML = '<section class="anlyx-section"><h3 class="anlyx-section-title">Loading</h3><div class="anlyx-empty">Loading Anlyx Flow Drawer...</div></section>';
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
window.__ANLYX_RENDER_FLOW_DRAWER__(body, {
|
|
1449
|
+
selectedEvent: selected,
|
|
1450
|
+
events: state.events,
|
|
1451
|
+
latestAction,
|
|
1452
|
+
scannedHints: getScannedHints(),
|
|
1453
|
+
loadError: state.loadError,
|
|
1454
|
+
runtimeBaseUrl
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
})();
|
|
1458
|
+
`;
|
|
1459
|
+
}
|
|
112
1460
|
function withDefaultDependencies(dependencies) {
|
|
113
1461
|
return {
|
|
114
1462
|
loadConfig: dependencies?.loadConfig ?? loadConfig,
|
|
115
1463
|
readReportData: dependencies?.readReportData ?? readReportData,
|
|
1464
|
+
runScanCommand: dependencies?.runScanCommand ?? runScanCommand,
|
|
116
1465
|
createLocalUiServer: dependencies?.createLocalUiServer ?? createLocalUiServer,
|
|
1466
|
+
isFrontendReachable: dependencies?.isFrontendReachable ?? isFrontendReachable,
|
|
1467
|
+
startFrontendDevServer: dependencies?.startFrontendDevServer ?? startFrontendDevServer,
|
|
117
1468
|
openBrowser: dependencies?.openBrowser ?? openBrowser
|
|
118
1469
|
};
|
|
119
1470
|
}
|