Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/lib/agent-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export const isRetryableUpgradeError = (err: unknown): boolean => {
};

const sessions = new Map<string, ActiveSession>();
const createdAt = new WeakMap<ActiveSession, number>();
// In-flight session creations keyed by session key. Concurrent
// getOrCreateSession callers await the same promise instead of each
// opening their own WebSocket.
Expand Down Expand Up @@ -730,6 +731,7 @@ export const getOrCreateSession = async (
os?: string,
humanlike?: boolean,
record?: boolean,
onSession?: (reused: boolean, ageMs: number) => void,
): Promise<ActiveSession> => {
sweepSessions();
// Reusing on a bare call guessed "same task" — but every concurrent task in a
Expand Down Expand Up @@ -758,6 +760,7 @@ export const getOrCreateSession = async (
) {
assertCompatibleRecordingMode(existing, record);
existing.lastUsedAt = Date.now();
onSession?.(true, Math.max(0, Date.now() - createdAt.get(existing)!));
return existing;
}

Expand All @@ -766,6 +769,7 @@ export const getOrCreateSession = async (
if (inFlight) {
const session = await inFlight;
assertCompatibleRecordingMode(session, record);
onSession?.(true, Math.max(0, Date.now() - createdAt.get(session)!));
return session;
}

Expand Down Expand Up @@ -827,6 +831,7 @@ export const getOrCreateSession = async (
skillState: createSkillState(),
lastUsedAt: Date.now(),
};
createdAt.set(session, Date.now());

// Auto-cleanup on close
ws.on('close', (code: number, reason: Buffer) => {
Expand All @@ -848,7 +853,9 @@ export const getOrCreateSession = async (

pending.set(key, creation);
try {
return await creation;
const session = await creation;
onSession?.(false, 0);
return session;
} finally {
// Clear the placeholder whether connect succeeded or threw, so a failed
// attempt doesn't block future retries.
Expand All @@ -863,6 +870,7 @@ export const send = async (
method: string,
params: Record<string, unknown> = {},
timeoutMs?: number,
onSession?: (reused: boolean, ageMs: number) => void,
): Promise<AgentResponse> => {
if (session.ws.readyState !== WebSocket.OPEN) {
if (!session.reconnecting) {
Expand Down Expand Up @@ -890,6 +898,7 @@ export const send = async (
if (session.ws !== ws) {
session.ws = ws;
session.msgId = 0;
createdAt.set(session, Date.now());

const key = [...sessions.entries()].find(([, s]) => s === session)?.[0];
if (key) {
Expand All @@ -901,6 +910,7 @@ export const send = async (
});
}
}
onSession?.(false, 0);
}

session.msgId++;
Expand All @@ -922,6 +932,7 @@ export const closeSession = (
echoedSessionId?: string,
integrationId?: string,
allowedDomains?: string[],
onSession?: (reused: boolean, ageMs: number) => void,
): void => {
const key = getSessionKey(
mcpSessionId,
Expand All @@ -936,6 +947,7 @@ export const closeSession = (
);
const session = sessions.get(key);
if (session) {
onSession?.(true, Math.max(0, Date.now() - createdAt.get(session)!));
try {
session.ws.close();
} catch {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/define-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface ToolDefinition<P, R> {
description: string;
parameters: ZodType<P>;
annotations?: ToolAnnotations;
/** Defaults also included when validation fails before run(). */
analyticsDefaults?: Record<string, unknown>;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/** Throw UserError if any URL in params is invalid. Runs before progress 0. */
validateUrl?: (params: P) => void;
/** Override the default ProfileNotFoundError → UserError message. */
Expand Down Expand Up @@ -204,6 +206,7 @@ export function defineTool<P, R>(
}
}
return {
...def.analyticsDefaults,
...cleanProps,
success,
duration_ms: Date.now() - startedAt,
Expand Down
45 changes: 42 additions & 3 deletions src/tools/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ export function registerAgentTools(

defineTool<AgentToolParams, Content[]>(server, config, analytics, {
name: 'browserless_agent',
analyticsDefaults: { session_reused: false, session_age_ms: 0 },
description:
(compliant
? COMPLIANT_AGENT_SYSTEM_PROMPT
Expand Down Expand Up @@ -753,12 +754,22 @@ export function registerAgentTools(
}

let lastCategory: ErrorCategory | undefined;
let liveUrlId: string | undefined;
let sessionReused = false;
let sessionAgeMs = 0;
const onSession = (reused: boolean, ageMs: number) => {
sessionReused = reused;
sessionAgeMs = ageMs;
};
let lastFailure: Record<string, unknown> | undefined;

const sendAnalytics = (success: boolean, err?: unknown) => {
analytics?.fireToolRequest(token, 'browserless_agent', {
...mcpSource,
...(prompt ? { _prompt: prompt } : {}),
...(liveUrlId ? { live_url_id: liveUrlId } : {}),
session_reused: sessionReused,
session_age_ms: sessionAgeMs,
methods: commands.map((c) => c.method).join(','),
command_count: commands.length,
api_url: apiUrl,
Expand Down Expand Up @@ -810,6 +821,7 @@ export function registerAgentTools(
echoedSessionId,
integrationId,
allowedDomains,
onSession,
);
sendAnalytics(true);
return [{ type: 'text' as const, text: 'Browser session closed.' }];
Expand Down Expand Up @@ -838,6 +850,7 @@ export function registerAgentTools(
os,
humanlike,
record,
onSession,
);
} catch (connErr: unknown) {
lastFailure = failureDetails(connErr, {
Expand All @@ -857,6 +870,7 @@ export function registerAgentTools(
}

const runCommands = async (isRetry: boolean): Promise<Content[]> => {
onSession(false, 0);
lastFailure = undefined;
lastCategory = undefined;
let agentSession;
Expand All @@ -877,6 +891,7 @@ export function registerAgentTools(
os,
humanlike,
record,
onSession,
);
} catch (connErr: unknown) {
// No retry when the server gave a definitive 4xx — re-attempting
Expand Down Expand Up @@ -955,7 +970,13 @@ export function registerAgentTools(
cmd.method === 'reportOutcome'
) {
try {
await send(agentSession, cmd.method, cmd.params);
await send(
agentSession,
cmd.method,
cmd.params,
undefined,
onSession,
);
} catch {
// noop
}
Expand Down Expand Up @@ -984,7 +1005,13 @@ export function registerAgentTools(

let resp;
try {
resp = await send(agentSession, cmd.method, outboundParams);
resp = await send(
agentSession,
cmd.method,
outboundParams,
undefined,
onSession,
);
} catch (sendErr: unknown) {
destroySession(
mcpSessionId,
Expand Down Expand Up @@ -1127,6 +1154,12 @@ export function registerAgentTools(
);
}

if (cmd.method === 'liveURL') {
const result = resp.result as { liveURLId?: unknown } | undefined;
if (typeof result?.liveURLId === 'string') {
liveUrlId = result.liveURLId;
}
}
results.push({ ...cmd, result: resp.result });
}

Expand Down Expand Up @@ -1184,7 +1217,13 @@ export function registerAgentTools(
let autoDownloads: DownloadEntry[] = [];
if (!closedDuringBatch && last.method !== 'getDownloads') {
try {
const dl = await send(agentSession, 'getDownloads', {});
const dl = await send(
agentSession,
'getDownloads',
{},
undefined,
onSession,
);
autoDownloads =
(dl.result as { downloads?: DownloadEntry[] } | undefined)
?.downloads ?? [];
Expand Down
26 changes: 26 additions & 0 deletions test/lib/agent-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,40 @@ import {
sessionHandle,
dropMcpSession,
UpgradeError,
send,
} from '../../src/lib/agent-client.js';
import type { ProxyOptions } from '../../src/@types/types.js';
import {
makeAcceptingServer,
makeRejectingServer,
makeStallingServer,
makeRespondingServer,
} from '../helpers/upgrade-server.js';

describe('agent-client reconnection telemetry', () => {
afterEach(() => sinon.restore());

it('reports a fresh connection when send reconnects an acquired session', async () => {
const clock = sinon.useFakeTimers({ now: 1000, toFake: ['Date'] });
const server = await makeRespondingServer(() => ({}));
try {
const session = await getOrCreateSession(
'reconnect-telemetry',
server.url,
'tok',
);
session.ws.terminate();
clock.setSystemTime(9000);
const acquired = sinon.spy();
await send(session, 'getCookies', {}, undefined, acquired);
expect(acquired.calledOnceWithExactly(false, 0)).to.equal(true);
expect(server.hits()).to.equal(2);
} finally {
await server.close();
}
});
});

describe('agent-client buildAgentWsUrl', () => {
// A base carrying a query used to concatenate into path `/` — the raw CDP
// socket — so every agent method came back as -32601 "wasn't found".
Expand Down
Loading