@debugg-ai/debugg-ai-mcp 2.3.0 → 2.3.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.
|
@@ -14,10 +14,9 @@ import { detectRepoName } from '../utils/gitContext.js';
|
|
|
14
14
|
import { tunnelManager } from '../services/ngrok/tunnelManager.js';
|
|
15
15
|
import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js';
|
|
16
16
|
import { extractLocalhostPort } from '../utils/urlParser.js';
|
|
17
|
+
import { getCachedTemplateUuid, getCachedProjectUuid, invalidateTemplateCache, invalidateProjectCache, } from '../utils/handlerCaches.js';
|
|
17
18
|
const logger = new Logger({ module: 'testPageChangesHandler' });
|
|
18
|
-
|
|
19
|
-
let cachedTemplateUuid = null;
|
|
20
|
-
const projectUuidCache = new Map();
|
|
19
|
+
const TEMPLATE_NAME = 'app evaluation';
|
|
21
20
|
// Concurrency control — max 2 simultaneous browser checks.
|
|
22
21
|
// Additional requests queue and run when a slot opens.
|
|
23
22
|
const MAX_CONCURRENT = 2;
|
|
@@ -177,41 +176,33 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
178
|
}
|
|
180
|
-
// ---
|
|
179
|
+
// --- Resolve template + project in parallel (both independent post-tunnel) ---
|
|
181
180
|
if (progressCallback) {
|
|
182
181
|
await progressCallback({ progress: 2, total: TOTAL_STEPS, message: 'Locating evaluation workflow template...' });
|
|
183
182
|
}
|
|
184
|
-
if (!cachedTemplateUuid) {
|
|
185
|
-
const template = await client.workflows.findEvaluationTemplate();
|
|
186
|
-
if (!template) {
|
|
187
|
-
throw new Error('App Evaluation Workflow Template not found. ' +
|
|
188
|
-
'Ensure the template is seeded in the backend (GET /api/v1/workflows/?is_template=true).');
|
|
189
|
-
}
|
|
190
|
-
cachedTemplateUuid = template.uuid;
|
|
191
|
-
logger.info(`Using workflow template: ${template.name} (${template.uuid})`);
|
|
192
|
-
}
|
|
193
|
-
// --- Resolve project UUID (best-effort, non-blocking) ---
|
|
194
|
-
// Use explicit repoName if provided, otherwise auto-detect from git remote
|
|
195
183
|
const repoName = input.repoName || detectRepoName();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
projectUuidCache.set(repoName, projectUuid);
|
|
205
|
-
logger.info(`Resolved project: ${project.name} (${project.uuid})`);
|
|
184
|
+
const [templateUuid, projectUuid] = await Promise.all([
|
|
185
|
+
getCachedTemplateUuid(TEMPLATE_NAME, async () => {
|
|
186
|
+
return client.workflows.findEvaluationTemplate();
|
|
187
|
+
}),
|
|
188
|
+
repoName
|
|
189
|
+
? getCachedProjectUuid(repoName, async (repo) => {
|
|
190
|
+
try {
|
|
191
|
+
return await client.findProjectByRepoName(repo);
|
|
206
192
|
}
|
|
207
|
-
|
|
208
|
-
logger.
|
|
193
|
+
catch (err) {
|
|
194
|
+
logger.warn(`Failed to look up project for repo "${repo}": ${err}`);
|
|
195
|
+
return null;
|
|
209
196
|
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
197
|
+
})
|
|
198
|
+
: Promise.resolve(undefined),
|
|
199
|
+
]);
|
|
200
|
+
if (!templateUuid) {
|
|
201
|
+
throw new Error('App Evaluation Workflow Template not found. ' +
|
|
202
|
+
'Ensure the template is seeded in the backend (GET /api/v1/workflows/?is_template=true).');
|
|
203
|
+
}
|
|
204
|
+
if (repoName && !projectUuid) {
|
|
205
|
+
logger.info(`No project found for repo "${repoName}" — proceeding without project_id`);
|
|
215
206
|
}
|
|
216
207
|
// --- Build context data (camelCase here — axiosTransport auto-converts to snake_case) ---
|
|
217
208
|
const contextData = {
|
|
@@ -238,7 +229,7 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
238
229
|
if (progressCallback) {
|
|
239
230
|
await progressCallback({ progress: 3, total: TOTAL_STEPS, message: 'Queuing workflow execution...' });
|
|
240
231
|
}
|
|
241
|
-
const executeResponse = await client.workflows.executeWorkflow(
|
|
232
|
+
const executeResponse = await client.workflows.executeWorkflow(templateUuid, contextData, Object.keys(env).length > 0 ? env : undefined);
|
|
242
233
|
const executionUuid = executeResponse.executionUuid;
|
|
243
234
|
logger.info(`Execution queued: ${executionUuid}`);
|
|
244
235
|
// --- Poll ---
|
|
@@ -475,7 +466,8 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
475
466
|
const duration = Date.now() - startTime;
|
|
476
467
|
logger.toolError('check_app_in_browser', error, duration);
|
|
477
468
|
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('401'))) {
|
|
478
|
-
|
|
469
|
+
invalidateTemplateCache();
|
|
470
|
+
invalidateProjectCache();
|
|
479
471
|
}
|
|
480
472
|
throw handleExternalServiceError(error, 'DebuggAI', 'test execution');
|
|
481
473
|
}
|
|
@@ -19,6 +19,7 @@ import { tunnelManager } from '../services/ngrok/tunnelManager.js';
|
|
|
19
19
|
import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js';
|
|
20
20
|
import { extractLocalhostPort } from '../utils/urlParser.js';
|
|
21
21
|
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
|
|
22
|
+
import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
|
|
22
23
|
const logger = new Logger({ module: 'triggerCrawlHandler' });
|
|
23
24
|
const TEMPLATE_KEYWORD = 'raw crawl';
|
|
24
25
|
export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
@@ -114,17 +115,17 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
|
-
// --- Find the crawl workflow template ---
|
|
118
|
+
// --- Find the crawl workflow template (cached across calls) ---
|
|
118
119
|
if (progressCallback) {
|
|
119
120
|
await progressCallback({ progress: 2, total: 4, message: 'Locating crawl workflow template...' });
|
|
120
121
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
122
|
+
const templateUuid = await getCachedTemplateUuid(TEMPLATE_KEYWORD, async (name) => {
|
|
123
|
+
return client.workflows.findTemplateByName(name);
|
|
124
|
+
});
|
|
125
|
+
if (!templateUuid) {
|
|
123
126
|
throw new Error(`Raw Crawl Workflow Template not found. ` +
|
|
124
127
|
`Ensure the backend has a template matching "${TEMPLATE_KEYWORD}" seeded and accessible.`);
|
|
125
128
|
}
|
|
126
|
-
const templateUuid = template.uuid;
|
|
127
|
-
logger.info(`Using crawl template: ${template.name} (${templateUuid})`);
|
|
128
129
|
// --- Build contextData + env ---
|
|
129
130
|
const contextData = {
|
|
130
131
|
targetUrl: ctx.targetUrl ?? ctx.originalUrl,
|
|
@@ -242,6 +243,9 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
|
|
|
242
243
|
catch (error) {
|
|
243
244
|
const duration = Date.now() - startTime;
|
|
244
245
|
logger.toolError('trigger_crawl', error, duration);
|
|
246
|
+
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('401'))) {
|
|
247
|
+
invalidateTemplateCache();
|
|
248
|
+
}
|
|
245
249
|
throw handleExternalServiceError(error, 'DebuggAI', 'crawl execution');
|
|
246
250
|
}
|
|
247
251
|
finally {
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
|
|
6
6
|
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
7
|
-
|
|
7
|
+
// Exponential backoff polling: short executions (10-15s crawls) detect terminal
|
|
8
|
+
// status quickly via the early polls; long executions (60-150s browser runs)
|
|
9
|
+
// avoid hammering the backend with 20+ roundtrips. Cap at 5s so we never wait
|
|
10
|
+
// more than 5s past terminal-state achievement.
|
|
11
|
+
const POLL_INTERVAL_INITIAL_MS = 1000;
|
|
12
|
+
const POLL_INTERVAL_MAX_MS = 5000;
|
|
13
|
+
const POLL_BACKOFF_MULTIPLIER = 1.5;
|
|
8
14
|
const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
|
|
9
15
|
export const createWorkflowsService = (tx) => {
|
|
10
16
|
const service = {
|
|
@@ -82,6 +88,7 @@ export const createWorkflowsService = (tx) => {
|
|
|
82
88
|
const deadline = Date.now() + EXECUTION_TIMEOUT_MS;
|
|
83
89
|
const pollStart = Date.now();
|
|
84
90
|
let pollCount = 0;
|
|
91
|
+
let intervalMs = POLL_INTERVAL_INITIAL_MS;
|
|
85
92
|
while (Date.now() < deadline) {
|
|
86
93
|
if (signal?.aborted) {
|
|
87
94
|
throw new Error(`Polling cancelled for execution ${executionUuid}`);
|
|
@@ -99,6 +106,7 @@ export const createWorkflowsService = (tx) => {
|
|
|
99
106
|
stepsTaken: execution.state?.stepsTaken ?? 0,
|
|
100
107
|
durationMs: Date.now() - pollStart,
|
|
101
108
|
pollCount,
|
|
109
|
+
finalIntervalMs: intervalMs,
|
|
102
110
|
});
|
|
103
111
|
return execution;
|
|
104
112
|
}
|
|
@@ -106,8 +114,9 @@ export const createWorkflowsService = (tx) => {
|
|
|
106
114
|
if (signal?.aborted) {
|
|
107
115
|
throw new Error(`Polling cancelled for execution ${executionUuid}`);
|
|
108
116
|
}
|
|
117
|
+
const sleepMs = intervalMs;
|
|
109
118
|
await new Promise((resolve, reject) => {
|
|
110
|
-
const timer = setTimeout(resolve,
|
|
119
|
+
const timer = setTimeout(resolve, sleepMs);
|
|
111
120
|
if (signal) {
|
|
112
121
|
const onAbort = () => { clearTimeout(timer); reject(new Error(`Polling cancelled for execution ${executionUuid}`)); };
|
|
113
122
|
if (signal.aborted) {
|
|
@@ -118,6 +127,9 @@ export const createWorkflowsService = (tx) => {
|
|
|
118
127
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
128
|
}
|
|
120
129
|
});
|
|
130
|
+
// Backoff for next iteration — capped at MAX so we don't wait too long
|
|
131
|
+
// past terminal-state achievement on the longest runs.
|
|
132
|
+
intervalMs = Math.min(Math.round(intervalMs * POLL_BACKOFF_MULTIPLIER), POLL_INTERVAL_MAX_MS);
|
|
121
133
|
}
|
|
122
134
|
throw new Error(`Execution ${executionUuid} timed out after ${EXECUTION_TIMEOUT_MS / 1000}s`);
|
|
123
135
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, process-scoped caches for handler hot-path lookups.
|
|
3
|
+
*
|
|
4
|
+
* Replaces per-handler `let cachedX: string | null = null` singletons with
|
|
5
|
+
* a single source of truth. Two handlers (testPageChanges + triggerCrawl)
|
|
6
|
+
* resolve the same workflow templates and the same project UUIDs; sharing
|
|
7
|
+
* the cache avoids duplicate backend roundtrips when the user invokes both.
|
|
8
|
+
*
|
|
9
|
+
* Each lookup emits a telemetry event with `hit: boolean` so the cache
|
|
10
|
+
* effectiveness is observable in PostHog without log-grepping.
|
|
11
|
+
*
|
|
12
|
+
* Caches are NEVER persisted across processes. Cleared explicitly by the
|
|
13
|
+
* handlers on auth failures (so a rotated API key doesn't keep serving
|
|
14
|
+
* stale UUIDs).
|
|
15
|
+
*/
|
|
16
|
+
import { Telemetry, TelemetryEvents } from './telemetry.js';
|
|
17
|
+
const templateUuidByName = new Map();
|
|
18
|
+
const projectUuidByRepo = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* Get a workflow template UUID by name, populating the cache on miss.
|
|
21
|
+
* Returns null if the lookup function returns null (template doesn't exist).
|
|
22
|
+
*/
|
|
23
|
+
export async function getCachedTemplateUuid(name, lookup) {
|
|
24
|
+
const cached = templateUuidByName.get(name);
|
|
25
|
+
if (cached) {
|
|
26
|
+
Telemetry.capture(TelemetryEvents.TEMPLATE_LOOKUP, { templateName: name, hit: true });
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
const t0 = Date.now();
|
|
30
|
+
const template = await lookup(name);
|
|
31
|
+
Telemetry.capture(TelemetryEvents.TEMPLATE_LOOKUP, {
|
|
32
|
+
templateName: name,
|
|
33
|
+
hit: false,
|
|
34
|
+
durationMs: Date.now() - t0,
|
|
35
|
+
found: !!template,
|
|
36
|
+
});
|
|
37
|
+
if (!template)
|
|
38
|
+
return null;
|
|
39
|
+
templateUuidByName.set(name, template.uuid);
|
|
40
|
+
return template.uuid;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get a project UUID by repo name, populating the cache on miss.
|
|
44
|
+
* Returns undefined if the lookup function returns null (no matching project).
|
|
45
|
+
*/
|
|
46
|
+
export async function getCachedProjectUuid(repoName, lookup) {
|
|
47
|
+
const cached = projectUuidByRepo.get(repoName);
|
|
48
|
+
if (cached) {
|
|
49
|
+
Telemetry.capture(TelemetryEvents.PROJECT_LOOKUP, { repoName, hit: true });
|
|
50
|
+
return cached;
|
|
51
|
+
}
|
|
52
|
+
const t0 = Date.now();
|
|
53
|
+
const project = await lookup(repoName);
|
|
54
|
+
Telemetry.capture(TelemetryEvents.PROJECT_LOOKUP, {
|
|
55
|
+
repoName,
|
|
56
|
+
hit: false,
|
|
57
|
+
durationMs: Date.now() - t0,
|
|
58
|
+
found: !!project,
|
|
59
|
+
});
|
|
60
|
+
if (!project)
|
|
61
|
+
return undefined;
|
|
62
|
+
projectUuidByRepo.set(repoName, project.uuid);
|
|
63
|
+
return project.uuid;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Clear the template cache. Called by handlers on auth failures.
|
|
67
|
+
*/
|
|
68
|
+
export function invalidateTemplateCache() {
|
|
69
|
+
templateUuidByName.clear();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Clear the project cache. Same trigger as templates.
|
|
73
|
+
*/
|
|
74
|
+
export function invalidateProjectCache() {
|
|
75
|
+
projectUuidByRepo.clear();
|
|
76
|
+
}
|
package/dist/utils/telemetry.js
CHANGED