Skip to content

Commit 5142404

Browse files
authored
Refactor the codebase to remove the dup functions and simplify the namings. (#37)
1 parent e8aa2a6 commit 5142404

30 files changed

Lines changed: 840 additions & 658 deletions

src/analysis/engine.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { createDefaultScanner, type SecurityScanner } from "./rules/index.js";
1414
import { computeInsights, type Insight } from "./insights.js";
1515
import { insightToIssue, securityFindingToIssue } from "./issue-mappers.js";
1616
import { computeIssueId } from "../utils/issue-id.js";
17-
import { extractActiveEndpoints, windowByEndpoint } from "./insights/prepare.js";
17+
import { extractActiveEndpoints, keepRecentPerEndpoint } from "./insights/prepare.js";
1818

1919
export type { AnalysisUpdate };
2020

@@ -71,7 +71,7 @@ export class AnalysisEngine {
7171
const logs = this.services.logStore.getAll() as readonly TracedLog[];
7272
const fetches = this.services.fetchStore.getAll() as readonly TracedFetch[];
7373

74-
const requests = windowByEndpoint(allRequests);
74+
const requests = keepRecentPerEndpoint(allRequests);
7575
const flows = groupRequestsIntoFlows(requests);
7676

7777
this.cachedFindings = this.scanner.scan({ requests, logs });
@@ -86,24 +86,18 @@ export class AnalysisEngine {
8686
});
8787

8888
const issueStore = this.services.issueStore;
89+
const currentIssueIds = new Set<string>();
8990

90-
// Upsert security findings into IssueStore
9191
for (const finding of this.cachedFindings) {
92-
issueStore.upsert(securityFindingToIssue(finding), "passive");
93-
}
94-
95-
// Upsert performance/reliability insights into IssueStore
96-
for (const insight of this.cachedInsights) {
97-
issueStore.upsert(insightToIssue(insight), "passive");
92+
const issue = securityFindingToIssue(finding);
93+
issueStore.upsert(issue, "passive");
94+
currentIssueIds.add(computeIssueId(issue));
9895
}
9996

100-
// Build the set of currently-detected issue IDs for reconciliation
101-
const currentIssueIds = new Set<string>();
102-
for (const finding of this.cachedFindings) {
103-
currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
104-
}
10597
for (const insight of this.cachedInsights) {
106-
currentIssueIds.add(computeIssueId(insightToIssue(insight)));
98+
const issue = insightToIssue(insight);
99+
issueStore.upsert(issue, "passive");
100+
currentIssueIds.add(computeIssueId(issue));
107101
}
108102

109103
const activeEndpoints = extractActiveEndpoints(allRequests);

src/analysis/group.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,36 @@ import { randomUUID } from "node:crypto";
66
import type { TracedRequest, LabeledRequest, RequestFlow } from "../types/index.js";
77
import { FLOW_GAP_MS, DASHBOARD_PREFIX } from "../constants/index.js";
88
import { isErrorStatus } from "../utils/http-status.js";
9+
import { stripQueryString } from "../utils/endpoint.js";
910
import { labelRequest, prettifyEndpoint, prettifyPageName, capitalize, deriveActionVerb } from "./label.js";
1011
import { getEffectivePath } from "./categorize.js";
11-
import { markDuplicates, collapsePolling, detectWarnings } from "./transforms.js";
12+
import { flagDuplicateRequests, mergePollingSequences, collectRequestWarnings } from "./transforms.js";
13+
14+
function shouldStartNewFlow(
15+
labeled: LabeledRequest,
16+
currentRequests: LabeledRequest[],
17+
lastEndTime: number,
18+
currentSourcePage: string | undefined,
19+
startedAt: number,
20+
): boolean {
21+
if (currentRequests.length === 0) return false;
22+
23+
const sourcePage = labeled.sourcePage;
24+
25+
// 1. Page change — referer switched, indicating user navigated
26+
const isNewPage =
27+
sourcePage !== undefined &&
28+
currentSourcePage !== undefined &&
29+
sourcePage !== currentSourcePage;
30+
31+
// 2. Time gap — idle period exceeds FLOW_GAP_MS, suggesting a new action
32+
const isTimeGap = startedAt - lastEndTime > FLOW_GAP_MS;
33+
34+
// 3. Page load — HTML/navigation request marks a fresh page lifecycle
35+
const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
36+
37+
return isNewPage || isTimeGap || isPageLoad;
38+
}
1239

1340
export function groupRequestsIntoFlows(
1441
requests: readonly TracedRequest[],
@@ -26,28 +53,13 @@ export function groupRequestsIntoFlows(
2653
const labeled = labelRequest(req);
2754
if (labeled.category === "static") continue;
2855

29-
const sourcePage = labeled.sourcePage;
30-
const gap = currentRequests.length > 0 ? req.startedAt - lastEndTime : 0;
31-
32-
// A new flow starts when any of three signals fire:
33-
// 1. Page change — referer switched, indicating user navigated
34-
// 2. Time gap — idle period exceeds FLOW_GAP_MS, suggesting a new action
35-
// 3. Page load — HTML/navigation request marks a fresh page lifecycle
36-
const isNewPage =
37-
currentRequests.length > 0 &&
38-
sourcePage !== undefined &&
39-
currentSourcePage !== undefined &&
40-
sourcePage !== currentSourcePage;
41-
const isPageLoad = labeled.category === "page-load" || labeled.category === "navigation";
42-
const isTimeGap = currentRequests.length > 0 && gap > FLOW_GAP_MS;
43-
44-
if (currentRequests.length > 0 && (isNewPage || isTimeGap || isPageLoad)) {
56+
if (shouldStartNewFlow(labeled, currentRequests, lastEndTime, currentSourcePage, req.startedAt)) {
4557
flows.push(buildFlow(currentRequests));
4658
currentRequests = [];
4759
}
4860

4961
currentRequests.push(labeled);
50-
currentSourcePage = sourcePage ?? currentSourcePage;
62+
currentSourcePage = labeled.sourcePage ?? currentSourcePage;
5163
lastEndTime = Math.max(lastEndTime, req.startedAt + req.durationMs);
5264
}
5365

@@ -59,8 +71,8 @@ export function groupRequestsIntoFlows(
5971
}
6072

6173
function buildFlow(rawRequests: LabeledRequest[]): RequestFlow {
62-
markDuplicates(rawRequests);
63-
const requests = collapsePolling(rawRequests);
74+
flagDuplicateRequests(rawRequests);
75+
const requests = mergePollingSequences(rawRequests);
6476

6577
const first = requests[0];
6678
const startTime = first.startedAt;
@@ -86,7 +98,7 @@ function buildFlow(rawRequests: LabeledRequest[]): RequestFlow {
8698
startTime,
8799
totalDurationMs: Math.round(endTime - startTime),
88100
hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
89-
warnings: detectWarnings(rawRequests),
101+
warnings: collectRequestWarnings(rawRequests),
90102
sourcePage,
91103
redundancyPct,
92104
};
@@ -100,16 +112,16 @@ function getDominantSourcePage(requests: LabeledRequest[]): string {
100112
}
101113
}
102114

103-
let best = "";
104-
let bestCount = 0;
115+
let mostCommonPage = "";
116+
let highestCount = 0;
105117
for (const [page, count] of counts) {
106-
if (count > bestCount) {
107-
best = page;
108-
bestCount = count;
118+
if (count > highestCount) {
119+
mostCommonPage = page;
120+
highestCount = count;
109121
}
110122
}
111123

112-
return best || requests[0]?.path?.split("?")[0] || "/";
124+
return mostCommonPage || (requests[0]?.path ? stripQueryString(requests[0].path) : "") || "/";
113125
}
114126

115127
function deriveFlowLabel(requests: LabeledRequest[], sourcePage: string): string {
@@ -122,7 +134,7 @@ function deriveFlowLabel(requests: LabeledRequest[], sourcePage: string): string
122134
requests[0];
123135

124136
if (trigger.category === "page-load" || trigger.category === "navigation") {
125-
const pageName = prettifyPageName(trigger.path.split("?")[0]);
137+
const pageName = prettifyPageName(stripQueryString(trigger.path));
126138
return `${pageName} Page`;
127139
}
128140

src/analysis/insights/prepare.ts

Lines changed: 71 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import type {
44
PreparedInsightContext,
55
EndpointGroup,
66
} from "./types.js";
7-
import { groupBy } from "../../utils/collections.js";
7+
import { groupBy, getOrCreate } from "../../utils/collections.js";
88
import { getEndpointKey } from "../../utils/endpoint.js";
99
import { DASHBOARD_PREFIX } from "../../constants/index.js";
1010
import { isErrorStatus } from "../../utils/http-status.js";
1111
import { INSIGHT_WINDOW_PER_ENDPOINT } from "../../constants/config.js";
1212
import { getQueryShape, getQueryInfo } from "./query-helpers.js";
1313

14-
function createEndpointGroup(): EndpointGroup {
14+
function emptyEndpointGroup(): EndpointGroup {
1515
return {
1616
total: 0,
1717
errors: 0,
@@ -24,23 +24,26 @@ function createEndpointGroup(): EndpointGroup {
2424
};
2525
}
2626

27-
export function windowByEndpoint(
27+
/**
28+
* Limits analysis to the N most recent requests per endpoint so that old,
29+
* stale data does not skew insight calculations. The window size is
30+
* controlled by INSIGHT_WINDOW_PER_ENDPOINT.
31+
*/
32+
export function keepRecentPerEndpoint(
2833
requests: readonly TracedRequest[],
2934
): TracedRequest[] {
3035
const byEndpoint = new Map<string, TracedRequest[]>();
31-
for (const r of requests) {
32-
const ep = getEndpointKey(r.method, r.path);
33-
let list = byEndpoint.get(ep);
34-
if (!list) {
35-
list = [];
36-
byEndpoint.set(ep, list);
37-
}
38-
list.push(r);
36+
for (const request of requests) {
37+
const endpointKey = getEndpointKey(request.method, request.path);
38+
const list = getOrCreate(byEndpoint, endpointKey, () => []);
39+
list.push(request);
3940
}
41+
4042
const windowed: TracedRequest[] = [];
4143
for (const [, reqs] of byEndpoint) {
4244
windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
4345
}
46+
4447
return windowed;
4548
}
4649

@@ -49,73 +52,77 @@ export function windowByEndpoint(
4952
* Used by AnalysisEngine to determine which endpoints were active
5053
* during a recompute cycle for evidence-based issue resolution.
5154
*/
55+
function filterUserRequests(requests: readonly TracedRequest[]): TracedRequest[] {
56+
return requests.filter(
57+
(request) =>
58+
!request.isStatic &&
59+
!request.isHealthCheck &&
60+
(!request.path || !request.path.startsWith(DASHBOARD_PREFIX)),
61+
);
62+
}
63+
5264
export function extractActiveEndpoints(
5365
requests: readonly TracedRequest[],
5466
): Set<string> {
5567
const endpoints = new Set<string>();
56-
for (const r of requests) {
57-
if (
58-
!r.isStatic &&
59-
!r.isHealthCheck &&
60-
(!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
61-
) {
62-
endpoints.add(getEndpointKey(r.method, r.path));
63-
}
68+
for (const request of filterUserRequests(requests)) {
69+
endpoints.add(getEndpointKey(request.method, request.path));
6470
}
6571
return endpoints;
6672
}
6773

68-
export function prepareContext(ctx: InsightContext): PreparedInsightContext {
69-
const nonStatic = ctx.requests.filter(
70-
(r) =>
71-
!r.isStatic &&
72-
!r.isHealthCheck &&
73-
(!r.path || !r.path.startsWith(DASHBOARD_PREFIX)),
74-
);
75-
76-
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
77-
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
78-
79-
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
80-
81-
const recent = windowByEndpoint(nonStatic);
74+
function aggregateEndpointMetrics(
75+
recent: readonly TracedRequest[],
76+
queriesByReq: Map<string, TracedQuery[]>,
77+
fetchesByReq: Map<string, { durationMs: number }[]>,
78+
): Map<string, EndpointGroup> {
8279
const endpointGroups = new Map<string, EndpointGroup>();
83-
for (const r of recent) {
84-
const ep = getEndpointKey(r.method, r.path);
85-
let g = endpointGroups.get(ep);
86-
if (!g) {
87-
g = createEndpointGroup();
88-
endpointGroups.set(ep, g);
89-
}
90-
g.total++;
91-
if (isErrorStatus(r.statusCode)) g.errors++;
92-
g.totalDuration += r.durationMs;
93-
g.totalSize += r.responseSize ?? 0;
80+
for (const request of recent) {
81+
const endpointKey = getEndpointKey(request.method, request.path);
82+
const group = getOrCreate(endpointGroups, endpointKey, emptyEndpointGroup);
83+
group.total++;
84+
if (isErrorStatus(request.statusCode)) group.errors++;
85+
group.totalDuration += request.durationMs;
86+
group.totalSize += request.responseSize ?? 0;
9487

95-
const reqQueries: TracedQuery[] = queriesByReq.get(r.id) ?? [];
96-
g.queryCount += reqQueries.length;
97-
for (const q of reqQueries) {
98-
g.totalQueryTimeMs += q.durationMs;
99-
const shape = getQueryShape(q);
100-
const info = getQueryInfo(q);
101-
let sd = g.queryShapeDurations.get(shape);
102-
if (!sd) {
103-
sd = {
104-
totalMs: 0,
105-
count: 0,
106-
label: info.op + (info.table ? ` ${info.table}` : ""),
107-
};
108-
g.queryShapeDurations.set(shape, sd);
109-
}
110-
sd.totalMs += q.durationMs;
111-
sd.count++;
88+
const reqQueries: TracedQuery[] = queriesByReq.get(request.id) ?? [];
89+
group.queryCount += reqQueries.length;
90+
for (const query of reqQueries) {
91+
group.totalQueryTimeMs += query.durationMs;
92+
const shape = getQueryShape(query);
93+
const info = getQueryInfo(query);
94+
const shapeDuration = getOrCreate(group.queryShapeDurations, shape, () => ({
95+
totalMs: 0,
96+
count: 0,
97+
label: info.op + (info.table ? ` ${info.table}` : ""),
98+
}));
99+
shapeDuration.totalMs += query.durationMs;
100+
shapeDuration.count++;
112101
}
113102

114-
const reqFetches = fetchesByReq.get(r.id) ?? [];
115-
for (const f of reqFetches) {
116-
g.totalFetchTimeMs += f.durationMs;
103+
const reqFetches = fetchesByReq.get(request.id) ?? [];
104+
for (const fetch of reqFetches) {
105+
group.totalFetchTimeMs += fetch.durationMs;
117106
}
118107
}
108+
return endpointGroups;
109+
}
110+
111+
/**
112+
* Pre-computes lookup tables (queries-by-request, fetches-by-request,
113+
* request-by-id) and aggregated per-endpoint metrics used by all insight
114+
* rules, so each rule can focus on detection logic rather than data wrangling.
115+
*/
116+
export function buildInsightContext(ctx: InsightContext): PreparedInsightContext {
117+
const nonStatic = filterUserRequests(ctx.requests);
118+
119+
const queriesByReq = groupBy(ctx.queries, (query) => query.parentRequestId);
120+
const fetchesByReq = groupBy(ctx.fetches, (fetch) => fetch.parentRequestId);
121+
122+
const reqById = new Map(nonStatic.map((request) => [request.id, request]));
123+
124+
const recent = keepRecentPerEndpoint(nonStatic);
125+
const endpointGroups = aggregateEndpointMetrics(recent, queriesByReq, fetchesByReq);
119126

120127
return {
121128
...ctx,

0 commit comments

Comments
 (0)