@@ -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" ;
88import { getEndpointKey } from "../../utils/endpoint.js" ;
99import { DASHBOARD_PREFIX } from "../../constants/index.js" ;
1010import { isErrorStatus } from "../../utils/http-status.js" ;
1111import { INSIGHT_WINDOW_PER_ENDPOINT } from "../../constants/config.js" ;
1212import { 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+
5264export 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