anlyx 0.1.3 → 0.1.5
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/README.md +53 -5
- package/dist/dev-command.d.ts +13 -5
- package/dist/dev-command.d.ts.map +1 -1
- package/dist/dev-command.js +613 -52
- package/dist/dev-command.js.map +1 -1
- package/dist/flow-commands.d.ts +41 -0
- package/dist/flow-commands.d.ts.map +1 -0
- package/dist/flow-commands.js +180 -0
- package/dist/flow-commands.js.map +1 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +134 -74
- package/dist/index.js.map +1 -1
- package/dist/init-command.d.ts.map +1 -1
- package/dist/init-command.js +6 -32
- package/dist/init-command.js.map +1 -1
- package/package.json +4 -9
- package/dist/scan-command.d.ts +0 -45
- package/dist/scan-command.d.ts.map +0 -1
- package/dist/scan-command.js +0 -153
- package/dist/scan-command.js.map +0 -1
package/dist/dev-command.js
CHANGED
|
@@ -3,27 +3,26 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { scanResultSchema } from "@anlyx/core";
|
|
6
|
+
import { buildFlowRecordFromBrowserEvent, mergeBackendSpansIntoFlowRecord, normalizeProjectInput, parseProjectData, 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";
|
|
10
9
|
const require = createRequire(import.meta.url);
|
|
11
10
|
const activeLocalUiServers = new Set();
|
|
12
11
|
export async function runDevCommand(options = {}) {
|
|
13
12
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
14
13
|
const dependencies = withDefaultDependencies(options.dependencies);
|
|
15
|
-
const config = await dependencies
|
|
14
|
+
const config = await loadConfigOrViewerFallback(dependencies, {
|
|
16
15
|
cwd,
|
|
17
16
|
...(options.configPath ? { configPath: options.configPath } : {})
|
|
18
17
|
});
|
|
19
18
|
const outputDir = resolve(cwd, options.outputDir ?? ".anlyx");
|
|
19
|
+
const projectDataPath = join(cwd, "anlyx.project.json");
|
|
20
|
+
const splitProjectDataPath = join(outputDir, "project", "index.json");
|
|
20
21
|
const reportDataPath = join(outputDir, "report-data.json");
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dependencies
|
|
25
|
-
...(options.configPath ? { configPath: options.configPath } : {}),
|
|
26
|
-
...(options.outputDir ? { outputDir: options.outputDir } : {})
|
|
22
|
+
const loadedData = await ensureProjectData({
|
|
23
|
+
projectDataPath,
|
|
24
|
+
splitProjectDataPath,
|
|
25
|
+
dependencies
|
|
27
26
|
});
|
|
28
27
|
const frontendStarted = await ensureFrontendDevServer({
|
|
29
28
|
cwd,
|
|
@@ -33,7 +32,7 @@ export async function runDevCommand(options = {}) {
|
|
|
33
32
|
const port = options.port ?? getConfiguredPort(config);
|
|
34
33
|
const server = await dependencies.createLocalUiServer({
|
|
35
34
|
port,
|
|
36
|
-
|
|
35
|
+
projectData: loadedData.data,
|
|
37
36
|
viewerRoot: getViewerRoot(),
|
|
38
37
|
frontendBaseUrl: config.frontend.baseUrl,
|
|
39
38
|
mode: config.server.mode
|
|
@@ -47,14 +46,47 @@ export async function runDevCommand(options = {}) {
|
|
|
47
46
|
return {
|
|
48
47
|
url: server.url,
|
|
49
48
|
port,
|
|
49
|
+
projectDataPath: loadedData.path,
|
|
50
50
|
reportDataPath,
|
|
51
51
|
mode: config.server.mode,
|
|
52
52
|
frontendStarted,
|
|
53
|
-
scanRan,
|
|
54
53
|
...(config.server.mode === "inject" ? { frontendUrl: config.frontend.baseUrl } : {}),
|
|
55
54
|
...(config.server.mode === "inject" ? { scriptTag: getOverlayScriptTag(server.url) } : {})
|
|
56
55
|
};
|
|
57
56
|
}
|
|
57
|
+
async function loadConfigOrViewerFallback(dependencies, options) {
|
|
58
|
+
try {
|
|
59
|
+
return await dependencies.loadConfig(options);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!isMissingConfigError(error)) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return createViewerFallbackConfig();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function createViewerFallbackConfig() {
|
|
69
|
+
return {
|
|
70
|
+
projectName: "Anlyx Project JSON",
|
|
71
|
+
backend: {
|
|
72
|
+
type: "openapi",
|
|
73
|
+
openApiUrl: "about:blank"
|
|
74
|
+
},
|
|
75
|
+
frontend: {
|
|
76
|
+
type: "manual",
|
|
77
|
+
baseUrl: "http://localhost:4777",
|
|
78
|
+
urls: [],
|
|
79
|
+
viewport: { width: 1440, height: 900 },
|
|
80
|
+
capture: { mode: "segments", segmentHeight: 900 }
|
|
81
|
+
},
|
|
82
|
+
server: {
|
|
83
|
+
port: 4777,
|
|
84
|
+
openBrowser: true,
|
|
85
|
+
mode: "viewer"
|
|
86
|
+
},
|
|
87
|
+
dev: {}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
58
90
|
export async function closeActiveLocalUiServers() {
|
|
59
91
|
const servers = Array.from(activeLocalUiServers);
|
|
60
92
|
activeLocalUiServers.clear();
|
|
@@ -62,33 +94,21 @@ export async function closeActiveLocalUiServers() {
|
|
|
62
94
|
await server.close?.();
|
|
63
95
|
}));
|
|
64
96
|
}
|
|
65
|
-
async function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
async function ensureProjectData(options) {
|
|
98
|
+
for (const path of [options.projectDataPath, options.splitProjectDataPath]) {
|
|
99
|
+
try {
|
|
100
|
+
return {
|
|
101
|
+
data: await options.dependencies.readProjectData(path),
|
|
102
|
+
path
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!isMissingProjectDataError(error)) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
|
-
|
|
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
|
-
};
|
|
111
|
+
throw new Error('Anlyx project data not found. Create or import "anlyx.project.json" first.');
|
|
92
112
|
}
|
|
93
113
|
async function ensureFrontendDevServer(options) {
|
|
94
114
|
const command = options.config.dev?.command;
|
|
@@ -104,8 +124,83 @@ async function ensureFrontendDevServer(options) {
|
|
|
104
124
|
});
|
|
105
125
|
return true;
|
|
106
126
|
}
|
|
107
|
-
function
|
|
108
|
-
return error instanceof Error && /
|
|
127
|
+
function isMissingProjectDataError(error) {
|
|
128
|
+
return error instanceof Error && /Anlyx project data not found/.test(error.message);
|
|
129
|
+
}
|
|
130
|
+
function isMissingConfigError(error) {
|
|
131
|
+
return error instanceof Error && /Anlyx config file not found/.test(error.message);
|
|
132
|
+
}
|
|
133
|
+
export async function readProjectData(path) {
|
|
134
|
+
let content;
|
|
135
|
+
try {
|
|
136
|
+
content = await readFile(path, "utf8");
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
140
|
+
throw new Error(`Anlyx project data not found: ${path}. Create anlyx.project.json first.`);
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(content);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
throw new Error(`Failed to parse Anlyx project data JSON at ${path}: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
if (path.endsWith("/index.json")) {
|
|
153
|
+
return await readSplitProjectData(path, parsed);
|
|
154
|
+
}
|
|
155
|
+
return normalizeProjectInput(parseProjectData(parsed));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
throw new Error(`Invalid Anlyx project data at ${path}: ${error instanceof Error ? error.message : "invalid project data"}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function readSplitProjectData(indexPath, index) {
|
|
162
|
+
const projectDir = dirname(indexPath);
|
|
163
|
+
const [areas, pages, features, requests, flows, architecture, evidence, measurements, dictionary] = await Promise.all([
|
|
164
|
+
readOptionalProjectJson(join(projectDir, "areas.json")),
|
|
165
|
+
readOptionalProjectJson(join(projectDir, "pages.json")),
|
|
166
|
+
readOptionalProjectJson(join(projectDir, "features.json")),
|
|
167
|
+
readOptionalProjectJson(join(projectDir, "requests.json")),
|
|
168
|
+
readOptionalProjectJson(join(projectDir, "flows.json")),
|
|
169
|
+
readOptionalProjectJson(join(projectDir, "architecture.json")),
|
|
170
|
+
readOptionalProjectJson(join(projectDir, "evidence.json")),
|
|
171
|
+
readOptionalProjectJson(join(projectDir, "measurements.json")),
|
|
172
|
+
readOptionalProjectJson(join(projectDir, "dictionary.json"))
|
|
173
|
+
]);
|
|
174
|
+
return normalizeProjectInput({
|
|
175
|
+
index,
|
|
176
|
+
...(areas === undefined ? {} : { areas }),
|
|
177
|
+
...(pages === undefined ? {} : { pages }),
|
|
178
|
+
...(features === undefined ? {} : { features }),
|
|
179
|
+
...(requests === undefined ? {} : { requests }),
|
|
180
|
+
...(flows === undefined ? {} : { flows }),
|
|
181
|
+
...(architecture === undefined ? {} : { architecture }),
|
|
182
|
+
...(evidence === undefined ? {} : { evidence }),
|
|
183
|
+
...(measurements === undefined ? {} : { measurements }),
|
|
184
|
+
...(dictionary === undefined ? {} : { dictionary })
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async function readOptionalProjectJson(path) {
|
|
188
|
+
let content;
|
|
189
|
+
try {
|
|
190
|
+
content = await readFile(path, "utf8");
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(content);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw new Error(`Failed to parse split Anlyx project JSON at ${path}: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
203
|
+
}
|
|
109
204
|
}
|
|
110
205
|
export async function readReportData(path) {
|
|
111
206
|
let content;
|
|
@@ -114,7 +209,7 @@ export async function readReportData(path) {
|
|
|
114
209
|
}
|
|
115
210
|
catch (error) {
|
|
116
211
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
117
|
-
throw new Error(`Anlyx report data not found: ${path}.
|
|
212
|
+
throw new Error(`Anlyx report data not found: ${path}. Create or import "anlyx.project.json" first.`);
|
|
118
213
|
}
|
|
119
214
|
throw error;
|
|
120
215
|
}
|
|
@@ -194,11 +289,12 @@ export function startFrontendDevServer(options) {
|
|
|
194
289
|
};
|
|
195
290
|
}
|
|
196
291
|
function createAnlyxDevPlugin(options) {
|
|
292
|
+
const liveEvents = options.reportData ? createLiveEventRuntime(options.reportData) : undefined;
|
|
197
293
|
return {
|
|
198
294
|
name: "anlyx-dev-runtime",
|
|
199
295
|
configureServer(server) {
|
|
200
296
|
server.middlewares.use((request, response, next) => {
|
|
201
|
-
if (request.method
|
|
297
|
+
if (isReadRequest(request.method) && options.mode === "viewer" && request.url === "/") {
|
|
202
298
|
request.url = "/viewer.html";
|
|
203
299
|
}
|
|
204
300
|
next();
|
|
@@ -211,10 +307,54 @@ function createAnlyxDevPlugin(options) {
|
|
|
211
307
|
response.end();
|
|
212
308
|
return;
|
|
213
309
|
}
|
|
310
|
+
if (request.method === "GET" && isProjectDataPath(requestUrl)) {
|
|
311
|
+
if (!options.projectData) {
|
|
312
|
+
sendJsonError(response, 404, new Error("Anlyx project data not loaded."));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
sendJson(response, options.projectData);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
214
318
|
if (request.method === "GET" && isReportDataPath(requestUrl)) {
|
|
319
|
+
if (!options.reportData) {
|
|
320
|
+
sendJsonError(response, 404, new Error("Legacy Anlyx report data not loaded."));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
215
323
|
sendJson(response, options.reportData);
|
|
216
324
|
return;
|
|
217
325
|
}
|
|
326
|
+
if (request.method === "GET" && requestUrl === "/_anlyx/events/stream") {
|
|
327
|
+
if (!liveEvents) {
|
|
328
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
liveEvents.openStream(request, response);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (request.method === "POST" && requestUrl === "/_anlyx/events/browser-request") {
|
|
335
|
+
if (!liveEvents) {
|
|
336
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
await liveEvents.recordBrowserRequest(request, response);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (request.method === "POST" && requestUrl === "/_anlyx/events/page-context") {
|
|
343
|
+
if (!liveEvents) {
|
|
344
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await liveEvents.recordPageContext(request, response);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (request.method === "POST" && requestUrl === "/_anlyx/events/backend-spans") {
|
|
351
|
+
if (!liveEvents) {
|
|
352
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
await liveEvents.recordBackendSpans(request, response);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
218
358
|
if (request.method === "GET" && requestUrl === "/_anlyx/overlay.js") {
|
|
219
359
|
response.statusCode = 200;
|
|
220
360
|
setCorsHeaders(response);
|
|
@@ -236,7 +376,7 @@ function createAnlyxDevPlugin(options) {
|
|
|
236
376
|
response.end(getInjectModeHtml(options.frontendBaseUrl, getOverlayScriptTag(getServerUrl(options.port))));
|
|
237
377
|
return;
|
|
238
378
|
}
|
|
239
|
-
if (request.method
|
|
379
|
+
if (isReadRequest(request.method) && isStandaloneViewerPath(requestUrl)) {
|
|
240
380
|
request.url = "/viewer.html";
|
|
241
381
|
next();
|
|
242
382
|
return;
|
|
@@ -256,12 +396,137 @@ function createAnlyxDevPlugin(options) {
|
|
|
256
396
|
}
|
|
257
397
|
};
|
|
258
398
|
}
|
|
399
|
+
export function createLiveEventRuntime(reportData) {
|
|
400
|
+
const streams = new Set();
|
|
401
|
+
const recordsByRequestId = new Map();
|
|
402
|
+
const pendingSpansByRequestId = new Map();
|
|
403
|
+
let latestPageContext;
|
|
404
|
+
let recentRecords = [];
|
|
405
|
+
const rememberRecord = (record) => {
|
|
406
|
+
recordsByRequestId.set(record.requestId, record);
|
|
407
|
+
recentRecords = [record, ...recentRecords.filter((item) => item.id !== record.id)].slice(0, 90);
|
|
408
|
+
};
|
|
409
|
+
const broadcastRecord = (record) => {
|
|
410
|
+
rememberRecord(record);
|
|
411
|
+
for (const stream of streams) {
|
|
412
|
+
writeFlowSseEvent(stream, record);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const broadcastPageContext = (event) => {
|
|
416
|
+
latestPageContext = event;
|
|
417
|
+
for (const stream of streams) {
|
|
418
|
+
writePageContextSseEvent(stream, event);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
openStream(request, response) {
|
|
423
|
+
response.statusCode = 200;
|
|
424
|
+
setCorsHeaders(response);
|
|
425
|
+
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
426
|
+
response.setHeader("cache-control", "no-cache, no-transform");
|
|
427
|
+
response.setHeader("connection", "keep-alive");
|
|
428
|
+
response.write(": Anlyx event stream ready\n\n");
|
|
429
|
+
streams.add(response);
|
|
430
|
+
if (latestPageContext) {
|
|
431
|
+
writePageContextSseEvent(response, latestPageContext);
|
|
432
|
+
}
|
|
433
|
+
for (const record of [...recentRecords].reverse()) {
|
|
434
|
+
writeFlowSseEvent(response, record);
|
|
435
|
+
}
|
|
436
|
+
const heartbeat = setInterval(() => {
|
|
437
|
+
response.write(": heartbeat\n\n");
|
|
438
|
+
}, 30000);
|
|
439
|
+
request.on("close", () => {
|
|
440
|
+
clearInterval(heartbeat);
|
|
441
|
+
streams.delete(response);
|
|
442
|
+
response.end();
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
async recordBrowserRequest(request, response) {
|
|
446
|
+
let payload;
|
|
447
|
+
try {
|
|
448
|
+
payload = await readJsonRequestBody(request);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
sendJsonError(response, 400, error);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (!isBrowserRequestEvent(payload)) {
|
|
455
|
+
sendJsonError(response, 400, new Error("Invalid Anlyx browser request event payload."));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const event = withRequestPageContext(payload, request, latestPageContext);
|
|
459
|
+
if (event.pageUrl) {
|
|
460
|
+
latestPageContext = {
|
|
461
|
+
type: "page_context",
|
|
462
|
+
pageUrl: event.pageUrl,
|
|
463
|
+
contextId: event.contextId ?? pageContextId(event.pageUrl),
|
|
464
|
+
observedAt: event.observedAt
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
let record = buildFlowRecordFromBrowserEvent(event, reportData);
|
|
468
|
+
const pendingSpans = pendingSpansByRequestId.get(event.id);
|
|
469
|
+
if (pendingSpans) {
|
|
470
|
+
pendingSpansByRequestId.delete(event.id);
|
|
471
|
+
record = mergeBackendSpansIntoFlowRecord(record, pendingSpans);
|
|
472
|
+
}
|
|
473
|
+
broadcastRecord(record);
|
|
474
|
+
sendAccepted(response, { accepted: true, id: record.id });
|
|
475
|
+
},
|
|
476
|
+
async recordPageContext(request, response) {
|
|
477
|
+
let payload;
|
|
478
|
+
try {
|
|
479
|
+
payload = await readJsonRequestBody(request);
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
sendJsonError(response, 400, error);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!isPageContextEvent(payload)) {
|
|
486
|
+
sendJsonError(response, 400, new Error("Invalid Anlyx page context event payload."));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
broadcastPageContext(payload);
|
|
490
|
+
sendAccepted(response, { accepted: true, contextId: payload.contextId });
|
|
491
|
+
},
|
|
492
|
+
async recordBackendSpans(request, response) {
|
|
493
|
+
let payload;
|
|
494
|
+
try {
|
|
495
|
+
payload = await readJsonRequestBody(request);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
sendJsonError(response, 400, error);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!isBackendSpanEvent(payload)) {
|
|
502
|
+
sendJsonError(response, 400, new Error("Invalid Anlyx backend span event payload."));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const existingRecord = recordsByRequestId.get(payload.requestId);
|
|
506
|
+
if (!existingRecord) {
|
|
507
|
+
const existingSpans = pendingSpansByRequestId.get(payload.requestId) ?? [];
|
|
508
|
+
pendingSpansByRequestId.set(payload.requestId, [...existingSpans, ...payload.spans]);
|
|
509
|
+
sendAccepted(response, { accepted: true, pending: true, requestId: payload.requestId });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const record = mergeBackendSpansIntoFlowRecord(existingRecord, payload.spans);
|
|
513
|
+
broadcastRecord(record);
|
|
514
|
+
sendAccepted(response, { accepted: true, id: record.id });
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
259
518
|
function isReportDataPath(path) {
|
|
260
519
|
return path === "/_anlyx/report-data" || path === "/api/report-data";
|
|
261
520
|
}
|
|
521
|
+
function isProjectDataPath(path) {
|
|
522
|
+
return path === "/_anlyx/project-data" || path === "/api/project-data";
|
|
523
|
+
}
|
|
262
524
|
function isStandaloneViewerPath(path) {
|
|
263
525
|
return path === "/_anlyx/viewer" || path === "/_anlyx/viewer.html";
|
|
264
526
|
}
|
|
527
|
+
function isReadRequest(method) {
|
|
528
|
+
return method === "GET" || method === "HEAD";
|
|
529
|
+
}
|
|
265
530
|
function isAnlyxPath(path) {
|
|
266
531
|
return path.startsWith("/_anlyx/");
|
|
267
532
|
}
|
|
@@ -271,6 +536,130 @@ function sendJson(response, value) {
|
|
|
271
536
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
272
537
|
response.end(JSON.stringify(value));
|
|
273
538
|
}
|
|
539
|
+
function sendAccepted(response, value) {
|
|
540
|
+
response.statusCode = 202;
|
|
541
|
+
setCorsHeaders(response);
|
|
542
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
543
|
+
response.end(JSON.stringify(value));
|
|
544
|
+
}
|
|
545
|
+
function sendJsonError(response, statusCode, error) {
|
|
546
|
+
response.statusCode = statusCode;
|
|
547
|
+
setCorsHeaders(response);
|
|
548
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
549
|
+
response.end(JSON.stringify({
|
|
550
|
+
error: error instanceof Error ? error.message : "Invalid Anlyx runtime event."
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
async function readJsonRequestBody(request) {
|
|
554
|
+
const body = await readRequestBody(request);
|
|
555
|
+
if (!body || body.byteLength === 0) {
|
|
556
|
+
throw new Error("Missing JSON request body.");
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
return JSON.parse(body.toString("utf8"));
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
throw new Error(`Failed to parse Anlyx runtime event JSON: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function writeFlowSseEvent(response, record) {
|
|
566
|
+
response.write("event: flow\n");
|
|
567
|
+
response.write(`data: ${JSON.stringify(record)}\n\n`);
|
|
568
|
+
}
|
|
569
|
+
function writePageContextSseEvent(response, event) {
|
|
570
|
+
response.write("event: page-context\n");
|
|
571
|
+
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
572
|
+
}
|
|
573
|
+
function withRequestPageContext(event, request, latestPageContext) {
|
|
574
|
+
const pageUrl = event.pageUrl ?? pageUrlFromReferer(request.headers.referer) ?? latestPageContext?.pageUrl;
|
|
575
|
+
if (!pageUrl) {
|
|
576
|
+
return event;
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
...event,
|
|
580
|
+
pageUrl,
|
|
581
|
+
contextId: event.contextId ?? latestPageContext?.contextId ?? pageContextId(pageUrl)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function pageUrlFromReferer(referer) {
|
|
585
|
+
const value = Array.isArray(referer) ? referer[0] : referer;
|
|
586
|
+
if (!value) {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const parsed = new URL(value);
|
|
591
|
+
if (parsed.pathname.startsWith("/_anlyx/")) {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
return `${parsed.origin}${parsed.pathname}${parsed.search}`;
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function pageContextId(pageUrl) {
|
|
601
|
+
try {
|
|
602
|
+
const parsed = new URL(pageUrl);
|
|
603
|
+
return `page:${parsed.pathname}${parsed.search}`;
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return `page:${pageUrl || "/"}`;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function isPageContextEvent(value) {
|
|
610
|
+
if (!value || typeof value !== "object") {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
const event = value;
|
|
614
|
+
return (event.type === "page_context" &&
|
|
615
|
+
typeof event.pageUrl === "string" &&
|
|
616
|
+
typeof event.contextId === "string" &&
|
|
617
|
+
typeof event.observedAt === "string");
|
|
618
|
+
}
|
|
619
|
+
function isBrowserRequestEvent(value) {
|
|
620
|
+
if (!value || typeof value !== "object") {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
const event = value;
|
|
624
|
+
return (event.type === "request" &&
|
|
625
|
+
typeof event.id === "string" &&
|
|
626
|
+
typeof event.method === "string" &&
|
|
627
|
+
typeof event.url === "string" &&
|
|
628
|
+
typeof event.observedAt === "string" &&
|
|
629
|
+
(event.path === undefined || typeof event.path === "string") &&
|
|
630
|
+
(event.pageUrl === undefined || typeof event.pageUrl === "string") &&
|
|
631
|
+
(event.status === undefined || typeof event.status === "number") &&
|
|
632
|
+
(event.durationMs === undefined || typeof event.durationMs === "number") &&
|
|
633
|
+
(event.contextId === undefined || typeof event.contextId === "string") &&
|
|
634
|
+
(event.priority === undefined ||
|
|
635
|
+
event.priority === "primary" ||
|
|
636
|
+
event.priority === "background") &&
|
|
637
|
+
(event.runtimeSource === undefined ||
|
|
638
|
+
event.runtimeSource === "browser" ||
|
|
639
|
+
event.runtimeSource === "server" ||
|
|
640
|
+
event.runtimeSource === "unknown"));
|
|
641
|
+
}
|
|
642
|
+
function isBackendSpanEvent(value) {
|
|
643
|
+
if (!value || typeof value !== "object") {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
const event = value;
|
|
647
|
+
return (event.type === "backend_spans" &&
|
|
648
|
+
typeof event.requestId === "string" &&
|
|
649
|
+
Array.isArray(event.spans) &&
|
|
650
|
+
event.spans.every(isBackendObservedSpan));
|
|
651
|
+
}
|
|
652
|
+
function isBackendObservedSpan(value) {
|
|
653
|
+
if (!value || typeof value !== "object") {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
const span = value;
|
|
657
|
+
return (typeof span.id === "string" &&
|
|
658
|
+
typeof span.type === "string" &&
|
|
659
|
+
typeof span.label === "string" &&
|
|
660
|
+
typeof span.startOffsetMs === "number" &&
|
|
661
|
+
typeof span.durationMs === "number");
|
|
662
|
+
}
|
|
274
663
|
async function sendRuntimeAsset(response, path, contentType) {
|
|
275
664
|
try {
|
|
276
665
|
const content = await readFile(path);
|
|
@@ -288,8 +677,8 @@ async function sendRuntimeAsset(response, path, contentType) {
|
|
|
288
677
|
}
|
|
289
678
|
function setCorsHeaders(response) {
|
|
290
679
|
response.setHeader("access-control-allow-origin", "*");
|
|
291
|
-
response.setHeader("access-control-allow-methods", "GET, OPTIONS");
|
|
292
|
-
response.setHeader("access-control-allow-headers", "content-type");
|
|
680
|
+
response.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
681
|
+
response.setHeader("access-control-allow-headers", "content-type, x-anlyx-request-id");
|
|
293
682
|
}
|
|
294
683
|
function getServerUrl(port) {
|
|
295
684
|
return `http://localhost:${port}`;
|
|
@@ -660,6 +1049,7 @@ export function getOverlayClientScript() {
|
|
|
660
1049
|
installUserActionTracker(root);
|
|
661
1050
|
installFetchInterceptor();
|
|
662
1051
|
installXhrInterceptor();
|
|
1052
|
+
installPageContextReporter();
|
|
663
1053
|
loadReport();
|
|
664
1054
|
}
|
|
665
1055
|
|
|
@@ -952,22 +1342,49 @@ export function getOverlayClientScript() {
|
|
|
952
1342
|
window.fetch = async function anlyxFetch(input, init) {
|
|
953
1343
|
const method = ((init && init.method) || (input && input.method) || "GET").toUpperCase();
|
|
954
1344
|
const url = typeof input === "string" ? input : input && input.url;
|
|
1345
|
+
const shouldTrack = shouldTrackRequestUrl(url);
|
|
1346
|
+
const requestId = shouldTrack ? createRequestId() : null;
|
|
1347
|
+
const args = requestId ? withAnlyxFetchRequestId(input, init, requestId) : arguments;
|
|
955
1348
|
const startedAt = performance.now();
|
|
956
1349
|
try {
|
|
957
|
-
const response = await originalFetch.apply(this,
|
|
958
|
-
if (
|
|
959
|
-
scheduleApiEventRecord({ method, url, status: response.status, durationMs: performance.now() - startedAt, startedAt });
|
|
1350
|
+
const response = await originalFetch.apply(this, args);
|
|
1351
|
+
if (shouldTrack) {
|
|
1352
|
+
scheduleApiEventRecord({ id: requestId, method, url, status: response.status, durationMs: performance.now() - startedAt, startedAt });
|
|
960
1353
|
}
|
|
961
1354
|
return response;
|
|
962
1355
|
} catch (error) {
|
|
963
|
-
if (
|
|
964
|
-
scheduleApiEventRecord({ method, url, status: "failed", durationMs: performance.now() - startedAt, startedAt });
|
|
1356
|
+
if (shouldTrack) {
|
|
1357
|
+
scheduleApiEventRecord({ id: requestId, method, url, status: "failed", durationMs: performance.now() - startedAt, startedAt });
|
|
965
1358
|
}
|
|
966
1359
|
throw error;
|
|
967
1360
|
}
|
|
968
1361
|
};
|
|
969
1362
|
}
|
|
970
1363
|
|
|
1364
|
+
function createRequestId() {
|
|
1365
|
+
return "anlyx-" + Date.now() + "-" + Math.random().toString(36).slice(2);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function withAnlyxFetchRequestId(input, init, requestId) {
|
|
1369
|
+
const nextInit = Object.assign({}, init || {});
|
|
1370
|
+
nextInit.headers = appendHeader(nextInit.headers || (input && input.headers), "X-Anlyx-Request-Id", requestId);
|
|
1371
|
+
return [input, nextInit];
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function appendHeader(headers, name, value) {
|
|
1375
|
+
try {
|
|
1376
|
+
if (typeof Headers !== "undefined") {
|
|
1377
|
+
const nextHeaders = new Headers(headers || {});
|
|
1378
|
+
nextHeaders.set(name, value);
|
|
1379
|
+
return nextHeaders;
|
|
1380
|
+
}
|
|
1381
|
+
} catch {
|
|
1382
|
+
// Fall through to a plain object header bag when Headers cannot clone input.
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return Object.assign({}, headers || {}, { [name]: value });
|
|
1386
|
+
}
|
|
1387
|
+
|
|
971
1388
|
function installUserActionTracker(root) {
|
|
972
1389
|
document.addEventListener("pointerdown", (event) => captureUserAction(event, root), true);
|
|
973
1390
|
document.addEventListener("click", (event) => captureUserAction(event, root), true);
|
|
@@ -1093,7 +1510,13 @@ export function getOverlayClientScript() {
|
|
|
1093
1510
|
const originalSend = XMLHttpRequest.prototype.send;
|
|
1094
1511
|
|
|
1095
1512
|
XMLHttpRequest.prototype.open = function anlyxOpen(method, url) {
|
|
1096
|
-
|
|
1513
|
+
const requestUrl = String(url || "");
|
|
1514
|
+
this.__anlyxRequest = {
|
|
1515
|
+
id: shouldTrackRequestUrl(requestUrl) ? createRequestId() : null,
|
|
1516
|
+
method: String(method || "GET").toUpperCase(),
|
|
1517
|
+
url: requestUrl,
|
|
1518
|
+
startedAt: 0
|
|
1519
|
+
};
|
|
1097
1520
|
return originalOpen.apply(this, arguments);
|
|
1098
1521
|
};
|
|
1099
1522
|
|
|
@@ -1101,9 +1524,17 @@ export function getOverlayClientScript() {
|
|
|
1101
1524
|
const request = this.__anlyxRequest;
|
|
1102
1525
|
if (request) {
|
|
1103
1526
|
request.startedAt = performance.now();
|
|
1527
|
+
if (request.id && this.setRequestHeader) {
|
|
1528
|
+
try {
|
|
1529
|
+
this.setRequestHeader("X-Anlyx-Request-Id", request.id);
|
|
1530
|
+
} catch {
|
|
1531
|
+
// Ignore header injection failures; the browser event can still be observed.
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1104
1534
|
this.addEventListener("loadend", () => {
|
|
1105
1535
|
if (shouldTrackRequestUrl(request.url)) {
|
|
1106
1536
|
scheduleApiEventRecord({
|
|
1537
|
+
id: request.id,
|
|
1107
1538
|
method: request.method,
|
|
1108
1539
|
url: request.url,
|
|
1109
1540
|
status: this.status || "unknown",
|
|
@@ -1131,14 +1562,16 @@ export function getOverlayClientScript() {
|
|
|
1131
1562
|
const passive = isPassiveRequest(event.method, normalized.pathname);
|
|
1132
1563
|
const triggeredBy = passive ? null : findActionForRequest(event.startedAt);
|
|
1133
1564
|
const item = {
|
|
1134
|
-
id:
|
|
1565
|
+
id: event.id || createRequestId(),
|
|
1135
1566
|
method: event.method,
|
|
1136
1567
|
path: normalized.pathname,
|
|
1137
1568
|
status: event.status,
|
|
1138
1569
|
durationMs: Math.round(event.durationMs),
|
|
1139
1570
|
count: 1,
|
|
1571
|
+
contextId: getCurrentContextId(),
|
|
1140
1572
|
lastSeenAt: Date.now(),
|
|
1141
1573
|
triggeredBy,
|
|
1574
|
+
priority: passive ? "background" : "primary",
|
|
1142
1575
|
source: triggeredBy ? "action" : classifyApiEventSource(normalized.pathname),
|
|
1143
1576
|
matchedEndpoint: matched.endpoint,
|
|
1144
1577
|
matchedFlow: matched.flow,
|
|
@@ -1152,8 +1585,10 @@ export function getOverlayClientScript() {
|
|
|
1152
1585
|
status: item.status,
|
|
1153
1586
|
durationMs: item.durationMs,
|
|
1154
1587
|
count: (existing.count || 1) + 1,
|
|
1588
|
+
contextId: item.contextId,
|
|
1155
1589
|
lastSeenAt: item.lastSeenAt,
|
|
1156
1590
|
triggeredBy: item.triggeredBy || existing.triggeredBy,
|
|
1591
|
+
priority: item.priority,
|
|
1157
1592
|
source: item.triggeredBy ? "action" : item.source,
|
|
1158
1593
|
matchedEndpoint: item.matchedEndpoint,
|
|
1159
1594
|
matchedFlow: item.matchedFlow,
|
|
@@ -1166,6 +1601,7 @@ export function getOverlayClientScript() {
|
|
|
1166
1601
|
state.open = true;
|
|
1167
1602
|
}
|
|
1168
1603
|
render();
|
|
1604
|
+
sendBrowserRequestEvent(updated);
|
|
1169
1605
|
return;
|
|
1170
1606
|
}
|
|
1171
1607
|
|
|
@@ -1176,12 +1612,124 @@ export function getOverlayClientScript() {
|
|
|
1176
1612
|
state.open = true;
|
|
1177
1613
|
}
|
|
1178
1614
|
render();
|
|
1615
|
+
sendBrowserRequestEvent(item);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function sendBrowserRequestEvent(item) {
|
|
1619
|
+
if (!window.fetch || !item || item.source === "anlyx") {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const payload = {
|
|
1624
|
+
id: item.id,
|
|
1625
|
+
type: "request",
|
|
1626
|
+
method: item.method,
|
|
1627
|
+
url: item.path,
|
|
1628
|
+
path: item.path,
|
|
1629
|
+
pageUrl: getCurrentPageUrl(),
|
|
1630
|
+
status: typeof item.status === "number" ? item.status : undefined,
|
|
1631
|
+
durationMs: typeof item.durationMs === "number" ? item.durationMs : undefined,
|
|
1632
|
+
observedAt: new Date(item.lastSeenAt || Date.now()).toISOString(),
|
|
1633
|
+
contextId: item.contextId,
|
|
1634
|
+
priority: item.priority === "background" ? "background" : "primary",
|
|
1635
|
+
runtimeSource: "browser",
|
|
1636
|
+
action: item.triggeredBy ? {
|
|
1637
|
+
label: item.triggeredBy.label,
|
|
1638
|
+
selector: item.triggeredBy.selector,
|
|
1639
|
+
observedAt: new Date(item.triggeredBy.capturedAt || item.lastSeenAt || Date.now()).toISOString()
|
|
1640
|
+
} : undefined
|
|
1641
|
+
};
|
|
1642
|
+
|
|
1643
|
+
try {
|
|
1644
|
+
window.fetch(runtimeBaseUrl + "/_anlyx/events/browser-request", {
|
|
1645
|
+
method: "POST",
|
|
1646
|
+
headers: { "content-type": "application/json" },
|
|
1647
|
+
body: JSON.stringify(payload),
|
|
1648
|
+
keepalive: true
|
|
1649
|
+
}).catch(() => undefined);
|
|
1650
|
+
} catch {
|
|
1651
|
+
// Runtime ingestion is best-effort and must never break the user's app.
|
|
1652
|
+
}
|
|
1179
1653
|
}
|
|
1180
1654
|
|
|
1181
1655
|
function shouldAutoFocusEvent(item) {
|
|
1182
1656
|
return Boolean(item && item.triggeredBy);
|
|
1183
1657
|
}
|
|
1184
1658
|
|
|
1659
|
+
function getCurrentContextId() {
|
|
1660
|
+
const path = window.location && window.location.pathname ? window.location.pathname : "/";
|
|
1661
|
+
const search = window.location && window.location.search ? window.location.search : "";
|
|
1662
|
+
return "page:" + path + search;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function getCurrentPageUrl() {
|
|
1666
|
+
if (!window.location) {
|
|
1667
|
+
return "/";
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return window.location.href.split("#")[0] || "/";
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function installPageContextReporter() {
|
|
1674
|
+
let lastPageUrl = "";
|
|
1675
|
+
let timer = null;
|
|
1676
|
+
|
|
1677
|
+
const schedulePageContext = () => {
|
|
1678
|
+
if (timer !== null) {
|
|
1679
|
+
window.clearTimeout(timer);
|
|
1680
|
+
}
|
|
1681
|
+
timer = window.setTimeout(() => {
|
|
1682
|
+
timer = null;
|
|
1683
|
+
sendPageContextEvent();
|
|
1684
|
+
}, 0);
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
const wrapHistoryMethod = (methodName) => {
|
|
1688
|
+
const original = window.history && window.history[methodName];
|
|
1689
|
+
if (!original || original.__anlyxWrapped) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const wrapped = function anlyxHistoryWrapper() {
|
|
1694
|
+
const result = original.apply(this, arguments);
|
|
1695
|
+
schedulePageContext();
|
|
1696
|
+
return result;
|
|
1697
|
+
};
|
|
1698
|
+
wrapped.__anlyxWrapped = true;
|
|
1699
|
+
window.history[methodName] = wrapped;
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
const sendPageContextEvent = () => {
|
|
1703
|
+
const pageUrl = getCurrentPageUrl();
|
|
1704
|
+
if (!pageUrl || pageUrl === lastPageUrl) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
lastPageUrl = pageUrl;
|
|
1708
|
+
|
|
1709
|
+
try {
|
|
1710
|
+
window.fetch(runtimeBaseUrl + "/_anlyx/events/page-context", {
|
|
1711
|
+
method: "POST",
|
|
1712
|
+
headers: { "content-type": "application/json" },
|
|
1713
|
+
body: JSON.stringify({
|
|
1714
|
+
type: "page_context",
|
|
1715
|
+
pageUrl,
|
|
1716
|
+
contextId: getCurrentContextId(),
|
|
1717
|
+
observedAt: new Date().toISOString()
|
|
1718
|
+
}),
|
|
1719
|
+
keepalive: true
|
|
1720
|
+
}).catch(() => undefined);
|
|
1721
|
+
} catch {
|
|
1722
|
+
// Page context reporting is best-effort and must never affect the app.
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
wrapHistoryMethod("pushState");
|
|
1727
|
+
wrapHistoryMethod("replaceState");
|
|
1728
|
+
window.addEventListener("popstate", schedulePageContext);
|
|
1729
|
+
window.addEventListener("hashchange", schedulePageContext);
|
|
1730
|
+
sendPageContextEvent();
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1185
1733
|
function brieflyExpandLauncher() {
|
|
1186
1734
|
launcherSettings.expandedUntil = Date.now() + 2600;
|
|
1187
1735
|
applyLauncherSettings();
|
|
@@ -1316,7 +1864,20 @@ export function getOverlayClientScript() {
|
|
|
1316
1864
|
|
|
1317
1865
|
function shouldTrackRequestUrl(value) {
|
|
1318
1866
|
const normalized = normalizeUrl(value);
|
|
1319
|
-
return Boolean(normalized && !shouldIgnoreRequest(normalized));
|
|
1867
|
+
return Boolean(normalized && isLocalProjectOrigin(normalized) && !shouldIgnoreRequest(normalized));
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function isLocalProjectOrigin(url) {
|
|
1871
|
+
if (url.origin === window.location.origin) {
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
return (
|
|
1876
|
+
url.hostname === "localhost" ||
|
|
1877
|
+
url.hostname === "127.0.0.1" ||
|
|
1878
|
+
url.hostname === "[::1]" ||
|
|
1879
|
+
url.hostname === "::1"
|
|
1880
|
+
);
|
|
1320
1881
|
}
|
|
1321
1882
|
|
|
1322
1883
|
function matchEndpoint(method, path) {
|
|
@@ -1460,8 +2021,8 @@ export function getOverlayClientScript() {
|
|
|
1460
2021
|
function withDefaultDependencies(dependencies) {
|
|
1461
2022
|
return {
|
|
1462
2023
|
loadConfig: dependencies?.loadConfig ?? loadConfig,
|
|
2024
|
+
readProjectData: dependencies?.readProjectData ?? readProjectData,
|
|
1463
2025
|
readReportData: dependencies?.readReportData ?? readReportData,
|
|
1464
|
-
runScanCommand: dependencies?.runScanCommand ?? runScanCommand,
|
|
1465
2026
|
createLocalUiServer: dependencies?.createLocalUiServer ?? createLocalUiServer,
|
|
1466
2027
|
isFrontendReachable: dependencies?.isFrontendReachable ?? isFrontendReachable,
|
|
1467
2028
|
startFrontendDevServer: dependencies?.startFrontendDevServer ?? startFrontendDevServer,
|