anlyx 0.1.3 → 0.1.6-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -5
- package/dist/dev-command.d.ts +16 -5
- package/dist/dev-command.d.ts.map +1 -1
- package/dist/dev-command.js +663 -54
- package/dist/dev-command.js.map +1 -1
- package/dist/flow-commands.d.ts +49 -0
- package/dist/flow-commands.d.ts.map +1 -0
- package/dist/flow-commands.js +187 -0
- package/dist/flow-commands.js.map +1 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -69
- 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,27 @@ 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, parseProjectValidationReport, 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
|
-
|
|
25
|
-
|
|
26
|
-
...(options.outputDir ? { outputDir: options.outputDir } : {})
|
|
22
|
+
const validationReportPath = join(outputDir, "validation-report.json");
|
|
23
|
+
const loadedData = await ensureProjectData({
|
|
24
|
+
projectDataPath,
|
|
25
|
+
splitProjectDataPath,
|
|
26
|
+
dependencies
|
|
27
27
|
});
|
|
28
28
|
const frontendStarted = await ensureFrontendDevServer({
|
|
29
29
|
cwd,
|
|
@@ -31,13 +31,18 @@ export async function runDevCommand(options = {}) {
|
|
|
31
31
|
dependencies
|
|
32
32
|
});
|
|
33
33
|
const port = options.port ?? getConfiguredPort(config);
|
|
34
|
-
const
|
|
34
|
+
const validationReport = await dependencies.readValidationReport(validationReportPath);
|
|
35
|
+
const serverOptions = {
|
|
35
36
|
port,
|
|
36
|
-
|
|
37
|
+
projectData: loadedData.data,
|
|
37
38
|
viewerRoot: getViewerRoot(),
|
|
38
39
|
frontendBaseUrl: config.frontend.baseUrl,
|
|
39
40
|
mode: config.server.mode
|
|
40
|
-
}
|
|
41
|
+
};
|
|
42
|
+
if (validationReport) {
|
|
43
|
+
serverOptions.validationReport = validationReport;
|
|
44
|
+
}
|
|
45
|
+
const server = await dependencies.createLocalUiServer(serverOptions);
|
|
41
46
|
activeLocalUiServers.add(server);
|
|
42
47
|
const shouldOpenBrowser = options.open ?? config.server.openBrowser;
|
|
43
48
|
const browserUrl = config.server.mode === "inject" ? config.frontend.baseUrl : server.url;
|
|
@@ -47,14 +52,47 @@ export async function runDevCommand(options = {}) {
|
|
|
47
52
|
return {
|
|
48
53
|
url: server.url,
|
|
49
54
|
port,
|
|
55
|
+
projectDataPath: loadedData.path,
|
|
50
56
|
reportDataPath,
|
|
51
57
|
mode: config.server.mode,
|
|
52
58
|
frontendStarted,
|
|
53
|
-
scanRan,
|
|
54
59
|
...(config.server.mode === "inject" ? { frontendUrl: config.frontend.baseUrl } : {}),
|
|
55
60
|
...(config.server.mode === "inject" ? { scriptTag: getOverlayScriptTag(server.url) } : {})
|
|
56
61
|
};
|
|
57
62
|
}
|
|
63
|
+
async function loadConfigOrViewerFallback(dependencies, options) {
|
|
64
|
+
try {
|
|
65
|
+
return await dependencies.loadConfig(options);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!isMissingConfigError(error)) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
return createViewerFallbackConfig();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function createViewerFallbackConfig() {
|
|
75
|
+
return {
|
|
76
|
+
projectName: "Anlyx Project JSON",
|
|
77
|
+
backend: {
|
|
78
|
+
type: "openapi",
|
|
79
|
+
openApiUrl: "about:blank"
|
|
80
|
+
},
|
|
81
|
+
frontend: {
|
|
82
|
+
type: "manual",
|
|
83
|
+
baseUrl: "http://localhost:4777",
|
|
84
|
+
urls: [],
|
|
85
|
+
viewport: { width: 1440, height: 900 },
|
|
86
|
+
capture: { mode: "segments", segmentHeight: 900 }
|
|
87
|
+
},
|
|
88
|
+
server: {
|
|
89
|
+
port: 4777,
|
|
90
|
+
openBrowser: true,
|
|
91
|
+
mode: "viewer"
|
|
92
|
+
},
|
|
93
|
+
dev: {}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
58
96
|
export async function closeActiveLocalUiServers() {
|
|
59
97
|
const servers = Array.from(activeLocalUiServers);
|
|
60
98
|
activeLocalUiServers.clear();
|
|
@@ -62,33 +100,21 @@ export async function closeActiveLocalUiServers() {
|
|
|
62
100
|
await server.close?.();
|
|
63
101
|
}));
|
|
64
102
|
}
|
|
65
|
-
async function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
async function ensureProjectData(options) {
|
|
104
|
+
for (const path of [options.projectDataPath, options.splitProjectDataPath]) {
|
|
105
|
+
try {
|
|
106
|
+
return {
|
|
107
|
+
data: await options.dependencies.readProjectData(path),
|
|
108
|
+
path
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!isMissingProjectDataError(error)) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
75
115
|
}
|
|
76
116
|
}
|
|
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
|
-
};
|
|
117
|
+
throw new Error('Anlyx project data not found. Create or import "anlyx.project.json" first.');
|
|
92
118
|
}
|
|
93
119
|
async function ensureFrontendDevServer(options) {
|
|
94
120
|
const command = options.config.dev?.command;
|
|
@@ -104,8 +130,93 @@ async function ensureFrontendDevServer(options) {
|
|
|
104
130
|
});
|
|
105
131
|
return true;
|
|
106
132
|
}
|
|
107
|
-
function
|
|
108
|
-
return error instanceof Error && /
|
|
133
|
+
function isMissingProjectDataError(error) {
|
|
134
|
+
return error instanceof Error && /Anlyx project data not found/.test(error.message);
|
|
135
|
+
}
|
|
136
|
+
function isMissingConfigError(error) {
|
|
137
|
+
return error instanceof Error && /Anlyx config file not found/.test(error.message);
|
|
138
|
+
}
|
|
139
|
+
export async function readProjectData(path) {
|
|
140
|
+
let content;
|
|
141
|
+
try {
|
|
142
|
+
content = await readFile(path, "utf8");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
146
|
+
throw new Error(`Anlyx project data not found: ${path}. Create anlyx.project.json first.`);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(content);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new Error(`Failed to parse Anlyx project data JSON at ${path}: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
if (path.endsWith("/index.json")) {
|
|
159
|
+
return await readSplitProjectData(path, parsed);
|
|
160
|
+
}
|
|
161
|
+
return normalizeProjectInput(parseProjectData(parsed));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new Error(`Invalid Anlyx project data at ${path}: ${error instanceof Error ? error.message : "invalid project data"}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function readSplitProjectData(indexPath, index) {
|
|
168
|
+
const projectDir = dirname(indexPath);
|
|
169
|
+
const [areas, pages, features, requests, flows, architecture, evidence, measurements, dictionary, coverage, overview, capabilities, dataLifecycles, impactMaps] = await Promise.all([
|
|
170
|
+
readOptionalProjectJson(join(projectDir, "areas.json")),
|
|
171
|
+
readOptionalProjectJson(join(projectDir, "pages.json")),
|
|
172
|
+
readOptionalProjectJson(join(projectDir, "features.json")),
|
|
173
|
+
readOptionalProjectJson(join(projectDir, "requests.json")),
|
|
174
|
+
readOptionalProjectJson(join(projectDir, "flows.json")),
|
|
175
|
+
readOptionalProjectJson(join(projectDir, "architecture.json")),
|
|
176
|
+
readOptionalProjectJson(join(projectDir, "evidence.json")),
|
|
177
|
+
readOptionalProjectJson(join(projectDir, "measurements.json")),
|
|
178
|
+
readOptionalProjectJson(join(projectDir, "dictionary.json")),
|
|
179
|
+
readOptionalProjectJson(join(projectDir, "coverage.json")),
|
|
180
|
+
readOptionalProjectJson(join(projectDir, "overview.json")),
|
|
181
|
+
readOptionalProjectJson(join(projectDir, "capabilities.json")),
|
|
182
|
+
readOptionalProjectJson(join(projectDir, "data-lifecycles.json")),
|
|
183
|
+
readOptionalProjectJson(join(projectDir, "impact-maps.json"))
|
|
184
|
+
]);
|
|
185
|
+
return normalizeProjectInput({
|
|
186
|
+
index,
|
|
187
|
+
...(areas === undefined ? {} : { areas }),
|
|
188
|
+
...(pages === undefined ? {} : { pages }),
|
|
189
|
+
...(features === undefined ? {} : { features }),
|
|
190
|
+
...(requests === undefined ? {} : { requests }),
|
|
191
|
+
...(flows === undefined ? {} : { flows }),
|
|
192
|
+
...(architecture === undefined ? {} : { architecture }),
|
|
193
|
+
...(evidence === undefined ? {} : { evidence }),
|
|
194
|
+
...(measurements === undefined ? {} : { measurements }),
|
|
195
|
+
...(dictionary === undefined ? {} : { dictionary }),
|
|
196
|
+
...(coverage === undefined ? {} : { coverage }),
|
|
197
|
+
...(overview === undefined ? {} : { overview }),
|
|
198
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
199
|
+
...(dataLifecycles === undefined ? {} : { dataLifecycles }),
|
|
200
|
+
...(impactMaps === undefined ? {} : { impactMaps })
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async function readOptionalProjectJson(path) {
|
|
204
|
+
let content;
|
|
205
|
+
try {
|
|
206
|
+
content = await readFile(path, "utf8");
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(content);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
throw new Error(`Failed to parse split Anlyx project JSON at ${path}: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
219
|
+
}
|
|
109
220
|
}
|
|
110
221
|
export async function readReportData(path) {
|
|
111
222
|
let content;
|
|
@@ -114,7 +225,7 @@ export async function readReportData(path) {
|
|
|
114
225
|
}
|
|
115
226
|
catch (error) {
|
|
116
227
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
117
|
-
throw new Error(`Anlyx report data not found: ${path}.
|
|
228
|
+
throw new Error(`Anlyx report data not found: ${path}. Create or import "anlyx.project.json" first.`);
|
|
118
229
|
}
|
|
119
230
|
throw error;
|
|
120
231
|
}
|
|
@@ -137,6 +248,26 @@ export async function readReportData(path) {
|
|
|
137
248
|
}
|
|
138
249
|
return result.data;
|
|
139
250
|
}
|
|
251
|
+
export async function readValidationReport(path) {
|
|
252
|
+
let content;
|
|
253
|
+
try {
|
|
254
|
+
content = await readFile(path, "utf8");
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(content);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
throw new Error(`Failed to parse Anlyx validation report JSON at ${path}: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
268
|
+
}
|
|
269
|
+
return parseProjectValidationReport(parsed);
|
|
270
|
+
}
|
|
140
271
|
export async function createLocalUiServer(options) {
|
|
141
272
|
const viteServer = await createServer({
|
|
142
273
|
root: options.viewerRoot,
|
|
@@ -194,11 +325,12 @@ export function startFrontendDevServer(options) {
|
|
|
194
325
|
};
|
|
195
326
|
}
|
|
196
327
|
function createAnlyxDevPlugin(options) {
|
|
328
|
+
const liveEvents = options.reportData ? createLiveEventRuntime(options.reportData) : undefined;
|
|
197
329
|
return {
|
|
198
330
|
name: "anlyx-dev-runtime",
|
|
199
331
|
configureServer(server) {
|
|
200
332
|
server.middlewares.use((request, response, next) => {
|
|
201
|
-
if (request.method
|
|
333
|
+
if (isReadRequest(request.method) && options.mode === "viewer" && request.url === "/") {
|
|
202
334
|
request.url = "/viewer.html";
|
|
203
335
|
}
|
|
204
336
|
next();
|
|
@@ -211,10 +343,62 @@ function createAnlyxDevPlugin(options) {
|
|
|
211
343
|
response.end();
|
|
212
344
|
return;
|
|
213
345
|
}
|
|
346
|
+
if (request.method === "GET" && isProjectDataPath(requestUrl)) {
|
|
347
|
+
if (!options.projectData) {
|
|
348
|
+
sendJsonError(response, 404, new Error("Anlyx project data not loaded."));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
sendJson(response, options.projectData);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
214
354
|
if (request.method === "GET" && isReportDataPath(requestUrl)) {
|
|
355
|
+
if (!options.reportData) {
|
|
356
|
+
sendJsonError(response, 404, new Error("Legacy Anlyx report data not loaded."));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
215
359
|
sendJson(response, options.reportData);
|
|
216
360
|
return;
|
|
217
361
|
}
|
|
362
|
+
if (request.method === "GET" && isValidationReportPath(requestUrl)) {
|
|
363
|
+
if (!options.validationReport) {
|
|
364
|
+
sendJsonError(response, 404, new Error("Anlyx validation report not loaded."));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
sendJson(response, options.validationReport);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (request.method === "GET" && requestUrl === "/_anlyx/events/stream") {
|
|
371
|
+
if (!liveEvents) {
|
|
372
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
liveEvents.openStream(request, response);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (request.method === "POST" && requestUrl === "/_anlyx/events/browser-request") {
|
|
379
|
+
if (!liveEvents) {
|
|
380
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await liveEvents.recordBrowserRequest(request, response);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (request.method === "POST" && requestUrl === "/_anlyx/events/page-context") {
|
|
387
|
+
if (!liveEvents) {
|
|
388
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await liveEvents.recordPageContext(request, response);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (request.method === "POST" && requestUrl === "/_anlyx/events/backend-spans") {
|
|
395
|
+
if (!liveEvents) {
|
|
396
|
+
sendJsonError(response, 404, new Error("Legacy live events are not available for project data."));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await liveEvents.recordBackendSpans(request, response);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
218
402
|
if (request.method === "GET" && requestUrl === "/_anlyx/overlay.js") {
|
|
219
403
|
response.statusCode = 200;
|
|
220
404
|
setCorsHeaders(response);
|
|
@@ -236,7 +420,7 @@ function createAnlyxDevPlugin(options) {
|
|
|
236
420
|
response.end(getInjectModeHtml(options.frontendBaseUrl, getOverlayScriptTag(getServerUrl(options.port))));
|
|
237
421
|
return;
|
|
238
422
|
}
|
|
239
|
-
if (request.method
|
|
423
|
+
if (isReadRequest(request.method) && isStandaloneViewerPath(requestUrl)) {
|
|
240
424
|
request.url = "/viewer.html";
|
|
241
425
|
next();
|
|
242
426
|
return;
|
|
@@ -256,12 +440,140 @@ function createAnlyxDevPlugin(options) {
|
|
|
256
440
|
}
|
|
257
441
|
};
|
|
258
442
|
}
|
|
443
|
+
export function createLiveEventRuntime(reportData) {
|
|
444
|
+
const streams = new Set();
|
|
445
|
+
const recordsByRequestId = new Map();
|
|
446
|
+
const pendingSpansByRequestId = new Map();
|
|
447
|
+
let latestPageContext;
|
|
448
|
+
let recentRecords = [];
|
|
449
|
+
const rememberRecord = (record) => {
|
|
450
|
+
recordsByRequestId.set(record.requestId, record);
|
|
451
|
+
recentRecords = [record, ...recentRecords.filter((item) => item.id !== record.id)].slice(0, 90);
|
|
452
|
+
};
|
|
453
|
+
const broadcastRecord = (record) => {
|
|
454
|
+
rememberRecord(record);
|
|
455
|
+
for (const stream of streams) {
|
|
456
|
+
writeFlowSseEvent(stream, record);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const broadcastPageContext = (event) => {
|
|
460
|
+
latestPageContext = event;
|
|
461
|
+
for (const stream of streams) {
|
|
462
|
+
writePageContextSseEvent(stream, event);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
return {
|
|
466
|
+
openStream(request, response) {
|
|
467
|
+
response.statusCode = 200;
|
|
468
|
+
setCorsHeaders(response);
|
|
469
|
+
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
470
|
+
response.setHeader("cache-control", "no-cache, no-transform");
|
|
471
|
+
response.setHeader("connection", "keep-alive");
|
|
472
|
+
response.write(": Anlyx event stream ready\n\n");
|
|
473
|
+
streams.add(response);
|
|
474
|
+
if (latestPageContext) {
|
|
475
|
+
writePageContextSseEvent(response, latestPageContext);
|
|
476
|
+
}
|
|
477
|
+
for (const record of [...recentRecords].reverse()) {
|
|
478
|
+
writeFlowSseEvent(response, record);
|
|
479
|
+
}
|
|
480
|
+
const heartbeat = setInterval(() => {
|
|
481
|
+
response.write(": heartbeat\n\n");
|
|
482
|
+
}, 30000);
|
|
483
|
+
request.on("close", () => {
|
|
484
|
+
clearInterval(heartbeat);
|
|
485
|
+
streams.delete(response);
|
|
486
|
+
response.end();
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
async recordBrowserRequest(request, response) {
|
|
490
|
+
let payload;
|
|
491
|
+
try {
|
|
492
|
+
payload = await readJsonRequestBody(request);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
sendJsonError(response, 400, error);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!isBrowserRequestEvent(payload)) {
|
|
499
|
+
sendJsonError(response, 400, new Error("Invalid Anlyx browser request event payload."));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const event = withRequestPageContext(payload, request, latestPageContext);
|
|
503
|
+
if (event.pageUrl) {
|
|
504
|
+
latestPageContext = {
|
|
505
|
+
type: "page_context",
|
|
506
|
+
pageUrl: event.pageUrl,
|
|
507
|
+
contextId: event.contextId ?? pageContextId(event.pageUrl),
|
|
508
|
+
observedAt: event.observedAt
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
let record = buildFlowRecordFromBrowserEvent(event, reportData);
|
|
512
|
+
const pendingSpans = pendingSpansByRequestId.get(event.id);
|
|
513
|
+
if (pendingSpans) {
|
|
514
|
+
pendingSpansByRequestId.delete(event.id);
|
|
515
|
+
record = mergeBackendSpansIntoFlowRecord(record, pendingSpans);
|
|
516
|
+
}
|
|
517
|
+
broadcastRecord(record);
|
|
518
|
+
sendAccepted(response, { accepted: true, id: record.id });
|
|
519
|
+
},
|
|
520
|
+
async recordPageContext(request, response) {
|
|
521
|
+
let payload;
|
|
522
|
+
try {
|
|
523
|
+
payload = await readJsonRequestBody(request);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
sendJsonError(response, 400, error);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (!isPageContextEvent(payload)) {
|
|
530
|
+
sendJsonError(response, 400, new Error("Invalid Anlyx page context event payload."));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
broadcastPageContext(payload);
|
|
534
|
+
sendAccepted(response, { accepted: true, contextId: payload.contextId });
|
|
535
|
+
},
|
|
536
|
+
async recordBackendSpans(request, response) {
|
|
537
|
+
let payload;
|
|
538
|
+
try {
|
|
539
|
+
payload = await readJsonRequestBody(request);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
sendJsonError(response, 400, error);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!isBackendSpanEvent(payload)) {
|
|
546
|
+
sendJsonError(response, 400, new Error("Invalid Anlyx backend span event payload."));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const existingRecord = recordsByRequestId.get(payload.requestId);
|
|
550
|
+
if (!existingRecord) {
|
|
551
|
+
const existingSpans = pendingSpansByRequestId.get(payload.requestId) ?? [];
|
|
552
|
+
pendingSpansByRequestId.set(payload.requestId, [...existingSpans, ...payload.spans]);
|
|
553
|
+
sendAccepted(response, { accepted: true, pending: true, requestId: payload.requestId });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const record = mergeBackendSpansIntoFlowRecord(existingRecord, payload.spans);
|
|
557
|
+
broadcastRecord(record);
|
|
558
|
+
sendAccepted(response, { accepted: true, id: record.id });
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
259
562
|
function isReportDataPath(path) {
|
|
260
563
|
return path === "/_anlyx/report-data" || path === "/api/report-data";
|
|
261
564
|
}
|
|
565
|
+
function isProjectDataPath(path) {
|
|
566
|
+
return path === "/_anlyx/project-data" || path === "/api/project-data";
|
|
567
|
+
}
|
|
568
|
+
function isValidationReportPath(path) {
|
|
569
|
+
return path === "/_anlyx/validation-report" || path === "/api/validation-report";
|
|
570
|
+
}
|
|
262
571
|
function isStandaloneViewerPath(path) {
|
|
263
572
|
return path === "/_anlyx/viewer" || path === "/_anlyx/viewer.html";
|
|
264
573
|
}
|
|
574
|
+
function isReadRequest(method) {
|
|
575
|
+
return method === "GET" || method === "HEAD";
|
|
576
|
+
}
|
|
265
577
|
function isAnlyxPath(path) {
|
|
266
578
|
return path.startsWith("/_anlyx/");
|
|
267
579
|
}
|
|
@@ -271,6 +583,130 @@ function sendJson(response, value) {
|
|
|
271
583
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
272
584
|
response.end(JSON.stringify(value));
|
|
273
585
|
}
|
|
586
|
+
function sendAccepted(response, value) {
|
|
587
|
+
response.statusCode = 202;
|
|
588
|
+
setCorsHeaders(response);
|
|
589
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
590
|
+
response.end(JSON.stringify(value));
|
|
591
|
+
}
|
|
592
|
+
function sendJsonError(response, statusCode, error) {
|
|
593
|
+
response.statusCode = statusCode;
|
|
594
|
+
setCorsHeaders(response);
|
|
595
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
596
|
+
response.end(JSON.stringify({
|
|
597
|
+
error: error instanceof Error ? error.message : "Invalid Anlyx runtime event."
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
async function readJsonRequestBody(request) {
|
|
601
|
+
const body = await readRequestBody(request);
|
|
602
|
+
if (!body || body.byteLength === 0) {
|
|
603
|
+
throw new Error("Missing JSON request body.");
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
return JSON.parse(body.toString("utf8"));
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
throw new Error(`Failed to parse Anlyx runtime event JSON: ${error instanceof Error ? error.message : "invalid JSON"}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function writeFlowSseEvent(response, record) {
|
|
613
|
+
response.write("event: flow\n");
|
|
614
|
+
response.write(`data: ${JSON.stringify(record)}\n\n`);
|
|
615
|
+
}
|
|
616
|
+
function writePageContextSseEvent(response, event) {
|
|
617
|
+
response.write("event: page-context\n");
|
|
618
|
+
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
619
|
+
}
|
|
620
|
+
function withRequestPageContext(event, request, latestPageContext) {
|
|
621
|
+
const pageUrl = event.pageUrl ?? pageUrlFromReferer(request.headers.referer) ?? latestPageContext?.pageUrl;
|
|
622
|
+
if (!pageUrl) {
|
|
623
|
+
return event;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
...event,
|
|
627
|
+
pageUrl,
|
|
628
|
+
contextId: event.contextId ?? latestPageContext?.contextId ?? pageContextId(pageUrl)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function pageUrlFromReferer(referer) {
|
|
632
|
+
const value = Array.isArray(referer) ? referer[0] : referer;
|
|
633
|
+
if (!value) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
const parsed = new URL(value);
|
|
638
|
+
if (parsed.pathname.startsWith("/_anlyx/")) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
return `${parsed.origin}${parsed.pathname}${parsed.search}`;
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function pageContextId(pageUrl) {
|
|
648
|
+
try {
|
|
649
|
+
const parsed = new URL(pageUrl);
|
|
650
|
+
return `page:${parsed.pathname}${parsed.search}`;
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
return `page:${pageUrl || "/"}`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function isPageContextEvent(value) {
|
|
657
|
+
if (!value || typeof value !== "object") {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
const event = value;
|
|
661
|
+
return (event.type === "page_context" &&
|
|
662
|
+
typeof event.pageUrl === "string" &&
|
|
663
|
+
typeof event.contextId === "string" &&
|
|
664
|
+
typeof event.observedAt === "string");
|
|
665
|
+
}
|
|
666
|
+
function isBrowserRequestEvent(value) {
|
|
667
|
+
if (!value || typeof value !== "object") {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
const event = value;
|
|
671
|
+
return (event.type === "request" &&
|
|
672
|
+
typeof event.id === "string" &&
|
|
673
|
+
typeof event.method === "string" &&
|
|
674
|
+
typeof event.url === "string" &&
|
|
675
|
+
typeof event.observedAt === "string" &&
|
|
676
|
+
(event.path === undefined || typeof event.path === "string") &&
|
|
677
|
+
(event.pageUrl === undefined || typeof event.pageUrl === "string") &&
|
|
678
|
+
(event.status === undefined || typeof event.status === "number") &&
|
|
679
|
+
(event.durationMs === undefined || typeof event.durationMs === "number") &&
|
|
680
|
+
(event.contextId === undefined || typeof event.contextId === "string") &&
|
|
681
|
+
(event.priority === undefined ||
|
|
682
|
+
event.priority === "primary" ||
|
|
683
|
+
event.priority === "background") &&
|
|
684
|
+
(event.runtimeSource === undefined ||
|
|
685
|
+
event.runtimeSource === "browser" ||
|
|
686
|
+
event.runtimeSource === "server" ||
|
|
687
|
+
event.runtimeSource === "unknown"));
|
|
688
|
+
}
|
|
689
|
+
function isBackendSpanEvent(value) {
|
|
690
|
+
if (!value || typeof value !== "object") {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
const event = value;
|
|
694
|
+
return (event.type === "backend_spans" &&
|
|
695
|
+
typeof event.requestId === "string" &&
|
|
696
|
+
Array.isArray(event.spans) &&
|
|
697
|
+
event.spans.every(isBackendObservedSpan));
|
|
698
|
+
}
|
|
699
|
+
function isBackendObservedSpan(value) {
|
|
700
|
+
if (!value || typeof value !== "object") {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
const span = value;
|
|
704
|
+
return (typeof span.id === "string" &&
|
|
705
|
+
typeof span.type === "string" &&
|
|
706
|
+
typeof span.label === "string" &&
|
|
707
|
+
typeof span.startOffsetMs === "number" &&
|
|
708
|
+
typeof span.durationMs === "number");
|
|
709
|
+
}
|
|
274
710
|
async function sendRuntimeAsset(response, path, contentType) {
|
|
275
711
|
try {
|
|
276
712
|
const content = await readFile(path);
|
|
@@ -288,8 +724,8 @@ async function sendRuntimeAsset(response, path, contentType) {
|
|
|
288
724
|
}
|
|
289
725
|
function setCorsHeaders(response) {
|
|
290
726
|
response.setHeader("access-control-allow-origin", "*");
|
|
291
|
-
response.setHeader("access-control-allow-methods", "GET, OPTIONS");
|
|
292
|
-
response.setHeader("access-control-allow-headers", "content-type");
|
|
727
|
+
response.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
728
|
+
response.setHeader("access-control-allow-headers", "content-type, x-anlyx-request-id");
|
|
293
729
|
}
|
|
294
730
|
function getServerUrl(port) {
|
|
295
731
|
return `http://localhost:${port}`;
|
|
@@ -660,6 +1096,7 @@ export function getOverlayClientScript() {
|
|
|
660
1096
|
installUserActionTracker(root);
|
|
661
1097
|
installFetchInterceptor();
|
|
662
1098
|
installXhrInterceptor();
|
|
1099
|
+
installPageContextReporter();
|
|
663
1100
|
loadReport();
|
|
664
1101
|
}
|
|
665
1102
|
|
|
@@ -952,22 +1389,49 @@ export function getOverlayClientScript() {
|
|
|
952
1389
|
window.fetch = async function anlyxFetch(input, init) {
|
|
953
1390
|
const method = ((init && init.method) || (input && input.method) || "GET").toUpperCase();
|
|
954
1391
|
const url = typeof input === "string" ? input : input && input.url;
|
|
1392
|
+
const shouldTrack = shouldTrackRequestUrl(url);
|
|
1393
|
+
const requestId = shouldTrack ? createRequestId() : null;
|
|
1394
|
+
const args = requestId ? withAnlyxFetchRequestId(input, init, requestId) : arguments;
|
|
955
1395
|
const startedAt = performance.now();
|
|
956
1396
|
try {
|
|
957
|
-
const response = await originalFetch.apply(this,
|
|
958
|
-
if (
|
|
959
|
-
scheduleApiEventRecord({ method, url, status: response.status, durationMs: performance.now() - startedAt, startedAt });
|
|
1397
|
+
const response = await originalFetch.apply(this, args);
|
|
1398
|
+
if (shouldTrack) {
|
|
1399
|
+
scheduleApiEventRecord({ id: requestId, method, url, status: response.status, durationMs: performance.now() - startedAt, startedAt });
|
|
960
1400
|
}
|
|
961
1401
|
return response;
|
|
962
1402
|
} catch (error) {
|
|
963
|
-
if (
|
|
964
|
-
scheduleApiEventRecord({ method, url, status: "failed", durationMs: performance.now() - startedAt, startedAt });
|
|
1403
|
+
if (shouldTrack) {
|
|
1404
|
+
scheduleApiEventRecord({ id: requestId, method, url, status: "failed", durationMs: performance.now() - startedAt, startedAt });
|
|
965
1405
|
}
|
|
966
1406
|
throw error;
|
|
967
1407
|
}
|
|
968
1408
|
};
|
|
969
1409
|
}
|
|
970
1410
|
|
|
1411
|
+
function createRequestId() {
|
|
1412
|
+
return "anlyx-" + Date.now() + "-" + Math.random().toString(36).slice(2);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function withAnlyxFetchRequestId(input, init, requestId) {
|
|
1416
|
+
const nextInit = Object.assign({}, init || {});
|
|
1417
|
+
nextInit.headers = appendHeader(nextInit.headers || (input && input.headers), "X-Anlyx-Request-Id", requestId);
|
|
1418
|
+
return [input, nextInit];
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function appendHeader(headers, name, value) {
|
|
1422
|
+
try {
|
|
1423
|
+
if (typeof Headers !== "undefined") {
|
|
1424
|
+
const nextHeaders = new Headers(headers || {});
|
|
1425
|
+
nextHeaders.set(name, value);
|
|
1426
|
+
return nextHeaders;
|
|
1427
|
+
}
|
|
1428
|
+
} catch {
|
|
1429
|
+
// Fall through to a plain object header bag when Headers cannot clone input.
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return Object.assign({}, headers || {}, { [name]: value });
|
|
1433
|
+
}
|
|
1434
|
+
|
|
971
1435
|
function installUserActionTracker(root) {
|
|
972
1436
|
document.addEventListener("pointerdown", (event) => captureUserAction(event, root), true);
|
|
973
1437
|
document.addEventListener("click", (event) => captureUserAction(event, root), true);
|
|
@@ -1093,7 +1557,13 @@ export function getOverlayClientScript() {
|
|
|
1093
1557
|
const originalSend = XMLHttpRequest.prototype.send;
|
|
1094
1558
|
|
|
1095
1559
|
XMLHttpRequest.prototype.open = function anlyxOpen(method, url) {
|
|
1096
|
-
|
|
1560
|
+
const requestUrl = String(url || "");
|
|
1561
|
+
this.__anlyxRequest = {
|
|
1562
|
+
id: shouldTrackRequestUrl(requestUrl) ? createRequestId() : null,
|
|
1563
|
+
method: String(method || "GET").toUpperCase(),
|
|
1564
|
+
url: requestUrl,
|
|
1565
|
+
startedAt: 0
|
|
1566
|
+
};
|
|
1097
1567
|
return originalOpen.apply(this, arguments);
|
|
1098
1568
|
};
|
|
1099
1569
|
|
|
@@ -1101,9 +1571,17 @@ export function getOverlayClientScript() {
|
|
|
1101
1571
|
const request = this.__anlyxRequest;
|
|
1102
1572
|
if (request) {
|
|
1103
1573
|
request.startedAt = performance.now();
|
|
1574
|
+
if (request.id && this.setRequestHeader) {
|
|
1575
|
+
try {
|
|
1576
|
+
this.setRequestHeader("X-Anlyx-Request-Id", request.id);
|
|
1577
|
+
} catch {
|
|
1578
|
+
// Ignore header injection failures; the browser event can still be observed.
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1104
1581
|
this.addEventListener("loadend", () => {
|
|
1105
1582
|
if (shouldTrackRequestUrl(request.url)) {
|
|
1106
1583
|
scheduleApiEventRecord({
|
|
1584
|
+
id: request.id,
|
|
1107
1585
|
method: request.method,
|
|
1108
1586
|
url: request.url,
|
|
1109
1587
|
status: this.status || "unknown",
|
|
@@ -1131,14 +1609,16 @@ export function getOverlayClientScript() {
|
|
|
1131
1609
|
const passive = isPassiveRequest(event.method, normalized.pathname);
|
|
1132
1610
|
const triggeredBy = passive ? null : findActionForRequest(event.startedAt);
|
|
1133
1611
|
const item = {
|
|
1134
|
-
id:
|
|
1612
|
+
id: event.id || createRequestId(),
|
|
1135
1613
|
method: event.method,
|
|
1136
1614
|
path: normalized.pathname,
|
|
1137
1615
|
status: event.status,
|
|
1138
1616
|
durationMs: Math.round(event.durationMs),
|
|
1139
1617
|
count: 1,
|
|
1618
|
+
contextId: getCurrentContextId(),
|
|
1140
1619
|
lastSeenAt: Date.now(),
|
|
1141
1620
|
triggeredBy,
|
|
1621
|
+
priority: passive ? "background" : "primary",
|
|
1142
1622
|
source: triggeredBy ? "action" : classifyApiEventSource(normalized.pathname),
|
|
1143
1623
|
matchedEndpoint: matched.endpoint,
|
|
1144
1624
|
matchedFlow: matched.flow,
|
|
@@ -1152,8 +1632,10 @@ export function getOverlayClientScript() {
|
|
|
1152
1632
|
status: item.status,
|
|
1153
1633
|
durationMs: item.durationMs,
|
|
1154
1634
|
count: (existing.count || 1) + 1,
|
|
1635
|
+
contextId: item.contextId,
|
|
1155
1636
|
lastSeenAt: item.lastSeenAt,
|
|
1156
1637
|
triggeredBy: item.triggeredBy || existing.triggeredBy,
|
|
1638
|
+
priority: item.priority,
|
|
1157
1639
|
source: item.triggeredBy ? "action" : item.source,
|
|
1158
1640
|
matchedEndpoint: item.matchedEndpoint,
|
|
1159
1641
|
matchedFlow: item.matchedFlow,
|
|
@@ -1166,6 +1648,7 @@ export function getOverlayClientScript() {
|
|
|
1166
1648
|
state.open = true;
|
|
1167
1649
|
}
|
|
1168
1650
|
render();
|
|
1651
|
+
sendBrowserRequestEvent(updated);
|
|
1169
1652
|
return;
|
|
1170
1653
|
}
|
|
1171
1654
|
|
|
@@ -1176,12 +1659,124 @@ export function getOverlayClientScript() {
|
|
|
1176
1659
|
state.open = true;
|
|
1177
1660
|
}
|
|
1178
1661
|
render();
|
|
1662
|
+
sendBrowserRequestEvent(item);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function sendBrowserRequestEvent(item) {
|
|
1666
|
+
if (!window.fetch || !item || item.source === "anlyx") {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const payload = {
|
|
1671
|
+
id: item.id,
|
|
1672
|
+
type: "request",
|
|
1673
|
+
method: item.method,
|
|
1674
|
+
url: item.path,
|
|
1675
|
+
path: item.path,
|
|
1676
|
+
pageUrl: getCurrentPageUrl(),
|
|
1677
|
+
status: typeof item.status === "number" ? item.status : undefined,
|
|
1678
|
+
durationMs: typeof item.durationMs === "number" ? item.durationMs : undefined,
|
|
1679
|
+
observedAt: new Date(item.lastSeenAt || Date.now()).toISOString(),
|
|
1680
|
+
contextId: item.contextId,
|
|
1681
|
+
priority: item.priority === "background" ? "background" : "primary",
|
|
1682
|
+
runtimeSource: "browser",
|
|
1683
|
+
action: item.triggeredBy ? {
|
|
1684
|
+
label: item.triggeredBy.label,
|
|
1685
|
+
selector: item.triggeredBy.selector,
|
|
1686
|
+
observedAt: new Date(item.triggeredBy.capturedAt || item.lastSeenAt || Date.now()).toISOString()
|
|
1687
|
+
} : undefined
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
try {
|
|
1691
|
+
window.fetch(runtimeBaseUrl + "/_anlyx/events/browser-request", {
|
|
1692
|
+
method: "POST",
|
|
1693
|
+
headers: { "content-type": "application/json" },
|
|
1694
|
+
body: JSON.stringify(payload),
|
|
1695
|
+
keepalive: true
|
|
1696
|
+
}).catch(() => undefined);
|
|
1697
|
+
} catch {
|
|
1698
|
+
// Runtime ingestion is best-effort and must never break the user's app.
|
|
1699
|
+
}
|
|
1179
1700
|
}
|
|
1180
1701
|
|
|
1181
1702
|
function shouldAutoFocusEvent(item) {
|
|
1182
1703
|
return Boolean(item && item.triggeredBy);
|
|
1183
1704
|
}
|
|
1184
1705
|
|
|
1706
|
+
function getCurrentContextId() {
|
|
1707
|
+
const path = window.location && window.location.pathname ? window.location.pathname : "/";
|
|
1708
|
+
const search = window.location && window.location.search ? window.location.search : "";
|
|
1709
|
+
return "page:" + path + search;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function getCurrentPageUrl() {
|
|
1713
|
+
if (!window.location) {
|
|
1714
|
+
return "/";
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
return window.location.href.split("#")[0] || "/";
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function installPageContextReporter() {
|
|
1721
|
+
let lastPageUrl = "";
|
|
1722
|
+
let timer = null;
|
|
1723
|
+
|
|
1724
|
+
const schedulePageContext = () => {
|
|
1725
|
+
if (timer !== null) {
|
|
1726
|
+
window.clearTimeout(timer);
|
|
1727
|
+
}
|
|
1728
|
+
timer = window.setTimeout(() => {
|
|
1729
|
+
timer = null;
|
|
1730
|
+
sendPageContextEvent();
|
|
1731
|
+
}, 0);
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
const wrapHistoryMethod = (methodName) => {
|
|
1735
|
+
const original = window.history && window.history[methodName];
|
|
1736
|
+
if (!original || original.__anlyxWrapped) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
const wrapped = function anlyxHistoryWrapper() {
|
|
1741
|
+
const result = original.apply(this, arguments);
|
|
1742
|
+
schedulePageContext();
|
|
1743
|
+
return result;
|
|
1744
|
+
};
|
|
1745
|
+
wrapped.__anlyxWrapped = true;
|
|
1746
|
+
window.history[methodName] = wrapped;
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const sendPageContextEvent = () => {
|
|
1750
|
+
const pageUrl = getCurrentPageUrl();
|
|
1751
|
+
if (!pageUrl || pageUrl === lastPageUrl) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
lastPageUrl = pageUrl;
|
|
1755
|
+
|
|
1756
|
+
try {
|
|
1757
|
+
window.fetch(runtimeBaseUrl + "/_anlyx/events/page-context", {
|
|
1758
|
+
method: "POST",
|
|
1759
|
+
headers: { "content-type": "application/json" },
|
|
1760
|
+
body: JSON.stringify({
|
|
1761
|
+
type: "page_context",
|
|
1762
|
+
pageUrl,
|
|
1763
|
+
contextId: getCurrentContextId(),
|
|
1764
|
+
observedAt: new Date().toISOString()
|
|
1765
|
+
}),
|
|
1766
|
+
keepalive: true
|
|
1767
|
+
}).catch(() => undefined);
|
|
1768
|
+
} catch {
|
|
1769
|
+
// Page context reporting is best-effort and must never affect the app.
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
wrapHistoryMethod("pushState");
|
|
1774
|
+
wrapHistoryMethod("replaceState");
|
|
1775
|
+
window.addEventListener("popstate", schedulePageContext);
|
|
1776
|
+
window.addEventListener("hashchange", schedulePageContext);
|
|
1777
|
+
sendPageContextEvent();
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1185
1780
|
function brieflyExpandLauncher() {
|
|
1186
1781
|
launcherSettings.expandedUntil = Date.now() + 2600;
|
|
1187
1782
|
applyLauncherSettings();
|
|
@@ -1316,7 +1911,20 @@ export function getOverlayClientScript() {
|
|
|
1316
1911
|
|
|
1317
1912
|
function shouldTrackRequestUrl(value) {
|
|
1318
1913
|
const normalized = normalizeUrl(value);
|
|
1319
|
-
return Boolean(normalized && !shouldIgnoreRequest(normalized));
|
|
1914
|
+
return Boolean(normalized && isLocalProjectOrigin(normalized) && !shouldIgnoreRequest(normalized));
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function isLocalProjectOrigin(url) {
|
|
1918
|
+
if (url.origin === window.location.origin) {
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
return (
|
|
1923
|
+
url.hostname === "localhost" ||
|
|
1924
|
+
url.hostname === "127.0.0.1" ||
|
|
1925
|
+
url.hostname === "[::1]" ||
|
|
1926
|
+
url.hostname === "::1"
|
|
1927
|
+
);
|
|
1320
1928
|
}
|
|
1321
1929
|
|
|
1322
1930
|
function matchEndpoint(method, path) {
|
|
@@ -1460,8 +2068,9 @@ export function getOverlayClientScript() {
|
|
|
1460
2068
|
function withDefaultDependencies(dependencies) {
|
|
1461
2069
|
return {
|
|
1462
2070
|
loadConfig: dependencies?.loadConfig ?? loadConfig,
|
|
2071
|
+
readProjectData: dependencies?.readProjectData ?? readProjectData,
|
|
1463
2072
|
readReportData: dependencies?.readReportData ?? readReportData,
|
|
1464
|
-
|
|
2073
|
+
readValidationReport: dependencies?.readValidationReport ?? readValidationReport,
|
|
1465
2074
|
createLocalUiServer: dependencies?.createLocalUiServer ?? createLocalUiServer,
|
|
1466
2075
|
isFrontendReachable: dependencies?.isFrontendReachable ?? isFrontendReachable,
|
|
1467
2076
|
startFrontendDevServer: dependencies?.startFrontendDevServer ?? startFrontendDevServer,
|