Skip to content
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This repo measures the minimal end‑to‑end lifecycle for a remote, provider
- Navigate to a URL (wait for `domcontentloaded`)
- Release the session (control plane)

Providers included: `STEEL`, `KERNEL`, `BROWSERBASE`, `HYPERBROWSER`, `ANCHORBROWSER`.
Providers included: `STEEL`, `KERNEL`, `NOTTE`, `BROWSERBASE`, `HYPERBROWSER`, `ANCHORBROWSER`.

## Results at a glance (from included sample results)

Expand All @@ -21,6 +21,7 @@ These summary stats were computed from the sample 5,000‑run result files under
| :-----------: | -------: | ----------: | --------: | --------: |
| KERNEL | 793.84 | 776.00 | 1,006.00 | 1,105.00 |
| STEEL | 894.13 | 867.00 | 1,090.00 | 1,340.05 |
| NOTTE | 1,067.22 | 1,015.00 | 1,371.00 | 1,718.10 |
| BROWSERBASE | 2,966.87 | 2,888.00 | 3,886.00 | 4,309.12 |
| HYPERBROWSER | 3,657.11 | 3,665.50 | 5,338.00 | 6,695.05 |
| ANCHORBROWSER | 8,001.29 | 7,919.00 | 11,561.00 | 13,957.14 |
Expand All @@ -31,6 +32,7 @@ These summary stats were computed from the sample 5,000‑run result files under
| :-----------: | -------: | ------: | -------: | -------: | -------------: | ---------: |
| KERNEL | 36.64 | 288.30 | 434.15 | 34.77 | 71.41 ms | 9.0% |
| STEEL | 181.57 | 174.64 | 490.29 | 47.62 | 229.19 ms | 25.6% |
| NOTTE | 416.45 | 113.00 | 364.30 | 173.46 | 589.91 ms | 55.3% |
| BROWSERBASE | 212.83 | 1794.42 | 745.08 | 214.55 | 427.38 ms | 14.4% |
| HYPERBROWSER | 1,731.63 | 347.60 | 377.89 | 1,199.98 | 2,931.61 ms | 80.2% |
| ANCHORBROWSER | 3,796.55 | 184.29 | 1,259.66 | 2,760.79 | 6,557.34 ms | 82.0% |
Expand All @@ -39,6 +41,7 @@ Reliability (5,000 attempts/provider):

- Kernel: 100% success (0 failures)
- Steel: 100% success (0 failures)
- Notte: 99.8% success (10 failures)
- Browserbase: 100% success (0 failures)
- Hyperbrowser: 100% success (0 failures)
- AnchorBrowser: 97.34% success (133 failures)
Expand Down Expand Up @@ -185,3 +188,4 @@ The SQL reads `results/*.jsonl` with `format='newline_delimited'` and uses `try_
## License

MIT

5,000 changes: 5,000 additions & 0 deletions results/notte.jsonl

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ async function main() {
for (const providerName of providerNames) {
const provider = resolveProvider(providerName);
const out = outArg || `results/${providerName}.jsonl`;

// Nuke existing file before starting
if (fs.existsSync(out)) {
fs.unlinkSync(out);
console.error(`[RESET] Deleted existing ${out}`);
}

await runLoop(provider, { runs, url, out, rate });
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SteelProvider } from "./steel.js";
import { AnchorBrowserProvider } from "./anchorbrowser.js";
import { HyperbrowserProvider } from "./hyperbrowser.js";
import { KernelProvider } from "./kernel.js";
import { NotteProvider } from "./notte.js";

export function resolveProvider(name: string): ProviderClient {
const key = name.trim().toLowerCase();
Expand All @@ -15,5 +16,6 @@ export function resolveProvider(name: string): ProviderClient {
if (key === "hyperbrowser" || key === "hyper")
return new HyperbrowserProvider();
if (key === "kernel") return new KernelProvider();
if (key === "notte") return new NotteProvider();
throw new Error(`Unknown provider: ${name}`);
}
116 changes: 116 additions & 0 deletions src/providers/notte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { ProviderClient, ProviderSession } from "../types.js";
import { requireEnv } from "../utils/env.js";

interface SessionStartRequest {
headless?: boolean;
solve_captchas?: boolean;
max_duration_minutes?: number;
idle_timeout_minutes?: number;
browser_type?: "chromium" | "chrome" | "firefox" | "chrome-nightly" | "chrome-turbo";
profile?: {
pool?: "local_pool" | "remote_pool";
};
}

interface SessionResponse {
session_id: string;
status: string;
created_at: string;
last_accessed_at: string;
}

interface SessionDebugResponse {
debug_url: string;
ws: {
cdp: string;
recording: string;
logs: string;
};
tabs: any[];
}

export class NotteProvider implements ProviderClient {
readonly name = "NOTTE";
private apiKey: string | null = null;
private baseUrl: string;

constructor() {
this.baseUrl = process.env.NOTTE_API_URL || "https://api.notte.cc";
}

private getApiKey(): string {
if (!this.apiKey) {
this.apiKey = requireEnv("NOTTE_API_KEY");
}
return this.apiKey;
}

async create(): Promise<ProviderSession> {
// Start a new session with default parameters
const startResponse = await fetch(`${this.baseUrl}/sessions/start`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.getApiKey()}`,
},
body: JSON.stringify({}),
});

if (!startResponse.ok) {
const errorText = await startResponse.text();
const isHtml = errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html');
const errorBody = isHtml ? startResponse.statusText : errorText;
throw new Error(`Failed to start Notte session: HTTP ${startResponse.status} - ${errorBody}`);
}

const sessionData: SessionResponse = await startResponse.json();
const sessionId = sessionData.session_id;

if (!sessionId) {
throw new Error("Invalid Notte session response: missing session_id");
}

// Get the CDP URL from the debug endpoint
const debugResponse = await fetch(
`${this.baseUrl}/sessions/${sessionId}/debug`,
{
method: "GET",
headers: {
"Authorization": `Bearer ${this.getApiKey()}`,
},
}
);

if (!debugResponse.ok) {
const errorText = await debugResponse.text();
const isHtml = errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html');
const errorBody = isHtml ? debugResponse.statusText : errorText;
throw new Error(`Failed to get Notte debug info: HTTP ${debugResponse.status} - ${errorBody}`);
}

const debugData: SessionDebugResponse = await debugResponse.json();
const cdpUrl = debugData.ws?.cdp;

if (!cdpUrl) {
throw new Error("Invalid Notte debug response: missing ws.cdp");
}

return { id: sessionId, cdpUrl };
}

async release(id: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/sessions/${id}/stop`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${this.getApiKey()}`,
},
});

if (!response.ok) {
const errorText = await response.text();
const isHtml = errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html');
const errorBody = isHtml ? response.statusText : errorText;
throw new Error(`Failed to stop Notte session: HTTP ${response.status} - ${errorBody}`);
}
}
}
2 changes: 1 addition & 1 deletion src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export async function runLoop(
{ runs, url, out, rate }: { runs: number; url: string; out: string; rate?: number }
) {
const minIntervalMs = rate ? (60 / rate) * 1000 : 0; // minimum ms between sessions

if (rate) {
console.error(`[RATE_LIMIT] Throttling to ${rate} sessions/min (${(minIntervalMs / 1000).toFixed(1)}s interval)`);
}
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export type ProviderName =
| "BROWSERBASE"
| "ANCHORBROWSER"
| "HYPERBROWSER"
| "KERNEL";
| "KERNEL"
| "NOTTE";

export type MetricRecord = {
created_at: string;
Expand Down