Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fe3eb44
update urls, ts ignore fix (#8)
Kylejeong2 Oct 4, 2025
112981f
feat: add query param parsing (#9)
Kylejeong2 Oct 5, 2025
2f906d3
disable captcha solving if query params are passed in (#10)
Kylejeong2 Oct 5, 2025
edf39f2
update model tag (#12)
miguelg719 Oct 6, 2025
fd6bf8b
redo region routing
sameelarif Oct 6, 2025
958d8c8
small updates
miguelg719 Sep 19, 2025
1fae745
changes
miguelg719 Sep 19, 2025
dc7bd03
render last reasoning
miguelg719 Sep 19, 2025
c76cd19
add sdk to repo
miguelg719 Sep 26, 2025
3200b83
adding stagehand package to next config
Kylejeong2 Sep 28, 2025
14f392f
update tailwind config ts ignore and exclude sdk from build process
Kylejeong2 Sep 28, 2025
cf2346a
updated sdk
miguelg719 Oct 6, 2025
96c4acf
probability based region routing (#13)
sameelarif Oct 6, 2025
face517
Revert "probability based region routing (#13)"
sameelarif Oct 6, 2025
1820eef
no inline import
sameelarif Oct 6, 2025
4620227
redo region routing
sameelarif Oct 6, 2025
11cc5b1
add suspense wrapper to home component for search params
Kylejeong2 Oct 6, 2025
f3d9381
pull from edge config
sameelarif Oct 6, 2025
9da2e38
Merge branch 'sameel/flags-new' into sameel/edge-config
sameelarif Oct 6, 2025
880651a
Merge branch 'main' into sameel/edge-config
sameelarif Oct 6, 2025
381115f
fix imports
sameelarif Oct 6, 2025
b45306d
Merge branch 'sameel/flags-new' of https://github.com/browserbase/pri…
sameelarif Oct 7, 2025
968f315
Merge branch 'sameel/flags-new' into sameel/edge-config
sameelarif Oct 7, 2025
33a83eb
Merge pull request #15 from browserbase/sameel/edge-config
sameelarif Oct 7, 2025
3cab49b
pull region dist. from edge config
sameelarif Oct 7, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ target/

test/
.vercel
.env*.local
9 changes: 7 additions & 2 deletions app/api/agent/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ function sseComment(comment: string): Uint8Array {

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const [sessionId, goal] = [searchParams.get("sessionId"), searchParams.get("goal")];
const [sessionId, goal, fromChat] = [
searchParams.get("sessionId"),
searchParams.get("goal"),
searchParams.get("fromChat") === "true"
];

if (!sessionId || !goal) {
return new Response(
Expand Down Expand Up @@ -118,6 +122,7 @@ export async function GET(request: Request) {
width: 1288,
height: 711,
},
solveCaptchas: !fromChat, // false if session is from a search param, true otherwise
},
},
useAPI: false,
Expand Down Expand Up @@ -195,4 +200,4 @@ export async function GET(request: Request) {
"X-Accel-Buffering": "no",
},
});
}
}
219 changes: 149 additions & 70 deletions app/api/session/route.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,185 @@
import { NextResponse } from "next/server";
import Browserbase from "@browserbasehq/sdk";
import { getAll } from "@vercel/edge-config";
import { NextResponse } from "next/server";

type BrowserbaseRegion =
| "us-west-2"
| "us-east-1"
| "eu-central-1"
| "ap-southeast-1";

// Exact timezone matches for east coast cities
const exactTimezoneMap: Record<string, BrowserbaseRegion> = {
"America/New_York": "us-east-1",
"America/Detroit": "us-east-1",
"America/Toronto": "us-east-1",
"America/Montreal": "us-east-1",
"America/Boston": "us-east-1",
"America/Chicago": "us-east-1",
// Timezone abbreviation to region mapping
const timezoneAbbreviationMap: Record<string, BrowserbaseRegion> = {
// US East Coast
EST: "us-east-1",
EDT: "us-east-1",

// US West Coast
PST: "us-west-2",
PDT: "us-west-2",

// US Mountain/Central - route to appropriate region
MST: "us-west-2",
MDT: "us-west-2",
CST: "us-east-1",
CDT: "us-east-1",

// Europe
GMT: "eu-central-1",
BST: "eu-central-1",
CET: "eu-central-1",
CEST: "eu-central-1",
EET: "eu-central-1",
EEST: "eu-central-1",
WET: "eu-central-1",
WEST: "eu-central-1",

// Asia-Pacific
JST: "ap-southeast-1", // Japan Standard Time
KST: "ap-southeast-1", // Korea Standard Time
IST: "ap-southeast-1", // India Standard Time
AEST: "ap-southeast-1", // Australian Eastern Standard Time
AEDT: "ap-southeast-1", // Australian Eastern Daylight Time
AWST: "ap-southeast-1", // Australian Western Standard Time
NZST: "ap-southeast-1", // New Zealand Standard Time
NZDT: "ap-southeast-1", // New Zealand Daylight Time
};

// Prefix-based region mapping
const prefixToRegion: Record<string, BrowserbaseRegion> = {
America: "us-west-2",
US: "us-west-2",
Canada: "us-west-2",
Europe: "eu-central-1",
Africa: "eu-central-1",
Asia: "ap-southeast-1",
Australia: "ap-southeast-1",
Pacific: "ap-southeast-1",
// Default fallback distributions if edge config is not available
const defaultDistributions: Record<
BrowserbaseRegion,
Record<BrowserbaseRegion, number>
> = {
"us-west-2": {
"us-west-2": 100,
"us-east-1": 0,
"eu-central-1": 0,
"ap-southeast-1": 0,
},
"us-east-1": {
"us-east-1": 100,
"us-west-2": 0,
"eu-central-1": 0,
"ap-southeast-1": 0,
},
"eu-central-1": {
"eu-central-1": 100,
"us-west-2": 0,
"us-east-1": 0,
"ap-southeast-1": 0,
},
"ap-southeast-1": {
"ap-southeast-1": 100,
"us-west-2": 0,
"us-east-1": 0,
"eu-central-1": 0,
},
};

// Offset ranges to regions (inclusive bounds)
const offsetRanges: {
min: number;
max: number;
region: BrowserbaseRegion;
}[] = [
{ min: -24, max: -4, region: "us-west-2" }, // UTC-24 to UTC-4
{ min: -3, max: 4, region: "eu-central-1" }, // UTC-3 to UTC+4
{ min: 5, max: 24, region: "ap-southeast-1" }, // UTC+5 to UTC+24
];

function getClosestRegion(timezone?: string): BrowserbaseRegion {
try {
if (!timezone) {
return "us-west-2"; // Default if no timezone provided
}
function selectRegionWithProbability(
baseRegion: BrowserbaseRegion,
distributions: Record<BrowserbaseRegion, Record<BrowserbaseRegion, number>>
): BrowserbaseRegion {
const distribution = distributions[baseRegion];
if (!distribution) {
return baseRegion;
}

const random = Math.random() * 100; // Generate random number between 0-100

// Check exact matches first
if (timezone in exactTimezoneMap) {
return exactTimezoneMap[timezone];
let cumulativeProbability = 0;
for (const [region, probability] of Object.entries(distribution)) {
cumulativeProbability += probability;
if (random < cumulativeProbability) {
return region as BrowserbaseRegion;
}
}

// Fallback to base region if something goes wrong
return baseRegion;
}

// Check prefix matches
const prefix = timezone.split("/")[0];
if (prefix in prefixToRegion) {
return prefixToRegion[prefix];
function getRegionFromTimezoneAbbr(timezoneAbbr?: string): BrowserbaseRegion {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timezone region mapping is completely broken - the client sends timezone identifiers like "America/New_York" but the server now expects timezone abbreviations like "EST". This will cause all sessions to default to "us-west-2" regardless of user location.

View Details
📝 Patch Details
diff --git a/app/api/session/route.ts b/app/api/session/route.ts
index 569427d..7d0c303 100644
--- a/app/api/session/route.ts
+++ b/app/api/session/route.ts
@@ -7,7 +7,84 @@ type BrowserbaseRegion =
   | "eu-central-1"
   | "ap-southeast-1";
 
-// Timezone abbreviation to region mapping
+// IANA timezone identifier to region mapping
+const exactTimezoneMap: Record<string, BrowserbaseRegion> = {
+  // US East Coast
+  "America/New_York": "us-east-1",
+  "America/Detroit": "us-east-1",
+  "America/Kentucky/Louisville": "us-east-1",
+  "America/Kentucky/Monticello": "us-east-1",
+  "America/Indiana/Indianapolis": "us-east-1",
+  "America/Indiana/Vincennes": "us-east-1",
+  "America/Indiana/Winamac": "us-east-1",
+  "America/Indiana/Marengo": "us-east-1",
+  "America/Indiana/Petersburg": "us-east-1",
+  "America/Indiana/Vevay": "us-east-1",
+  "America/Toronto": "us-east-1",
+  "America/Montreal": "us-east-1",
+  "America/Halifax": "us-east-1",
+
+  // US West Coast
+  "America/Los_Angeles": "us-west-2",
+  "America/Vancouver": "us-west-2",
+  "America/Tijuana": "us-west-2",
+
+  // Europe
+  "Europe/London": "eu-central-1",
+  "Europe/Paris": "eu-central-1",
+  "Europe/Berlin": "eu-central-1",
+  "Europe/Rome": "eu-central-1",
+  "Europe/Madrid": "eu-central-1",
+  "Europe/Amsterdam": "eu-central-1",
+  "Europe/Brussels": "eu-central-1",
+  "Europe/Vienna": "eu-central-1",
+  "Europe/Prague": "eu-central-1",
+  "Europe/Warsaw": "eu-central-1",
+  "Europe/Budapest": "eu-central-1",
+  "Europe/Stockholm": "eu-central-1",
+  "Europe/Oslo": "eu-central-1",
+  "Europe/Copenhagen": "eu-central-1",
+  "Europe/Helsinki": "eu-central-1",
+  "Europe/Zurich": "eu-central-1",
+  "Europe/Dublin": "eu-central-1",
+  "Europe/Lisbon": "eu-central-1",
+  "Europe/Athens": "eu-central-1",
+  "Europe/Istanbul": "eu-central-1",
+  "Europe/Moscow": "eu-central-1",
+
+  // Asia-Pacific
+  "Asia/Tokyo": "ap-southeast-1",
+  "Asia/Seoul": "ap-southeast-1",
+  "Asia/Shanghai": "ap-southeast-1",
+  "Asia/Hong_Kong": "ap-southeast-1",
+  "Asia/Singapore": "ap-southeast-1",
+  "Asia/Bangkok": "ap-southeast-1",
+  "Asia/Jakarta": "ap-southeast-1",
+  "Asia/Manila": "ap-southeast-1",
+  "Asia/Kuala_Lumpur": "ap-southeast-1",
+  "Asia/Kolkata": "ap-southeast-1",
+  "Asia/Dubai": "ap-southeast-1",
+  "Australia/Sydney": "ap-southeast-1",
+  "Australia/Melbourne": "ap-southeast-1",
+  "Australia/Brisbane": "ap-southeast-1",
+  "Australia/Perth": "ap-southeast-1",
+  "Australia/Adelaide": "ap-southeast-1",
+  "Pacific/Auckland": "ap-southeast-1",
+};
+
+// Prefix-based region mapping for fallback
+const prefixToRegion: Record<string, BrowserbaseRegion> = {
+  America: "us-west-2",
+  US: "us-west-2",
+  Canada: "us-west-2",
+  Europe: "eu-central-1",
+  Africa: "eu-central-1",
+  Asia: "ap-southeast-1",
+  Australia: "ap-southeast-1",
+  Pacific: "ap-southeast-1",
+};
+
+// Timezone abbreviation to region mapping (for legacy support)
 const timezoneAbbreviationMap: Record<string, BrowserbaseRegion> = {
   // US East Coast
   EST: "us-east-1",
@@ -93,19 +170,52 @@ function selectRegionWithProbability(
   return baseRegion;
 }
 
-function getRegionFromTimezoneAbbr(timezoneAbbr?: string): BrowserbaseRegion {
+function getClosestRegion(timezone?: string): BrowserbaseRegion {
   try {
-    if (!timezoneAbbr) {
+    if (!timezone) {
       return "us-west-2"; // Default if no timezone provided
     }
 
-    // Direct lookup from timezone abbreviation
-    const region = timezoneAbbreviationMap[timezoneAbbr.toUpperCase()];
-    if (region) {
-      return region;
+    // Check exact IANA timezone identifier matches first
+    if (timezone in exactTimezoneMap) {
+      return exactTimezoneMap[timezone];
+    }
+
+    // Check timezone abbreviation matches (legacy support)
+    const abbreviationRegion = timezoneAbbreviationMap[timezone.toUpperCase()];
+    if (abbreviationRegion) {
+      return abbreviationRegion;
+    }
+
+    // Check prefix matches for IANA identifiers
+    const prefix = timezone.split("/")[0];
+    if (prefix in prefixToRegion) {
+      return prefixToRegion[prefix];
+    }
+
+    // Use offset-based fallback for unknown timezones
+    try {
+      const date = new Date();
+      // Create a date formatter for the given timezone
+      const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone });
+      // Get the timezone offset by comparing formatted times
+      const utcDate = new Date(date.toISOString().slice(0, -1));
+      const localDate = new Date(formatter.format(date) + " UTC");
+      const hourOffset = (localDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
+
+      // Map offset ranges to regions
+      if (hourOffset >= -12 && hourOffset <= -4) {
+        return "us-west-2"; // US West Coast and Pacific
+      } else if (hourOffset >= -3 && hourOffset <= 4) {
+        return "eu-central-1"; // Europe and Africa
+      } else if (hourOffset >= 5 && hourOffset <= 12) {
+        return "ap-southeast-1"; // Asia-Pacific
+      }
+    } catch {
+      // If offset calculation fails, fall through to default
     }
 
-    // Fallback to us-west-2 for unknown abbreviations
+    // Fallback to us-west-2 for unknown timezones
     return "us-west-2";
   } catch {
     return "us-west-2";
@@ -128,12 +238,12 @@ async function createSession(timezone?: string) {
     advancedStealth: true
   };
 
-  // Use timezone abbreviation to determine base region
-  const closestRegion = getRegionFromTimezoneAbbr(timezone);
+  // Use timezone identifier to determine base region
+  const closestRegion = getClosestRegion(timezone);
   // Apply probability routing for potential load balancing
   const finalRegion = selectRegionWithProbability(closestRegion);
 
-  console.log("timezone abbreviation:", timezone);
+  console.log("timezone:", timezone);
   console.log("mapped to region:", closestRegion);
   console.log("final region after probability routing:", finalRegion);
 

Analysis

Timezone region mapping broken for IANA identifiers in session creation

What fails: getRegionFromTimezoneAbbr() only handles timezone abbreviations like "EST" but client sends IANA timezone identifiers like "America/New_York", causing all sessions to default to "us-west-2"

How to reproduce:

// Client sends IANA identifier from browser:
Intl.DateTimeFormat().resolvedOptions().timeZone; // Returns "America/New_York"

// Server function only matches abbreviations:
getRegionFromTimezoneAbbr("America/New_York"); // Returns "us-west-2" (fallback)

Result: All users with IANA timezone identifiers (default browser behavior) get routed to us-west-2 instead of their geographic region

Expected: "America/New_York" should map to "us-east-1", "Europe/London" to "eu-central-1", per Intl.DateTimeFormat docs which return IANA timezone names

try {
if (!timezoneAbbr) {
return "us-west-2"; // Default if no timezone provided
}

// Use offset-based fallback
const date = new Date();
// Create a date formatter for the given timezone
const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone });
// Get the timezone offset in minutes
const timeString = formatter.format(date);
const testDate = new Date(timeString);
const hourOffset = (testDate.getTime() - date.getTime()) / (1000 * 60 * 60);

const matchingRange = offsetRanges.find(
(range) => hourOffset >= range.min && hourOffset <= range.max
);
// Direct lookup from timezone abbreviation
const region = timezoneAbbreviationMap[timezoneAbbr.toUpperCase()];
if (region) {
return region;
}

return matchingRange?.region ?? "us-west-2";
// Fallback to us-west-2 for unknown abbreviations
return "us-west-2";
} catch {
return "us-west-2";
}
}

interface EdgeConfig {
advancedStealth: boolean | undefined;
proxies: boolean | undefined;
regionDistribution:
| Record<BrowserbaseRegion, Record<BrowserbaseRegion, number>>
| undefined;
}

async function createSession(timezone?: string) {
const bb = new Browserbase({
apiKey: process.env.BROWSERBASE_API_KEY!,
});

const browserSettings: Browserbase.Sessions.SessionCreateParams.BrowserSettings = {
viewport: {
width: 2560,
height: 1440,
},
//@ts-expect-error - not present in the types, but valid
os: "windows",
blockAds: true,
advancedStealth: true
};

console.log("timezone ", timezone);
console.log("getClosestRegion(timezone)", getClosestRegion(timezone));
const config = await getAll<EdgeConfig>();

const {
advancedStealth: advancedStealthConfig,
proxies: proxiesConfig,
regionDistribution: distributionsConfig,
} = config;

const advancedStealth: boolean = advancedStealthConfig ?? false;
const proxies: boolean = proxiesConfig ?? true;

// Build browserSettings conditionally
const browserSettings: Browserbase.Sessions.SessionCreateParams.BrowserSettings =
{
viewport: {
width: 2560,
height: 1440,
},
blockAds: true,
advancedStealth,
// Only set os if advancedStealth is true
...(advancedStealth
? {
os: "mac",
}
: {
os: "linux",
}),
};

// Use timezone abbreviation to determine base region
const closestRegion = getRegionFromTimezoneAbbr(timezone);

// Get distributions from config or use default
const distributions = distributionsConfig ?? defaultDistributions;

// Apply probability routing for potential load balancing
const finalRegion = selectRegionWithProbability(closestRegion, distributions);

console.log("timezone abbreviation:", timezone);
console.log("mapped to region:", closestRegion);
console.log("final region after probability routing:", finalRegion);

const session = await bb.sessions.create({
projectId: process.env.BROWSERBASE_PROJECT_ID!,
proxies: true,
proxies,
browserSettings,
keepAlive: true,
region: getClosestRegion(timezone),
region: finalRegion,
});
return {
session,
Expand Down
6 changes: 4 additions & 2 deletions app/components/BrowserSessionContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface BrowserSessionContainerProps {
isCompleted: boolean;
initialMessage: string | undefined;
sessionTime?: number;
isFromSearchParam?: boolean;
onStop?: () => void;
onRestart?: () => void;
}
Expand Down Expand Up @@ -104,6 +105,7 @@ const BrowserSessionContainer: React.FC<BrowserSessionContainerProps> = ({
isCompleted,
initialMessage,
sessionTime = 0,
isFromSearchParam = false,
onStop = () => {},
onRestart = () => {},
}) => {
Expand Down Expand Up @@ -192,7 +194,7 @@ const BrowserSessionContainer: React.FC<BrowserSessionContainerProps> = ({
sessionUrl ? (
<iframe
src={sessionUrl}
className="w-full h-full border-none pointer-events-none"
className={`w-full h-full border-none ${!isFromSearchParam ? 'pointer-events-none' : ''}`}
sandbox="allow-same-origin allow-scripts allow-forms"
allow="clipboard-read; clipboard-write"
loading="lazy"
Expand Down Expand Up @@ -316,4 +318,4 @@ const BrowserSessionContainer: React.FC<BrowserSessionContainerProps> = ({
);
};

export default BrowserSessionContainer;
export default BrowserSessionContainer;
5 changes: 4 additions & 1 deletion app/components/ChatFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ChatFeedProps, AgentState, BrowserStep } from "../types/ChatFeed";

export default function ChatFeed({
initialMessage,
isFromSearchParam = false,
onClose,
}: ChatFeedProps) {
const renderCount = useRef(0);
Expand Down Expand Up @@ -199,6 +200,7 @@ export default function ChatFeed({
} = useAgentStream({
sessionId: null,
goal: initialMessage,
isFromSearchParam,
onStart: handleStart,
onDone: handleDone,
onError: handleError,
Expand Down Expand Up @@ -297,6 +299,7 @@ export default function ChatFeed({
isCompleted={agentFinished}
initialMessage={initialMessage || undefined}
sessionTime={sessionTime}
isFromSearchParam={isFromSearchParam}
onStop={handleDone}
onRestart={onClose}
/>
Expand Down Expand Up @@ -370,4 +373,4 @@ export default function ChatFeed({
</main>
</motion.div>
);
}
}
Loading