@codebaz/nextdoctor-agent 0.1.0-beta.1
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/.turbo/turbo-build.log +3 -0
- package/README.md +568 -0
- package/dist/detectors/__tests__/cold-start-threshold.test.d.ts +2 -0
- package/dist/detectors/__tests__/cold-start-threshold.test.d.ts.map +1 -0
- package/dist/detectors/__tests__/cold-start-threshold.test.js +156 -0
- package/dist/detectors/__tests__/cold-start-threshold.test.js.map +1 -0
- package/dist/detectors/__tests__/dynamic-route-candidate.test.d.ts +2 -0
- package/dist/detectors/__tests__/dynamic-route-candidate.test.d.ts.map +1 -0
- package/dist/detectors/__tests__/dynamic-route-candidate.test.js +318 -0
- package/dist/detectors/__tests__/dynamic-route-candidate.test.js.map +1 -0
- package/dist/detectors/__tests__/fetch-no-cache.test.d.ts +2 -0
- package/dist/detectors/__tests__/fetch-no-cache.test.d.ts.map +1 -0
- package/dist/detectors/__tests__/fetch-no-cache.test.js +199 -0
- package/dist/detectors/__tests__/fetch-no-cache.test.js.map +1 -0
- package/dist/detectors/base-detector.d.ts +17 -0
- package/dist/detectors/base-detector.d.ts.map +1 -0
- package/dist/detectors/base-detector.js +50 -0
- package/dist/detectors/base-detector.js.map +1 -0
- package/dist/detectors/cold-start-threshold.detector.d.ts +11 -0
- package/dist/detectors/cold-start-threshold.detector.d.ts.map +1 -0
- package/dist/detectors/cold-start-threshold.detector.js +87 -0
- package/dist/detectors/cold-start-threshold.detector.js.map +1 -0
- package/dist/detectors/dynamic-route-candidate.detector.d.ts +23 -0
- package/dist/detectors/dynamic-route-candidate.detector.d.ts.map +1 -0
- package/dist/detectors/dynamic-route-candidate.detector.js +96 -0
- package/dist/detectors/dynamic-route-candidate.detector.js.map +1 -0
- package/dist/detectors/fetch-no-cache.detector.d.ts +12 -0
- package/dist/detectors/fetch-no-cache.detector.d.ts.map +1 -0
- package/dist/detectors/fetch-no-cache.detector.js +178 -0
- package/dist/detectors/fetch-no-cache.detector.js.map +1 -0
- package/dist/detectors/index.d.ts +28 -0
- package/dist/detectors/index.d.ts.map +1 -0
- package/dist/detectors/index.js +97 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/types.d.ts +32 -0
- package/dist/detectors/types.d.ts.map +1 -0
- package/dist/detectors/types.js +2 -0
- package/dist/detectors/types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +133 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +363 -0
- package/dist/init.js.map +1 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +61 -0
- package/dist/middleware.js.map +1 -0
- package/dist/optimization.d.ts +43 -0
- package/dist/optimization.d.ts.map +1 -0
- package/dist/optimization.js +139 -0
- package/dist/optimization.js.map +1 -0
- package/dist/system-monitor.d.ts +124 -0
- package/dist/system-monitor.d.ts.map +1 -0
- package/dist/system-monitor.js +221 -0
- package/dist/system-monitor.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
- package/src/detectors/__tests__/cold-start-threshold.test.ts +183 -0
- package/src/detectors/__tests__/dynamic-route-candidate.test.ts +365 -0
- package/src/detectors/__tests__/fetch-no-cache.test.ts +239 -0
- package/src/detectors/base-detector.ts +69 -0
- package/src/detectors/cold-start-threshold.detector.ts +95 -0
- package/src/detectors/dynamic-route-candidate.detector.ts +107 -0
- package/src/detectors/fetch-no-cache.detector.ts +204 -0
- package/src/detectors/index.ts +127 -0
- package/src/detectors/types.ts +38 -0
- package/src/index.ts +60 -0
- package/src/init.ts +424 -0
- package/src/middleware.ts +75 -0
- package/src/optimization.ts +164 -0
- package/src/system-monitor.ts +295 -0
- package/src/types.ts +66 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FetchNoCacheDetector } from '../fetch-no-cache.detector.js';
|
|
3
|
+
// Helper to create mock spans
|
|
4
|
+
function createMockSpan(overrides = {}) {
|
|
5
|
+
const defaultStartTime = [Math.floor(Date.now() / 1000), 0];
|
|
6
|
+
const defaultEndTime = [
|
|
7
|
+
defaultStartTime[0],
|
|
8
|
+
defaultStartTime[1] + 100_000_000, // 100ms
|
|
9
|
+
];
|
|
10
|
+
return {
|
|
11
|
+
name: 'fetch',
|
|
12
|
+
spanContext: () => ({
|
|
13
|
+
traceId: 'test-trace',
|
|
14
|
+
spanId: 'test-span-' + Math.random().toString(36).substr(2, 9),
|
|
15
|
+
traceFlags: 0x01,
|
|
16
|
+
traceState: undefined,
|
|
17
|
+
isRecording: true,
|
|
18
|
+
}),
|
|
19
|
+
parentSpanId: undefined,
|
|
20
|
+
startTime: defaultStartTime,
|
|
21
|
+
endTime: defaultEndTime,
|
|
22
|
+
attributes: {
|
|
23
|
+
'http.method': 'GET',
|
|
24
|
+
'http.url': 'https://api.example.com/data',
|
|
25
|
+
...overrides.attributes,
|
|
26
|
+
},
|
|
27
|
+
links: [],
|
|
28
|
+
events: [],
|
|
29
|
+
status: { code: 0 },
|
|
30
|
+
instrumentationLibrary: {
|
|
31
|
+
name: 'nextdoctor',
|
|
32
|
+
version: '0.1.0',
|
|
33
|
+
},
|
|
34
|
+
resource: {
|
|
35
|
+
attributes: {},
|
|
36
|
+
},
|
|
37
|
+
duration: defaultEndTime,
|
|
38
|
+
ended: true,
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
describe('FetchNoCacheDetector', () => {
|
|
43
|
+
const detector = new FetchNoCacheDetector();
|
|
44
|
+
it('detects GET fetch with no cache', () => {
|
|
45
|
+
const span = createMockSpan({
|
|
46
|
+
name: 'fetch',
|
|
47
|
+
attributes: {
|
|
48
|
+
'http.method': 'GET',
|
|
49
|
+
'http.url': 'https://api.example.com/users',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
53
|
+
expect(issues).toHaveLength(1);
|
|
54
|
+
expect(issues[0].id).toBe('FETCH_NO_CACHE');
|
|
55
|
+
expect(issues[0].severity).toBe('high');
|
|
56
|
+
expect(issues[0].message).toContain('no cache');
|
|
57
|
+
});
|
|
58
|
+
it('ignores POST fetch (no cache expected)', () => {
|
|
59
|
+
const span = createMockSpan({
|
|
60
|
+
name: 'fetch',
|
|
61
|
+
attributes: {
|
|
62
|
+
'http.method': 'POST',
|
|
63
|
+
'http.url': 'https://api.example.com/users',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
67
|
+
expect(issues).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
it('ignores fetch with force-cache', () => {
|
|
70
|
+
const span = createMockSpan({
|
|
71
|
+
name: 'fetch',
|
|
72
|
+
attributes: {
|
|
73
|
+
'http.method': 'GET',
|
|
74
|
+
'http.url': 'https://api.example.com/users',
|
|
75
|
+
'fetch.cache': 'force-cache',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
79
|
+
expect(issues).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
it('ignores fetch with next.revalidate', () => {
|
|
82
|
+
const span = createMockSpan({
|
|
83
|
+
name: 'fetch',
|
|
84
|
+
attributes: {
|
|
85
|
+
'http.method': 'GET',
|
|
86
|
+
'http.url': 'https://api.example.com/users',
|
|
87
|
+
'next.revalidate': 3600,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
91
|
+
expect(issues).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
it('ignores internal URLs (localhost)', () => {
|
|
94
|
+
const span = createMockSpan({
|
|
95
|
+
name: 'fetch',
|
|
96
|
+
attributes: {
|
|
97
|
+
'http.method': 'GET',
|
|
98
|
+
'http.url': 'http://localhost:3000/api/internal',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
102
|
+
expect(issues).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
it('ignores internal URLs (192.168)', () => {
|
|
105
|
+
const span = createMockSpan({
|
|
106
|
+
name: 'fetch',
|
|
107
|
+
attributes: {
|
|
108
|
+
'http.method': 'GET',
|
|
109
|
+
'http.url': 'http://192.168.1.1:3000/api',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
113
|
+
expect(issues).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
it('ignores fetch with duration < 50ms', () => {
|
|
116
|
+
const span = createMockSpan({
|
|
117
|
+
name: 'fetch',
|
|
118
|
+
attributes: {
|
|
119
|
+
'http.method': 'GET',
|
|
120
|
+
'http.url': 'https://api.example.com/users',
|
|
121
|
+
},
|
|
122
|
+
startTime: [1000, 0],
|
|
123
|
+
endTime: [1000, 10_000_000], // 10ms only
|
|
124
|
+
});
|
|
125
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
126
|
+
expect(issues).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
it('detects N+1 fetch pattern (>= 3 calls)', () => {
|
|
129
|
+
const url = 'https://api.example.com/user/123';
|
|
130
|
+
const spans = Array.from({ length: 3 }).map((_, i) => createMockSpan({
|
|
131
|
+
name: 'fetch',
|
|
132
|
+
attributes: {
|
|
133
|
+
'http.method': 'GET',
|
|
134
|
+
'http.url': url,
|
|
135
|
+
},
|
|
136
|
+
spanContext: () => ({
|
|
137
|
+
traceId: 'test-trace',
|
|
138
|
+
spanId: `span-${i}`,
|
|
139
|
+
traceFlags: 0x01,
|
|
140
|
+
traceState: undefined,
|
|
141
|
+
isRecording: true,
|
|
142
|
+
}),
|
|
143
|
+
}));
|
|
144
|
+
const issues = detector.detect(spans, { runtime: 'nodejs' });
|
|
145
|
+
expect(issues).toHaveLength(1);
|
|
146
|
+
expect(issues[0].id).toBe('FETCH_NO_CACHE');
|
|
147
|
+
expect(issues[0].severity).toBe('critical');
|
|
148
|
+
expect(issues[0].message).toContain('3x');
|
|
149
|
+
});
|
|
150
|
+
it('reports high severity when same URL is fetched exactly 2 times', () => {
|
|
151
|
+
const url = 'https://api.example.com/twice';
|
|
152
|
+
const spans = Array.from({ length: 2 }).map((_, i) => createMockSpan({
|
|
153
|
+
name: 'fetch',
|
|
154
|
+
attributes: {
|
|
155
|
+
'http.method': 'GET',
|
|
156
|
+
'http.url': url,
|
|
157
|
+
},
|
|
158
|
+
spanContext: () => ({
|
|
159
|
+
traceId: 'test-trace',
|
|
160
|
+
spanId: `span-${i}`,
|
|
161
|
+
traceFlags: 0x01,
|
|
162
|
+
traceState: undefined,
|
|
163
|
+
isRecording: true,
|
|
164
|
+
}),
|
|
165
|
+
}));
|
|
166
|
+
const issues = detector.detect(spans, { runtime: 'nodejs' });
|
|
167
|
+
expect(issues).toHaveLength(1);
|
|
168
|
+
expect(issues[0].id).toBe('FETCH_NO_CACHE');
|
|
169
|
+
expect(issues[0].severity).toBe('high');
|
|
170
|
+
expect(issues[0].attributes?.callCount).toBe(2);
|
|
171
|
+
});
|
|
172
|
+
it('includes URL and duration in attributes', () => {
|
|
173
|
+
const span = createMockSpan({
|
|
174
|
+
name: 'fetch',
|
|
175
|
+
attributes: {
|
|
176
|
+
'http.method': 'GET',
|
|
177
|
+
'http.url': 'https://api.example.com/slow',
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const issues = detector.detect([span], { runtime: 'nodejs' });
|
|
181
|
+
expect(issues[0].attributes?.url).toBe('https://api.example.com/slow');
|
|
182
|
+
expect(issues[0].attributes?.duration).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
it('includes route in issue if context provides it', () => {
|
|
185
|
+
const span = createMockSpan({
|
|
186
|
+
name: 'fetch',
|
|
187
|
+
attributes: {
|
|
188
|
+
'http.method': 'GET',
|
|
189
|
+
'http.url': 'https://api.example.com/data',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const issues = detector.detect([span], {
|
|
193
|
+
runtime: 'nodejs',
|
|
194
|
+
route: '/products',
|
|
195
|
+
});
|
|
196
|
+
expect(issues[0].route).toBe('/products');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
//# sourceMappingURL=fetch-no-cache.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-no-cache.test.js","sourceRoot":"","sources":["../../../src/detectors/__tests__/fetch-no-cache.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAErE,8BAA8B;AAC9B,SAAS,cAAc,CAAC,YAAmC,EAAE;IAC3D,MAAM,gBAAgB,GAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,MAAM,cAAc,GAAqB;QACvC,gBAAgB,CAAC,CAAC,CAAC;QACnB,gBAAgB,CAAC,CAAC,CAAC,GAAG,WAAW,EAAE,QAAQ;KAC5C,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;YAClB,OAAO,EAAE,YAAY;YACrB,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;YAC9D,UAAU,EAAE,IAAI;YAChB,UAAU,EAAE,SAAS;YACrB,WAAW,EAAE,IAAI;SAClB,CAAC;QACF,YAAY,EAAE,SAAS;QACvB,SAAS,EAAE,gBAAgB;QAC3B,OAAO,EAAE,cAAc;QACvB,UAAU,EAAE;YACV,aAAa,EAAE,KAAK;YACpB,UAAU,EAAE,8BAA8B;YAC1C,GAAG,SAAS,CAAC,UAAU;SACxB;QACD,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;QACnB,sBAAsB,EAAE;YACtB,IAAI,EAAE,YAAY;YAClB,OAAO,EAAE,OAAO;SACjB;QACD,QAAQ,EAAE;YACR,UAAU,EAAE,EAAE;SACf;QACD,QAAQ,EAAE,cAAc;QACxB,KAAK,EAAE,IAAI;QACX,GAAG,SAAS;KACG,CAAC;AACpB,CAAC;AAED,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,MAAM,QAAQ,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE5C,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,+BAA+B;aAC5C;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,MAAM;gBACrB,UAAU,EAAE,+BAA+B;aAC5C;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,+BAA+B;gBAC3C,aAAa,EAAE,aAAa;aAC7B;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,+BAA+B;gBAC3C,iBAAiB,EAAE,IAAI;aACxB;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,oCAAoC;aACjD;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,6BAA6B;aAC1C;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,+BAA+B;aAC5C;YACD,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,YAAY;SAC1C,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,GAAG,GAAG,kCAAkC,CAAC;QAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACnD,cAAc,CAAC;YACb,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,GAAG;aAChB;YACD,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;gBAClB,OAAO,EAAE,YAAY;gBACrB,MAAM,EAAE,QAAQ,CAAC,EAAE;gBACnB,UAAU,EAAE,IAAI;gBAChB,UAAU,EAAE,SAAS;gBACrB,WAAW,EAAE,IAAI;aAClB,CAAC;SACH,CAAC,CACH,CAAC;QAEF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE7D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,GAAG,GAAG,+BAA+B,CAAC;QAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACnD,cAAc,CAAC;YACb,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,GAAG;aAChB;YACD,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;gBAClB,OAAO,EAAE,YAAY;gBACrB,MAAM,EAAE,QAAQ,CAAC,EAAE;gBACnB,UAAU,EAAE,IAAI;gBAChB,UAAU,EAAE,SAAS;gBACrB,WAAW,EAAE,IAAI;aAClB,CAAC;SACH,CAAC,CACH,CAAC;QAEF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE7D,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,8BAA8B;aAC3C;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QACxE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,OAAO;YACb,UAAU,EAAE;gBACV,aAAa,EAAE,KAAK;gBACpB,UAAU,EAAE,8BAA8B;aAC3C;SACF,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE;YACrC,OAAO,EAAE,QAAQ;YACjB,KAAK,EAAE,WAAW;SACnB,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import type { DetectedIssue, DetectorContext, DetectorResult } from './types.js';
|
|
3
|
+
export declare abstract class BaseDetector {
|
|
4
|
+
abstract readonly id: string;
|
|
5
|
+
abstract readonly name: string;
|
|
6
|
+
abstract detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[];
|
|
7
|
+
run(spans: ReadableSpan[], context: DetectorContext): DetectorResult;
|
|
8
|
+
protected getSpanDurationMs(span: ReadableSpan): number;
|
|
9
|
+
protected getStringAttribute(span: ReadableSpan, key: string): string | undefined;
|
|
10
|
+
protected getNumberAttribute(span: ReadableSpan, key: string): number | undefined;
|
|
11
|
+
protected getSpanUrl(span: ReadableSpan): string | undefined;
|
|
12
|
+
protected getSpanMethod(span: ReadableSpan): string | undefined;
|
|
13
|
+
protected hasChildSpanWithName(parent: ReadableSpan, childName: string, allSpans: ReadableSpan[]): boolean;
|
|
14
|
+
protected getChildSpans(parent: ReadableSpan, allSpans: ReadableSpan[]): ReadableSpan[];
|
|
15
|
+
protected percentile(sortedAsc: number[], p: number): number;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=base-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-detector.d.ts","sourceRoot":"","sources":["../../src/detectors/base-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjF,8BAAsB,YAAY;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAE/B,QAAQ,CAAC,MAAM,CACb,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,EAAE,eAAe,GACvB,aAAa,EAAE;IAElB,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc;IAapE,SAAS,CAAC,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAMvD,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKjF,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKjF,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS;IAI5D,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS;IAI/D,SAAS,CAAC,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,OAAO;IAQ1G,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE;IAMvF,SAAS,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;CAK7D"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export class BaseDetector {
|
|
2
|
+
run(spans, context) {
|
|
3
|
+
const start = Date.now();
|
|
4
|
+
const issues = this.detect(spans, context);
|
|
5
|
+
const durationMs = Date.now() - start;
|
|
6
|
+
return {
|
|
7
|
+
issues,
|
|
8
|
+
detectorId: this.id,
|
|
9
|
+
analyzedSpans: spans.length,
|
|
10
|
+
durationMs,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
getSpanDurationMs(span) {
|
|
14
|
+
const [startSec, startNano] = span.startTime;
|
|
15
|
+
const [endSec, endNano] = span.endTime;
|
|
16
|
+
return (endSec - startSec) * 1000 + (endNano - startNano) / 1_000_000;
|
|
17
|
+
}
|
|
18
|
+
getStringAttribute(span, key) {
|
|
19
|
+
const val = span.attributes?.[key];
|
|
20
|
+
return typeof val === 'string' ? val : undefined;
|
|
21
|
+
}
|
|
22
|
+
getNumberAttribute(span, key) {
|
|
23
|
+
const val = span.attributes?.[key];
|
|
24
|
+
return typeof val === 'number' ? val : undefined;
|
|
25
|
+
}
|
|
26
|
+
getSpanUrl(span) {
|
|
27
|
+
return this.getStringAttribute(span, 'http.url');
|
|
28
|
+
}
|
|
29
|
+
getSpanMethod(span) {
|
|
30
|
+
return this.getStringAttribute(span, 'http.method');
|
|
31
|
+
}
|
|
32
|
+
hasChildSpanWithName(parent, childName, allSpans) {
|
|
33
|
+
const parentSpanId = parent.spanContext().spanId;
|
|
34
|
+
return allSpans.some(
|
|
35
|
+
// @ts-expect-error parentSpanId exists in newer OTel versions
|
|
36
|
+
s => s.parentSpanId === parentSpanId && s.name === childName);
|
|
37
|
+
}
|
|
38
|
+
getChildSpans(parent, allSpans) {
|
|
39
|
+
const parentSpanId = parent.spanContext().spanId;
|
|
40
|
+
// @ts-expect-error parentSpanId exists in newer OTel versions
|
|
41
|
+
return allSpans.filter(s => s.parentSpanId === parentSpanId);
|
|
42
|
+
}
|
|
43
|
+
percentile(sortedAsc, p) {
|
|
44
|
+
if (sortedAsc.length === 0)
|
|
45
|
+
return 0;
|
|
46
|
+
const idx = Math.ceil(sortedAsc.length * (p / 100)) - 1;
|
|
47
|
+
return sortedAsc[Math.max(0, idx)];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=base-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-detector.js","sourceRoot":"","sources":["../../src/detectors/base-detector.ts"],"names":[],"mappings":"AAGA,MAAM,OAAgB,YAAY;IAShC,GAAG,CAAC,KAAqB,EAAE,OAAwB;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QAEtC,OAAO;YACL,MAAM;YACN,UAAU,EAAE,IAAI,CAAC,EAAE;YACnB,aAAa,EAAE,KAAK,CAAC,MAAM;YAC3B,UAAU;SACX,CAAC;IACJ,CAAC;IAES,iBAAiB,CAAC,IAAkB;QAC5C,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC;QAC7C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QACvC,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACxE,CAAC;IAES,kBAAkB,CAAC,IAAkB,EAAE,GAAW;QAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;QACnC,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;IAES,kBAAkB,CAAC,IAAkB,EAAE,GAAW;QAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;QACnC,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;IAES,UAAU,CAAC,IAAkB;QACrC,OAAO,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACnD,CAAC;IAES,aAAa,CAAC,IAAkB;QACxC,OAAO,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACtD,CAAC;IAES,oBAAoB,CAAC,MAAoB,EAAE,SAAiB,EAAE,QAAwB;QAC9F,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC;QACjD,OAAO,QAAQ,CAAC,IAAI;QAClB,8DAA8D;QAC9D,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,CAC7D,CAAC;IACJ,CAAC;IAES,aAAa,CAAC,MAAoB,EAAE,QAAwB;QACpE,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC;QACjD,8DAA8D;QAC9D,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,YAAY,CAAC,CAAC;IAC/D,CAAC;IAES,UAAU,CAAC,SAAmB,EAAE,CAAS;QACjD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACxD,OAAO,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAE,CAAC;IACtC,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import { BaseDetector } from './base-detector.js';
|
|
3
|
+
import type { DetectedIssue, DetectorContext } from './types.js';
|
|
4
|
+
export declare class ColdStartThresholdDetector extends BaseDetector {
|
|
5
|
+
readonly id = "COLD_START_THRESHOLD";
|
|
6
|
+
readonly name = "Cold Start Threshold Detector";
|
|
7
|
+
private readonly threshold;
|
|
8
|
+
private readonly minSamplesForVariance;
|
|
9
|
+
detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[];
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=cold-start-threshold.detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cold-start-threshold.detector.d.ts","sourceRoot":"","sources":["../../src/detectors/cold-start-threshold.detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,qBAAa,0BAA2B,SAAQ,YAAY;IAC1D,QAAQ,CAAC,EAAE,0BAA0B;IACrC,QAAQ,CAAC,IAAI,mCAAmC;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAO;IACjC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAM;IAE5C,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,eAAe,GAAG,aAAa,EAAE;CAoFzE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BaseDetector } from './base-detector.js';
|
|
2
|
+
export class ColdStartThresholdDetector extends BaseDetector {
|
|
3
|
+
id = 'COLD_START_THRESHOLD';
|
|
4
|
+
name = 'Cold Start Threshold Detector';
|
|
5
|
+
threshold = 800; // ms
|
|
6
|
+
minSamplesForVariance = 20;
|
|
7
|
+
detect(spans, context) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
// Check startup time if provided
|
|
10
|
+
if (context.startupTimeMs && context.startupTimeMs > this.threshold) {
|
|
11
|
+
issues.push({
|
|
12
|
+
id: this.id,
|
|
13
|
+
severity: 'critical',
|
|
14
|
+
message: `Edge cold start ${context.startupTimeMs}ms > ${this.threshold}ms → users are paying this cost on every cold invocation`,
|
|
15
|
+
suggestion: `Options in order of impact:
|
|
16
|
+
|
|
17
|
+
1. Move heavy imports outside the handler:
|
|
18
|
+
// ❌ Inside the handler
|
|
19
|
+
export default async function handler(req, res) {
|
|
20
|
+
const { heavy } = await import('./heavy-lib');
|
|
21
|
+
return handleRequest(heavy, req);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ✅ Outside the handler
|
|
25
|
+
const heavyPromise = import('./heavy-lib');
|
|
26
|
+
export default async function handler(req, res) {
|
|
27
|
+
const { heavy } = await heavyPromise;
|
|
28
|
+
return handleRequest(heavy, req);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
2. Consider switching to Node.js runtime if Edge is not required:
|
|
32
|
+
export const runtime = 'nodejs';
|
|
33
|
+
|
|
34
|
+
3. Use route warming via a cron job to keep instances warm.`,
|
|
35
|
+
route: context.route,
|
|
36
|
+
attributes: {
|
|
37
|
+
startupTimeMs: context.startupTimeMs,
|
|
38
|
+
threshold: this.threshold,
|
|
39
|
+
runtime: context.runtime,
|
|
40
|
+
},
|
|
41
|
+
detectedAt: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Detect intermittent cold starts via span latency variance
|
|
45
|
+
const routeSpans = context.route
|
|
46
|
+
? spans.filter(s => s.attributes?.['http.route'] === context.route)
|
|
47
|
+
: spans.filter(s => s.attributes?.['http.route']);
|
|
48
|
+
if (routeSpans.length >= this.minSamplesForVariance) {
|
|
49
|
+
const durations = routeSpans.map(s => this.getSpanDurationMs(s));
|
|
50
|
+
const sorted = [...durations].sort((a, b) => a - b);
|
|
51
|
+
const p50 = this.percentile(sorted, 50);
|
|
52
|
+
const p99 = this.percentile(sorted, 99);
|
|
53
|
+
const variance = p99 - p50;
|
|
54
|
+
if (variance > 2000) {
|
|
55
|
+
issues.push({
|
|
56
|
+
id: 'COLD_START_INTERMITTENT',
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
message: `Route "${context.route || 'unknown'}" has high latency variance: P50=${Math.round(p50)}ms vs P99=${Math.round(p99)}ms → likely intermittent cold starts`,
|
|
59
|
+
suggestion: `A difference > 2000ms between P50 and P99 indicates periodic cold starts. Consider these strategies:
|
|
60
|
+
|
|
61
|
+
1. Keep-warm via external cron job (call your endpoint every 5 minutes)
|
|
62
|
+
2. Migrate to Node.js runtime if Edge Runtime is optional:
|
|
63
|
+
export const runtime = 'nodejs';
|
|
64
|
+
|
|
65
|
+
3. Use Next.js Middleware to keep the runtime warm — middleware runs on every
|
|
66
|
+
request and prevents full cold starts on subsequent edge invocations:
|
|
67
|
+
|
|
68
|
+
// middleware.ts
|
|
69
|
+
export const config = { matcher: '/api/:path*' };
|
|
70
|
+
export function middleware() {
|
|
71
|
+
// Intentionally lightweight — presence keeps runtime warm
|
|
72
|
+
}`,
|
|
73
|
+
route: context.route,
|
|
74
|
+
attributes: {
|
|
75
|
+
p50: Math.round(p50),
|
|
76
|
+
p99: Math.round(p99),
|
|
77
|
+
variance: Math.round(variance),
|
|
78
|
+
sampleCount: sorted.length,
|
|
79
|
+
},
|
|
80
|
+
detectedAt: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return issues;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=cold-start-threshold.detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cold-start-threshold.detector.js","sourceRoot":"","sources":["../../src/detectors/cold-start-threshold.detector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import { BaseDetector } from './base-detector.js';
|
|
3
|
+
import type { DetectedIssue, DetectorContext } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* DYNAMIC_ROUTE_CANDIDATE Detector
|
|
6
|
+
*
|
|
7
|
+
* Detects routes that call cookies() or headers() without reading any value,
|
|
8
|
+
* forcing unnecessary dynamic rendering.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ KNOWN LIMITATION: Next.js does not emit granular OTel spans for individual
|
|
11
|
+
* cookie key reads (e.g. cookies().get('key')). This detector uses a conservative
|
|
12
|
+
* heuristic: if the cookies/headers span has no child spans, the call is considered
|
|
13
|
+
* unused. This may produce false negatives (misses cases where a key is read) but
|
|
14
|
+
* avoids false positives (incorrectly flagging correct usage).
|
|
15
|
+
*
|
|
16
|
+
* Severity is 'info' until validated with real production traces.
|
|
17
|
+
*/
|
|
18
|
+
export declare class DynamicRouteCandidateDetector extends BaseDetector {
|
|
19
|
+
readonly id = "DYNAMIC_ROUTE_CANDIDATE";
|
|
20
|
+
readonly name = "Dynamic Route Candidate Detector";
|
|
21
|
+
detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[];
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=dynamic-route-candidate.detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-route-candidate.detector.d.ts","sourceRoot":"","sources":["../../src/detectors/dynamic-route-candidate.detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE;;;;;;;;;;;;;GAaG;AACH,qBAAa,6BAA8B,SAAQ,YAAY;IAC7D,QAAQ,CAAC,EAAE,6BAA6B;IACxC,QAAQ,CAAC,IAAI,sCAAsC;IAEnD,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,eAAe,GAAG,aAAa,EAAE;CAoFzE"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BaseDetector } from './base-detector.js';
|
|
2
|
+
/**
|
|
3
|
+
* DYNAMIC_ROUTE_CANDIDATE Detector
|
|
4
|
+
*
|
|
5
|
+
* Detects routes that call cookies() or headers() without reading any value,
|
|
6
|
+
* forcing unnecessary dynamic rendering.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ KNOWN LIMITATION: Next.js does not emit granular OTel spans for individual
|
|
9
|
+
* cookie key reads (e.g. cookies().get('key')). This detector uses a conservative
|
|
10
|
+
* heuristic: if the cookies/headers span has no child spans, the call is considered
|
|
11
|
+
* unused. This may produce false negatives (misses cases where a key is read) but
|
|
12
|
+
* avoids false positives (incorrectly flagging correct usage).
|
|
13
|
+
*
|
|
14
|
+
* Severity is 'info' until validated with real production traces.
|
|
15
|
+
*/
|
|
16
|
+
export class DynamicRouteCandidateDetector extends BaseDetector {
|
|
17
|
+
id = 'DYNAMIC_ROUTE_CANDIDATE';
|
|
18
|
+
name = 'Dynamic Route Candidate Detector';
|
|
19
|
+
detect(spans, context) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
if (!context.route) {
|
|
22
|
+
return issues;
|
|
23
|
+
}
|
|
24
|
+
// Find spans that indicate reading of cookies/headers
|
|
25
|
+
const cookiesSpans = spans.filter(s => s.name === 'cookies');
|
|
26
|
+
const headersSpans = spans.filter(s => s.name === 'headers');
|
|
27
|
+
const dynamicTriggerSpans = [...cookiesSpans, ...headersSpans];
|
|
28
|
+
dynamicTriggerSpans.forEach(span => {
|
|
29
|
+
const children = this.getChildSpans(span, spans);
|
|
30
|
+
// Conservative heuristic: if there are NO child spans at all,
|
|
31
|
+
// cookies()/headers() was called but nothing was read from it.
|
|
32
|
+
// If there ARE children, assume the value was used (safer default).
|
|
33
|
+
const hasAnyChildActivity = children.length > 0;
|
|
34
|
+
if (!hasAnyChildActivity) {
|
|
35
|
+
const spanName = span.name === 'cookies' ? 'cookies()' : 'headers()';
|
|
36
|
+
issues.push({
|
|
37
|
+
id: this.id,
|
|
38
|
+
severity: 'info',
|
|
39
|
+
message: `Route "${context.route}" may be unnecessarily dynamic: ${spanName} called with no subsequent reads detected`,
|
|
40
|
+
suggestion: `This route is paying the dynamic rendering cost without reading any actual values.
|
|
41
|
+
|
|
42
|
+
Option 1 — Remove the ${spanName}() call:
|
|
43
|
+
// ❌ Before
|
|
44
|
+
export default async function Page() {
|
|
45
|
+
const cookies = cookies(); // forces dynamic but never used
|
|
46
|
+
return <div>Static content</div>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ✅ After
|
|
50
|
+
export default function Page() {
|
|
51
|
+
return <div>Static content</div>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Option 2 — Move to a Server Action:
|
|
55
|
+
'use client';
|
|
56
|
+
import { getSessionCookie } from './actions';
|
|
57
|
+
|
|
58
|
+
export default function Page() {
|
|
59
|
+
const handleClick = async () => {
|
|
60
|
+
const session = await getSessionCookie();
|
|
61
|
+
// ...
|
|
62
|
+
};
|
|
63
|
+
return <button onClick={handleClick}>Click me</button>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// app/actions.ts
|
|
67
|
+
'use server';
|
|
68
|
+
import { cookies } from 'next/headers';
|
|
69
|
+
|
|
70
|
+
export async function getSessionCookie() {
|
|
71
|
+
const c = cookies();
|
|
72
|
+
return c.get('session')?.value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Option 3 — If the route is fully static and shouldn't trigger dynamic rendering:
|
|
76
|
+
export const dynamic = 'force-static';
|
|
77
|
+
|
|
78
|
+
export default function Page() {
|
|
79
|
+
// ... static content
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
See: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic`,
|
|
83
|
+
route: context.route,
|
|
84
|
+
spanId: span.spanContext().spanId,
|
|
85
|
+
attributes: {
|
|
86
|
+
trigger: span.name,
|
|
87
|
+
childrenCount: children.length,
|
|
88
|
+
},
|
|
89
|
+
detectedAt: Date.now(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return issues;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=dynamic-route-candidate.detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-route-candidate.detector.js","sourceRoot":"","sources":["../../src/detectors/dynamic-route-candidate.detector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,6BAA8B,SAAQ,YAAY;IACpD,EAAE,GAAG,yBAAyB,CAAC;IAC/B,IAAI,GAAG,kCAAkC,CAAC;IAEnD,MAAM,CAAC,KAAqB,EAAE,OAAwB;QACpD,MAAM,MAAM,GAAoB,EAAE,CAAC;QAEnC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,sDAAsD;QACtD,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QAC7D,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QAE7D,MAAM,mBAAmB,GAAG,CAAC,GAAG,YAAY,EAAE,GAAG,YAAY,CAAC,CAAC;QAE/D,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAEjD,8DAA8D;YAC9D,+DAA+D;YAC/D,oEAAoE;YACpE,MAAM,mBAAmB,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YAEhD,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC;gBAErE,MAAM,CAAC,IAAI,CAAC;oBACV,EAAE,EAAE,IAAI,CAAC,EAAE;oBACX,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,UAAU,OAAO,CAAC,KAAK,mCAAmC,QAAQ,2CAA2C;oBACtH,UAAU,EAAE;;wBAEE,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wFAwCwD;oBAC9E,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM;oBACjC,UAAU,EAAE;wBACV,OAAO,EAAE,IAAI,CAAC,IAAI;wBAClB,aAAa,EAAE,QAAQ,CAAC,MAAM;qBAC/B;oBACD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;iBACvB,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import { BaseDetector } from './base-detector.js';
|
|
3
|
+
import type { DetectedIssue, DetectorContext } from './types.js';
|
|
4
|
+
export declare class FetchNoCacheDetector extends BaseDetector {
|
|
5
|
+
readonly id = "FETCH_NO_CACHE";
|
|
6
|
+
readonly name = "Fetch Without Cache Detector";
|
|
7
|
+
private readonly minDurationMs;
|
|
8
|
+
private readonly nPlus1Threshold;
|
|
9
|
+
detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[];
|
|
10
|
+
private isInternalUrl;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=fetch-no-cache.detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-no-cache.detector.d.ts","sourceRoot":"","sources":["../../src/detectors/fetch-no-cache.detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAQjE,qBAAa,oBAAqB,SAAQ,YAAY;IACpD,QAAQ,CAAC,EAAE,oBAAoB;IAC/B,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IAErC,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,eAAe,GAAG,aAAa,EAAE;IA0KxE,OAAO,CAAC,aAAa;CAiBtB"}
|