Skip to content

telemetry + debug logging #569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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
5 changes: 5 additions & 0 deletions .changeset/fifty-crabs-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

you can now call stagehand.metrics to get token usage metrics. you can also set logInferenceToFile in stagehand config to log the entire call/response history from stagehand & the LLM.
1 change: 1 addition & 0 deletions lib/StagehandPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class StagehandPage {

if (this.llmClient) {
this.actHandler = new StagehandActHandler({
stagehand: this.stagehand,
verbose: this.stagehand.verbose,
llmProvider: this.stagehand.llmProvider,
enableCaching: this.stagehand.enableCaching,
Expand Down
26 changes: 24 additions & 2 deletions lib/handlers/actHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@ import {
ObserveResult,
ActOptions,
ObserveOptions,
StagehandFunctionName,
} from "@/types/stagehand";
import { MethodHandlerContext, SupportedPlaywrightAction } from "@/types/act";
import { buildActObservePrompt } from "../prompt";
import {
methodHandlerMap,
fallbackLocatorMethod,
} from "./handlerUtils/actHandlerUtils";

import { Stagehand } from "@/lib";
/**
* NOTE: Vision support has been removed from this version of Stagehand.
* If useVision or verifierUseVision is set to true, a warning is logged and
* the flow continues as if vision = false.
*/
export class StagehandActHandler {
private readonly stagehand: Stagehand;
private readonly stagehandPage: StagehandPage;
private readonly verbose: 0 | 1 | 2;
private readonly llmProvider: LLMProvider;
Expand All @@ -44,6 +46,7 @@ export class StagehandActHandler {
private readonly waitForCaptchaSolves: boolean;

constructor({
stagehand,
verbose,
llmProvider,
enableCaching,
Expand All @@ -53,6 +56,7 @@ export class StagehandActHandler {
selfHeal,
waitForCaptchaSolves,
}: {
stagehand: Stagehand;
verbose: 0 | 1 | 2;
llmProvider: LLMProvider;
enableCaching: boolean;
Expand All @@ -64,6 +68,7 @@ export class StagehandActHandler {
selfHeal: boolean;
waitForCaptchaSolves: boolean;
}) {
this.stagehand = stagehand;
this.verbose = verbose;
this.llmProvider = llmProvider;
this.enableCaching = enableCaching;
Expand Down Expand Up @@ -337,15 +342,17 @@ export class StagehandActHandler {
});

// Always use text-based DOM verification (no vision).
actionCompleted = await verifyActCompletion({
const verifyResult = await verifyActCompletion({
goal: action,
steps,
llmProvider: this.llmProvider,
llmClient: verifyLLmClient,
domElements,
logger: this.logger,
requestId,
logInferenceToFile: this.stagehand.logInferenceToFile,
});
actionCompleted = verifyResult.completed;

this.logger({
category: "action",
Expand All @@ -362,6 +369,12 @@ export class StagehandActHandler {
},
},
});
this.stagehand.updateMetrics(
StagehandFunctionName.ACT,
verifyResult.prompt_tokens,
verifyResult.completion_tokens,
verifyResult.inference_time_ms,
);
}

return actionCompleted;
Expand Down Expand Up @@ -682,6 +695,15 @@ export class StagehandActHandler {
requestId,
variables,
userProvidedInstructions: this.userProvidedInstructions,
onActMetrics: (promptTokens, completionTokens, inferenceTimeMs) => {
this.stagehand.updateMetrics(
StagehandFunctionName.ACT,
promptTokens,
completionTokens,
inferenceTimeMs,
);
},
logInferenceToFile: this.stagehand.logInferenceToFile,
});

this.logger({
Expand Down
24 changes: 23 additions & 1 deletion lib/handlers/extractHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { extract } from "../inference";
import { LLMClient } from "../llm/LLMClient";
import { formatText } from "../utils";
import { StagehandPage } from "../StagehandPage";
import { Stagehand } from "../index";
import { Stagehand, StagehandFunctionName } from "../index";
import { pageTextSchema } from "../../types/page";

const PROXIMITY_THRESHOLD = 15;
Expand Down Expand Up @@ -357,13 +357,24 @@ export class StagehandExtractHandler {
requestId,
userProvidedInstructions: this.userProvidedInstructions,
logger: this.logger,
logInferenceToFile: this.stagehand.logInferenceToFile,
});

const {
metadata: { completed },
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
inference_time_ms: inferenceTimeMs,
...output
} = extractionResponse;

this.stagehand.updateMetrics(
StagehandFunctionName.EXTRACT,
promptTokens,
completionTokens,
inferenceTimeMs,
);

// Clean up debug
await this.stagehandPage.cleanupDomDebug();

Expand Down Expand Up @@ -489,13 +500,24 @@ export class StagehandExtractHandler {
isUsingTextExtract: false,
userProvidedInstructions: this.userProvidedInstructions,
logger: this.logger,
logInferenceToFile: this.stagehand.logInferenceToFile,
});

const {
metadata: { completed },
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
inference_time_ms: inferenceTimeMs,
...output
} = extractionResponse;

this.stagehand.updateMetrics(
StagehandFunctionName.EXTRACT,
promptTokens,
completionTokens,
inferenceTimeMs,
);

await this.stagehandPage.cleanupDomDebug();

this.logger({
Expand Down
16 changes: 15 additions & 1 deletion lib/handlers/observeHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LogLine } from "../../types/log";
import { Stagehand } from "../index";
import { Stagehand, StagehandFunctionName } from "../index";
import { observe } from "../inference";
import { LLMClient } from "../llm/LLMClient";
import { StagehandPage } from "../StagehandPage";
Expand Down Expand Up @@ -113,8 +113,22 @@ export class StagehandObserveHandler {
logger: this.logger,
isUsingAccessibilityTree: useAccessibilityTree,
returnAction,
logInferenceToFile: this.stagehand.logInferenceToFile,
});

const {
prompt_tokens = 0,
completion_tokens = 0,
inference_time_ms = 0,
} = observationResponse;

this.stagehand.updateMetrics(
StagehandFunctionName.OBSERVE,
prompt_tokens,
completion_tokens,
inference_time_ms,
);

//Add iframes to the observation response if there are any on the page
if (iframes.length > 0) {
iframes.forEach((iframe) => {
Expand Down
63 changes: 62 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
ObserveOptions,
ObserveResult,
AgentConfig,
StagehandMetrics,
StagehandFunctionName,
} from "../types/stagehand";
import { StagehandContext } from "./StagehandContext";
import { StagehandPage } from "./StagehandPage";
Expand Down Expand Up @@ -382,7 +384,7 @@ export class Stagehand {
public readonly selfHeal: boolean;
private cleanupCalled = false;
public readonly actTimeoutMs: number;

public readonly logInferenceToFile?: boolean;
protected setActivePage(page: StagehandPage): void {
this.stagehandPage = page;
}
Expand All @@ -396,6 +398,63 @@ export class Stagehand {
return this.stagehandPage.page;
}

public stagehandMetrics: StagehandMetrics = {
actPromptTokens: 0,
actCompletionTokens: 0,
actInferenceTimeMs: 0,
extractPromptTokens: 0,
extractCompletionTokens: 0,
extractInferenceTimeMs: 0,
observePromptTokens: 0,
observeCompletionTokens: 0,
observeInferenceTimeMs: 0,
totalPromptTokens: 0,
totalCompletionTokens: 0,
totalInferenceTimeMs: 0,
};

public get metrics(): StagehandMetrics {
return this.stagehandMetrics;
}

public updateMetrics(
functionName: StagehandFunctionName,
promptTokens: number,
completionTokens: number,
inferenceTimeMs: number,
): void {
switch (functionName) {
case StagehandFunctionName.ACT:
this.stagehandMetrics.actPromptTokens += promptTokens;
this.stagehandMetrics.actCompletionTokens += completionTokens;
this.stagehandMetrics.actInferenceTimeMs += inferenceTimeMs;
break;

case StagehandFunctionName.EXTRACT:
this.stagehandMetrics.extractPromptTokens += promptTokens;
this.stagehandMetrics.extractCompletionTokens += completionTokens;
this.stagehandMetrics.extractInferenceTimeMs += inferenceTimeMs;
break;

case StagehandFunctionName.OBSERVE:
this.stagehandMetrics.observePromptTokens += promptTokens;
this.stagehandMetrics.observeCompletionTokens += completionTokens;
this.stagehandMetrics.observeInferenceTimeMs += inferenceTimeMs;
break;
}
this.updateTotalMetrics(promptTokens, completionTokens, inferenceTimeMs);
}

private updateTotalMetrics(
promptTokens: number,
completionTokens: number,
inferenceTimeMs: number,
): void {
this.stagehandMetrics.totalPromptTokens += promptTokens;
this.stagehandMetrics.totalCompletionTokens += completionTokens;
this.stagehandMetrics.totalInferenceTimeMs += inferenceTimeMs;
}

constructor(
{
env,
Expand All @@ -419,6 +478,7 @@ export class Stagehand {
selfHeal = true,
waitForCaptchaSolves = false,
actTimeoutMs = 60_000,
logInferenceToFile = false,
}: ConstructorParams = {
env: "BROWSERBASE",
},
Expand Down Expand Up @@ -473,6 +533,7 @@ export class Stagehand {
if (this.usingAPI) {
this.registerSignalHandlers();
}
this.logInferenceToFile = logInferenceToFile;
}

private registerSignalHandlers() {
Expand Down
Loading