@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.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +3 -0
  2. package/README.md +568 -0
  3. package/dist/detectors/__tests__/cold-start-threshold.test.d.ts +2 -0
  4. package/dist/detectors/__tests__/cold-start-threshold.test.d.ts.map +1 -0
  5. package/dist/detectors/__tests__/cold-start-threshold.test.js +156 -0
  6. package/dist/detectors/__tests__/cold-start-threshold.test.js.map +1 -0
  7. package/dist/detectors/__tests__/dynamic-route-candidate.test.d.ts +2 -0
  8. package/dist/detectors/__tests__/dynamic-route-candidate.test.d.ts.map +1 -0
  9. package/dist/detectors/__tests__/dynamic-route-candidate.test.js +318 -0
  10. package/dist/detectors/__tests__/dynamic-route-candidate.test.js.map +1 -0
  11. package/dist/detectors/__tests__/fetch-no-cache.test.d.ts +2 -0
  12. package/dist/detectors/__tests__/fetch-no-cache.test.d.ts.map +1 -0
  13. package/dist/detectors/__tests__/fetch-no-cache.test.js +199 -0
  14. package/dist/detectors/__tests__/fetch-no-cache.test.js.map +1 -0
  15. package/dist/detectors/base-detector.d.ts +17 -0
  16. package/dist/detectors/base-detector.d.ts.map +1 -0
  17. package/dist/detectors/base-detector.js +50 -0
  18. package/dist/detectors/base-detector.js.map +1 -0
  19. package/dist/detectors/cold-start-threshold.detector.d.ts +11 -0
  20. package/dist/detectors/cold-start-threshold.detector.d.ts.map +1 -0
  21. package/dist/detectors/cold-start-threshold.detector.js +87 -0
  22. package/dist/detectors/cold-start-threshold.detector.js.map +1 -0
  23. package/dist/detectors/dynamic-route-candidate.detector.d.ts +23 -0
  24. package/dist/detectors/dynamic-route-candidate.detector.d.ts.map +1 -0
  25. package/dist/detectors/dynamic-route-candidate.detector.js +96 -0
  26. package/dist/detectors/dynamic-route-candidate.detector.js.map +1 -0
  27. package/dist/detectors/fetch-no-cache.detector.d.ts +12 -0
  28. package/dist/detectors/fetch-no-cache.detector.d.ts.map +1 -0
  29. package/dist/detectors/fetch-no-cache.detector.js +178 -0
  30. package/dist/detectors/fetch-no-cache.detector.js.map +1 -0
  31. package/dist/detectors/index.d.ts +28 -0
  32. package/dist/detectors/index.d.ts.map +1 -0
  33. package/dist/detectors/index.js +97 -0
  34. package/dist/detectors/index.js.map +1 -0
  35. package/dist/detectors/types.d.ts +32 -0
  36. package/dist/detectors/types.d.ts.map +1 -0
  37. package/dist/detectors/types.js +2 -0
  38. package/dist/detectors/types.js.map +1 -0
  39. package/dist/index.d.ts +10 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +7 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/init.d.ts +133 -0
  44. package/dist/init.d.ts.map +1 -0
  45. package/dist/init.js +363 -0
  46. package/dist/init.js.map +1 -0
  47. package/dist/middleware.d.ts +10 -0
  48. package/dist/middleware.d.ts.map +1 -0
  49. package/dist/middleware.js +61 -0
  50. package/dist/middleware.js.map +1 -0
  51. package/dist/optimization.d.ts +43 -0
  52. package/dist/optimization.d.ts.map +1 -0
  53. package/dist/optimization.js +139 -0
  54. package/dist/optimization.js.map +1 -0
  55. package/dist/system-monitor.d.ts +124 -0
  56. package/dist/system-monitor.d.ts.map +1 -0
  57. package/dist/system-monitor.js +221 -0
  58. package/dist/system-monitor.js.map +1 -0
  59. package/dist/types.d.ts +61 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +14 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +55 -0
  64. package/src/detectors/__tests__/cold-start-threshold.test.ts +183 -0
  65. package/src/detectors/__tests__/dynamic-route-candidate.test.ts +365 -0
  66. package/src/detectors/__tests__/fetch-no-cache.test.ts +239 -0
  67. package/src/detectors/base-detector.ts +69 -0
  68. package/src/detectors/cold-start-threshold.detector.ts +95 -0
  69. package/src/detectors/dynamic-route-candidate.detector.ts +107 -0
  70. package/src/detectors/fetch-no-cache.detector.ts +204 -0
  71. package/src/detectors/index.ts +127 -0
  72. package/src/detectors/types.ts +38 -0
  73. package/src/index.ts +60 -0
  74. package/src/init.ts +424 -0
  75. package/src/middleware.ts +75 -0
  76. package/src/optimization.ts +164 -0
  77. package/src/system-monitor.ts +295 -0
  78. package/src/types.ts +66 -0
  79. package/tsconfig.json +11 -0
  80. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,95 @@
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
+ export class ColdStartThresholdDetector extends BaseDetector {
6
+ readonly id = 'COLD_START_THRESHOLD';
7
+ readonly name = 'Cold Start Threshold Detector';
8
+ private readonly threshold = 800; // ms
9
+ private readonly minSamplesForVariance = 20;
10
+
11
+ detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[] {
12
+ const issues: DetectedIssue[] = [];
13
+
14
+ // Check startup time if provided
15
+ if (context.startupTimeMs && context.startupTimeMs > this.threshold) {
16
+ issues.push({
17
+ id: this.id,
18
+ severity: 'critical',
19
+ message: `Edge cold start ${context.startupTimeMs}ms > ${this.threshold}ms → users are paying this cost on every cold invocation`,
20
+ suggestion: `Options in order of impact:
21
+
22
+ 1. Move heavy imports outside the handler:
23
+ // ❌ Inside the handler
24
+ export default async function handler(req, res) {
25
+ const { heavy } = await import('./heavy-lib');
26
+ return handleRequest(heavy, req);
27
+ }
28
+
29
+ // ✅ Outside the handler
30
+ const heavyPromise = import('./heavy-lib');
31
+ export default async function handler(req, res) {
32
+ const { heavy } = await heavyPromise;
33
+ return handleRequest(heavy, req);
34
+ }
35
+
36
+ 2. Consider switching to Node.js runtime if Edge is not required:
37
+ export const runtime = 'nodejs';
38
+
39
+ 3. Use route warming via a cron job to keep instances warm.`,
40
+ route: context.route,
41
+ attributes: {
42
+ startupTimeMs: context.startupTimeMs,
43
+ threshold: this.threshold,
44
+ runtime: context.runtime,
45
+ },
46
+ detectedAt: Date.now(),
47
+ });
48
+ }
49
+
50
+ // Detect intermittent cold starts via span latency variance
51
+ const routeSpans = context.route
52
+ ? spans.filter(s => s.attributes?.['http.route'] === context.route)
53
+ : spans.filter(s => s.attributes?.['http.route']);
54
+
55
+ if (routeSpans.length >= this.minSamplesForVariance) {
56
+ const durations = routeSpans.map(s => this.getSpanDurationMs(s));
57
+ const sorted = [...durations].sort((a, b) => a - b);
58
+ const p50 = this.percentile(sorted, 50);
59
+ const p99 = this.percentile(sorted, 99);
60
+ const variance = p99 - p50;
61
+
62
+ if (variance > 2000) {
63
+ issues.push({
64
+ id: 'COLD_START_INTERMITTENT',
65
+ severity: 'warning',
66
+ message: `Route "${context.route || 'unknown'}" has high latency variance: P50=${Math.round(p50)}ms vs P99=${Math.round(p99)}ms → likely intermittent cold starts`,
67
+ suggestion: `A difference > 2000ms between P50 and P99 indicates periodic cold starts. Consider these strategies:
68
+
69
+ 1. Keep-warm via external cron job (call your endpoint every 5 minutes)
70
+ 2. Migrate to Node.js runtime if Edge Runtime is optional:
71
+ export const runtime = 'nodejs';
72
+
73
+ 3. Use Next.js Middleware to keep the runtime warm — middleware runs on every
74
+ request and prevents full cold starts on subsequent edge invocations:
75
+
76
+ // middleware.ts
77
+ export const config = { matcher: '/api/:path*' };
78
+ export function middleware() {
79
+ // Intentionally lightweight — presence keeps runtime warm
80
+ }`,
81
+ route: context.route,
82
+ attributes: {
83
+ p50: Math.round(p50),
84
+ p99: Math.round(p99),
85
+ variance: Math.round(variance),
86
+ sampleCount: sorted.length,
87
+ },
88
+ detectedAt: Date.now(),
89
+ });
90
+ }
91
+ }
92
+
93
+ return issues;
94
+ }
95
+ }
@@ -0,0 +1,107 @@
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
+ /**
6
+ * DYNAMIC_ROUTE_CANDIDATE Detector
7
+ *
8
+ * Detects routes that call cookies() or headers() without reading any value,
9
+ * forcing unnecessary dynamic rendering.
10
+ *
11
+ * ⚠️ KNOWN LIMITATION: Next.js does not emit granular OTel spans for individual
12
+ * cookie key reads (e.g. cookies().get('key')). This detector uses a conservative
13
+ * heuristic: if the cookies/headers span has no child spans, the call is considered
14
+ * unused. This may produce false negatives (misses cases where a key is read) but
15
+ * avoids false positives (incorrectly flagging correct usage).
16
+ *
17
+ * Severity is 'info' until validated with real production traces.
18
+ */
19
+ export class DynamicRouteCandidateDetector extends BaseDetector {
20
+ readonly id = 'DYNAMIC_ROUTE_CANDIDATE';
21
+ readonly name = 'Dynamic Route Candidate Detector';
22
+
23
+ detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[] {
24
+ const issues: DetectedIssue[] = [];
25
+
26
+ if (!context.route) {
27
+ return issues;
28
+ }
29
+
30
+ // Find spans that indicate reading of cookies/headers
31
+ const cookiesSpans = spans.filter(s => s.name === 'cookies');
32
+ const headersSpans = spans.filter(s => s.name === 'headers');
33
+
34
+ const dynamicTriggerSpans = [...cookiesSpans, ...headersSpans];
35
+
36
+ dynamicTriggerSpans.forEach(span => {
37
+ const children = this.getChildSpans(span, spans);
38
+
39
+ // Conservative heuristic: if there are NO child spans at all,
40
+ // cookies()/headers() was called but nothing was read from it.
41
+ // If there ARE children, assume the value was used (safer default).
42
+ const hasAnyChildActivity = children.length > 0;
43
+
44
+ if (!hasAnyChildActivity) {
45
+ const spanName = span.name === 'cookies' ? 'cookies()' : 'headers()';
46
+
47
+ issues.push({
48
+ id: this.id,
49
+ severity: 'info',
50
+ message: `Route "${context.route}" may be unnecessarily dynamic: ${spanName} called with no subsequent reads detected`,
51
+ suggestion: `This route is paying the dynamic rendering cost without reading any actual values.
52
+
53
+ Option 1 — Remove the ${spanName}() call:
54
+ // ❌ Before
55
+ export default async function Page() {
56
+ const cookies = cookies(); // forces dynamic but never used
57
+ return <div>Static content</div>;
58
+ }
59
+
60
+ // ✅ After
61
+ export default function Page() {
62
+ return <div>Static content</div>;
63
+ }
64
+
65
+ Option 2 — Move to a Server Action:
66
+ 'use client';
67
+ import { getSessionCookie } from './actions';
68
+
69
+ export default function Page() {
70
+ const handleClick = async () => {
71
+ const session = await getSessionCookie();
72
+ // ...
73
+ };
74
+ return <button onClick={handleClick}>Click me</button>;
75
+ }
76
+
77
+ // app/actions.ts
78
+ 'use server';
79
+ import { cookies } from 'next/headers';
80
+
81
+ export async function getSessionCookie() {
82
+ const c = cookies();
83
+ return c.get('session')?.value;
84
+ }
85
+
86
+ Option 3 — If the route is fully static and shouldn't trigger dynamic rendering:
87
+ export const dynamic = 'force-static';
88
+
89
+ export default function Page() {
90
+ // ... static content
91
+ }
92
+
93
+ See: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic`,
94
+ route: context.route,
95
+ spanId: span.spanContext().spanId,
96
+ attributes: {
97
+ trigger: span.name,
98
+ childrenCount: children.length,
99
+ },
100
+ detectedAt: Date.now(),
101
+ });
102
+ }
103
+ });
104
+
105
+ return issues;
106
+ }
107
+ }
@@ -0,0 +1,204 @@
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
+ interface FetchInfo {
6
+ url: string;
7
+ duration: number;
8
+ spanId: string;
9
+ }
10
+
11
+ export class FetchNoCacheDetector extends BaseDetector {
12
+ readonly id = 'FETCH_NO_CACHE';
13
+ readonly name = 'Fetch Without Cache Detector';
14
+ private readonly minDurationMs = 50;
15
+ private readonly nPlus1Threshold = 3;
16
+
17
+ detect(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[] {
18
+ const issues: DetectedIssue[] = [];
19
+
20
+ // Find fetch spans without cache directive
21
+ const fetchSpans = spans.filter(span => {
22
+ const name = span.name.toLowerCase();
23
+ return name.includes('fetch') || name.includes('http.client');
24
+ });
25
+
26
+ const noCacheFetches: FetchInfo[] = [];
27
+ const urlCounts = new Map<string, FetchInfo[]>();
28
+
29
+ fetchSpans.forEach(span => {
30
+ const method = this.getSpanMethod(span);
31
+ const url = this.getSpanUrl(span);
32
+
33
+ // Ignore non-GET and non-POST (POST/PUT/DELETE shouldn't be cached anyway)
34
+ if (!method || !['GET', 'HEAD'].includes(method.toUpperCase())) {
35
+ return;
36
+ }
37
+
38
+ if (!url) {
39
+ return;
40
+ }
41
+
42
+ // Ignore internal URLs
43
+ if (this.isInternalUrl(url)) {
44
+ return;
45
+ }
46
+
47
+ // Check cache directive
48
+ const cacheAttr = this.getStringAttribute(span, 'fetch.cache');
49
+ const nextRevalidate = this.getNumberAttribute(span, 'next.revalidate');
50
+
51
+ // Has cache if:
52
+ // - has fetch.cache='force-cache' or 'force-revalidate'
53
+ // - has next.revalidate > 0
54
+ const hasCache =
55
+ cacheAttr === 'force-cache' ||
56
+ cacheAttr === 'force-revalidate' ||
57
+ (nextRevalidate !== undefined && nextRevalidate > 0);
58
+
59
+ if (hasCache) {
60
+ return;
61
+ }
62
+
63
+ const duration = this.getSpanDurationMs(span);
64
+ if (duration < this.minDurationMs) {
65
+ return;
66
+ }
67
+
68
+ const fetchInfo = {
69
+ url,
70
+ duration,
71
+ spanId: span.spanContext().spanId,
72
+ };
73
+
74
+ noCacheFetches.push(fetchInfo);
75
+
76
+ // Track for N+1 detection
77
+ const existing = urlCounts.get(url);
78
+ if (existing) {
79
+ existing.push(fetchInfo);
80
+ } else {
81
+ urlCounts.set(url, [fetchInfo]);
82
+ }
83
+ });
84
+
85
+ // Report individual fetches and group N+1s
86
+ urlCounts.forEach((fetches, url) => {
87
+ if (fetches.length >= this.nPlus1Threshold) {
88
+ // N+1 detected
89
+ issues.push({
90
+ id: this.id,
91
+ severity: 'critical',
92
+ message: `Fetch "${url}" called ${fetches.length}x with no cache → likely fetch N+1 pattern`,
93
+ suggestion: `This endpoint is being called multiple times without caching. Add a cache directive:
94
+
95
+ // Option 1: Force cache (recommended for static data)
96
+ fetch("${url}", { cache: 'force-cache' })
97
+
98
+ // Option 2: Revalidate after time (recommended for semi-dynamic data)
99
+ fetch("${url}", {
100
+ next: { revalidate: 3600 } // revalidate every hour
101
+ })
102
+
103
+ // Option 3: Use server-side data fetching instead
104
+ // Move to a parent Server Component or Route Handler
105
+
106
+ // Option 4: Use unstable_cache wrapper (experimental)
107
+ import { unstable_cache } from 'next/cache';
108
+ const cachedFetch = unstable_cache(
109
+ async () => fetch("${url}").then(r => r.json()),
110
+ [${JSON.stringify(url)}],
111
+ { revalidate: 3600 }
112
+ );`,
113
+ route: context.route,
114
+ spanId: fetches[0]?.spanId,
115
+ attributes: {
116
+ url,
117
+ callCount: fetches.length,
118
+ totalDuration: Math.round(fetches.reduce((sum, f) => sum + f.duration, 0)),
119
+ avgDuration: Math.round(
120
+ fetches.reduce((sum, f) => sum + f.duration, 0) / fetches.length
121
+ ),
122
+ },
123
+ detectedAt: Date.now(),
124
+ });
125
+ } else if (fetches.length === 1) {
126
+ // Single slow fetch without cache
127
+ const fetch = fetches[0]!;
128
+ issues.push({
129
+ id: this.id,
130
+ severity: 'high',
131
+ message: `Fetch "${url}" has no cache → +${Math.round(fetch.duration)}ms per request`,
132
+ suggestion: `Add a cache directive to this fetch:
133
+
134
+ // Option 1: Force cache (best for static APIs)
135
+ fetch("${url}", { cache: 'force-cache' })
136
+
137
+ // Option 2: Revalidate after time (better for semi-dynamic data)
138
+ fetch("${url}", {
139
+ next: { revalidate: 3600 } // revalidate every hour
140
+ })
141
+
142
+ // Option 3: Combine with other optimizations
143
+ const data = await fetch("${url}", {
144
+ cache: 'force-cache',
145
+ headers: {
146
+ 'User-Agent': 'NextDoctor-Agent/1.0'
147
+ }
148
+ });`,
149
+ route: context.route,
150
+ spanId: fetch.spanId,
151
+ attributes: {
152
+ url,
153
+ duration: Math.round(fetch.duration),
154
+ },
155
+ detectedAt: Date.now(),
156
+ });
157
+ } else {
158
+ // 2 to nPlus1Threshold-1 calls: still suspicious
159
+ const totalDuration = Math.round(fetches.reduce((sum, f) => sum + f.duration, 0));
160
+ issues.push({
161
+ id: this.id,
162
+ severity: 'high',
163
+ message: `Fetch "${url}" called ${fetches.length}x with no cache → ${totalDuration}ms wasted per request`,
164
+ suggestion: `This endpoint is being called multiple times without caching. Consider adding a cache directive or deduplicating the request:
165
+
166
+ // Option 1: Force cache
167
+ fetch("${url}", { cache: 'force-cache' })
168
+
169
+ // Option 2: Revalidate after time
170
+ fetch("${url}", { next: { revalidate: 3600 } })`,
171
+ route: context.route,
172
+ spanId: fetches[0]?.spanId,
173
+ attributes: {
174
+ url,
175
+ callCount: fetches.length,
176
+ totalDuration,
177
+ avgDuration: Math.round(totalDuration / fetches.length),
178
+ },
179
+ detectedAt: Date.now(),
180
+ });
181
+ }
182
+ });
183
+
184
+ return issues;
185
+ }
186
+
187
+ private isInternalUrl(url: string): boolean {
188
+ try {
189
+ const parsed = new URL(url);
190
+ const hostname = parsed.hostname;
191
+ return (
192
+ hostname === 'localhost' ||
193
+ hostname === '127.0.0.1' ||
194
+ hostname === '::1' ||
195
+ hostname.endsWith('.local') ||
196
+ hostname.endsWith('.test') ||
197
+ hostname.startsWith('192.168.') ||
198
+ hostname.startsWith('10.')
199
+ );
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+ }
@@ -0,0 +1,127 @@
1
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
2
+ import { BaseDetector } from './base-detector.js';
3
+ import { ColdStartThresholdDetector } from './cold-start-threshold.detector.js';
4
+ import { FetchNoCacheDetector } from './fetch-no-cache.detector.js';
5
+ import { DynamicRouteCandidateDetector } from './dynamic-route-candidate.detector.js';
6
+ import type { DetectedIssue, DetectorContext, IssueDeduplicationKey, DedupedIssue } from './types.js';
7
+
8
+ export class DetectionEngine {
9
+ private detectors: BaseDetector[];
10
+ private issueCache = new Map<string, DedupedIssue>();
11
+ private readonly dedupWindowMs = 60_000; // 60 seconds
12
+
13
+ constructor(config?: { disabledDetectors?: string[] }) {
14
+ const disabledSet = new Set(config?.disabledDetectors || []);
15
+
16
+ // Instantiate all detectors
17
+ const allDetectors: BaseDetector[] = [
18
+ new ColdStartThresholdDetector(),
19
+ new FetchNoCacheDetector(),
20
+ new DynamicRouteCandidateDetector(),
21
+ ];
22
+
23
+ this.detectors = allDetectors.filter(d => !disabledSet.has(d.id));
24
+ }
25
+
26
+ analyzeSpans(spans: ReadableSpan[], context: DetectorContext): DetectedIssue[] {
27
+ const allIssues: DetectedIssue[] = [];
28
+
29
+ // Run all detectors
30
+ for (const detector of this.detectors) {
31
+ const result = detector.run(spans, context);
32
+ allIssues.push(...result.issues);
33
+ }
34
+
35
+ // Deduplicate within the window
36
+ const deduped = this.deduplicateIssues(allIssues);
37
+
38
+ // Sort by severity: critical > high > warning > info
39
+ const severityOrder = { critical: 0, high: 1, warning: 2, info: 3 };
40
+ deduped.sort(
41
+ (a, b) =>
42
+ severityOrder[a.severity as keyof typeof severityOrder] -
43
+ severityOrder[b.severity as keyof typeof severityOrder]
44
+ );
45
+
46
+ return deduped;
47
+ }
48
+
49
+ private deduplicateIssues(issues: DetectedIssue[]): DetectedIssue[] {
50
+ const now = Date.now();
51
+ const dedupedMap = new Map<string, DedupedIssue>();
52
+
53
+ // Clean old entries from cache
54
+ for (const [key, cached] of this.issueCache.entries()) {
55
+ if (now - cached.lastDetectedAt > this.dedupWindowMs) {
56
+ this.issueCache.delete(key);
57
+ }
58
+ }
59
+
60
+ // Process new issues
61
+ for (const issue of issues) {
62
+ const key = this.getDeduplicationKey(issue);
63
+ const existingCached = this.issueCache.get(key);
64
+
65
+ if (existingCached) {
66
+ // Update cache
67
+ existingCached.lastDetectedAt = now;
68
+ existingCached.count++;
69
+ // Don't duplicate in output if already reported recently
70
+ continue;
71
+ }
72
+
73
+ // New issue - add to cache and output
74
+ const dedupedIssue: DedupedIssue = {
75
+ ...issue,
76
+ firstDetectedAt: now,
77
+ lastDetectedAt: now,
78
+ count: 1,
79
+ };
80
+
81
+ this.issueCache.set(key, dedupedIssue);
82
+ dedupedMap.set(key, dedupedIssue);
83
+ }
84
+
85
+ return Array.from(dedupedMap.values());
86
+ }
87
+
88
+ private getDeduplicationKey(issue: DetectedIssue): string {
89
+ const key: IssueDeduplicationKey = {
90
+ id: issue.id,
91
+ route: issue.route,
92
+ };
93
+ return JSON.stringify(key);
94
+ }
95
+
96
+ getDetectorById(id: string): BaseDetector | undefined {
97
+ return this.detectors.find(d => d.id === id);
98
+ }
99
+
100
+ listDetectors(): Array<{ id: string; name: string }> {
101
+ return this.detectors.map(d => ({
102
+ id: d.id,
103
+ name: d.name,
104
+ }));
105
+ }
106
+
107
+ clearCache(): void {
108
+ this.issueCache.clear();
109
+ }
110
+
111
+ getCacheSize(): number {
112
+ return this.issueCache.size;
113
+ }
114
+ }
115
+
116
+ // Singleton instance
117
+ // MVP LIMITATION: In persistent Node.js environments (self-hosted), this cache
118
+ // is shared globally. Deduplication keys include 'id' and 'route', but lack
119
+ // project-level scope, which could cause cache collisions in multi-tenant usages.
120
+ export const detectionEngine = new DetectionEngine();
121
+
122
+ // Re-export all types and detectors for public API
123
+ export type { DetectedIssue, DetectorContext, DetectorResult } from './types.js';
124
+ export { BaseDetector } from './base-detector.js';
125
+ export { ColdStartThresholdDetector } from './cold-start-threshold.detector.js';
126
+ export { FetchNoCacheDetector } from './fetch-no-cache.detector.js';
127
+ export { DynamicRouteCandidateDetector } from './dynamic-route-candidate.detector.js';
@@ -0,0 +1,38 @@
1
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
2
+
3
+ export type IssueSeverity = 'info' | 'warning' | 'high' | 'critical';
4
+
5
+ export interface DetectedIssue {
6
+ id: string;
7
+ severity: IssueSeverity;
8
+ message: string;
9
+ suggestion: string;
10
+ route?: string;
11
+ spanId?: string;
12
+ attributes?: Record<string, unknown>;
13
+ detectedAt: number;
14
+ }
15
+
16
+ export interface DetectorResult {
17
+ issues: DetectedIssue[];
18
+ detectorId: string;
19
+ analyzedSpans: number;
20
+ durationMs: number;
21
+ }
22
+
23
+ export interface DetectorContext {
24
+ route?: string;
25
+ runtime: 'nodejs' | 'edge';
26
+ startupTimeMs?: number;
27
+ }
28
+
29
+ export interface IssueDeduplicationKey {
30
+ id: string;
31
+ route?: string;
32
+ }
33
+
34
+ export interface DedupedIssue extends DetectedIssue {
35
+ firstDetectedAt: number;
36
+ lastDetectedAt: number;
37
+ count: number;
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ export {
2
+ initNextDoctor,
3
+ shutdownNextDoctor,
4
+ getNextDoctorAgent,
5
+ reportMetric,
6
+ getHealthStatus,
7
+ getDetectedIssues,
8
+ getSystemMetrics,
9
+ getSystemHealth,
10
+ getSystemSummary,
11
+ } from './init.js';
12
+
13
+ export type {
14
+ NextDoctorConfig,
15
+ DetectedIssue,
16
+ AgentHealth,
17
+ RetryPolicy,
18
+ ExporterConfig,
19
+ } from './types.js';
20
+
21
+ export {
22
+ LogLevel,
23
+ ExporterType,
24
+ } from './types.js';
25
+
26
+ export {
27
+ withNextDoctorMonitoring,
28
+ withNextDoctorTiming,
29
+ } from './middleware.js';
30
+
31
+ export {
32
+ IntelligentSampler,
33
+ BatchProcessor,
34
+ CircuitBreaker,
35
+ } from './optimization.js';
36
+
37
+ export {
38
+ SystemMonitor,
39
+ CPUMonitor,
40
+ MemoryMonitor,
41
+ } from './system-monitor.js';
42
+
43
+ export type {
44
+ CPUMetrics,
45
+ MemoryMetrics,
46
+ SystemMetrics,
47
+ } from './system-monitor.js';
48
+
49
+ export {
50
+ detectionEngine,
51
+ ColdStartThresholdDetector,
52
+ FetchNoCacheDetector,
53
+ DynamicRouteCandidateDetector,
54
+ } from './detectors/index.js';
55
+
56
+ export type {
57
+ DetectorContext,
58
+ DetectorResult,
59
+ IssueSeverity,
60
+ } from './detectors/types.js';