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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,14 @@ browser = launch(humanize=True, human_preset="careful")
browser = launch(stealth_args=False, args=["--fingerprint=12345"])
```

#### geoip warnings

When you call `cloakbrowser.launch(geoip=True)` without `proxy=`, the library
logs a warning. Geoip timezone and locale resolution only runs at launch with
the launch-time proxy. Per-context proxies set later with
`browser.new_context(proxy=...)` will not trigger geoip resolution. Pass
`proxy=` to `launch()` or set explicit `timezone=` / `locale=` to silence it.

Returns a standard Playwright `Browser` object. All Playwright methods work: `new_page()`, `new_context()`, `close()`, etc.

### `launch_async()`
Expand Down
12 changes: 12 additions & 0 deletions cloakbrowser/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ def launch(

binary_path = ensure_binary(license_key=license_key, browser_version=browser_version)
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
if geoip and not proxy:
logger.warning(
"geoip=True without proxy= at launch — timezone/locale will default to "
"UTC/en-US. Per-context proxies don't trigger geoip resolution. Pass "
"proxy= to launch() or set explicit timezone=/locale=."
)
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy, browser_version, license_key)
args = _resolve_webrtc_args(args, proxy)
args = _append_webrtc_exit_ip(args, exit_ip)
Expand Down Expand Up @@ -312,6 +318,12 @@ async def launch_async( # noqa: C901

binary_path = ensure_binary(license_key=license_key, browser_version=browser_version)
timezone, locale, exit_ip = maybe_resolve_geoip(geoip, proxy, timezone, locale)
if geoip and not proxy:
logger.warning(
"geoip=True without proxy= at launch — timezone/locale will default to "
"UTC/en-US. Per-context proxies don't trigger geoip resolution. Pass "
"proxy= to launch() or set explicit timezone=/locale=."
)
proxy_kwargs, proxy_extra_args = _resolve_proxy_config(proxy, browser_version, license_key)
args = _resolve_webrtc_args(args, proxy)
args = _append_webrtc_exit_ip(args, exit_ip)
Expand Down
8 changes: 8 additions & 0 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ const context = await launchContext({ proxy: 'http://proxy:8080', geoip: true })
const browser = await launch({ proxy: 'http://proxy:8080', geoip: true, timezone: 'Europe/London' });
```

#### geoip warnings

When you call `launch({ geoip: true })` without `proxy`, the library logs a
warning. Geoip timezone and locale resolution only runs at launch with the
launch-time proxy. Per-context proxies set later with
`browser.newContext(...)` will not trigger geoip resolution. Pass `proxy` to
`launch()` or set explicit `timezone` / `locale` to silence it.

> **Note:** For rotating residential proxies, the DNS-resolved IP may differ from the exit IP. Pass explicit `timezone`/`locale` in those cases.

### CLI
Expand Down
7 changes: 7 additions & 0 deletions js/src/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ export async function buildLaunchOptions(
process.env.CLOAKBROWSER_BINARY_PATH ||
(await ensureBinary(options.licenseKey, options.browserVersion));
const { exitIp, ...resolved } = await maybeResolveGeoip(options);
if (options.geoip && !options.proxy) {
console.warn(
"geoip=True without proxy= at launch — timezone/locale will default to " +
"UTC/en-US. Per-context proxies don't trigger geoip resolution. Pass " +
"proxy= to launch() or set explicit timezone=/locale=."
);
}
const { proxyOption, proxyArgs } = resolveProxyConfig(options.proxy, options.browserVersion, options.licenseKey);
let resolvedArgs = await resolveWebrtcArgs(options);
resolvedArgs = appendWebrtcExitIp(resolvedArgs, exitIp);
Expand Down
56 changes: 56 additions & 0 deletions js/tests/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,62 @@ describe("composable Playwright launch helpers", () => {
});
});

describe("launch geoip warnings (unit)", () => {
let mockChromium: any;
const origBinaryPath = process.env.CLOAKBROWSER_BINARY_PATH;
const warning =
"geoip=True without proxy= at launch — timezone/locale will default to " +
"UTC/en-US. Per-context proxies don't trigger geoip resolution. Pass " +
"proxy= to launch() or set explicit timezone=/locale=.";

beforeEach(() => {
vi.resetModules();
process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
mockChromium = {
launch: vi.fn().mockResolvedValue({ close: vi.fn() }),
};
vi.doMock("playwright-core", () => ({ chromium: mockChromium }));
vi.doMock("../src/geoip.js", () => ({
maybeResolveGeoip: vi.fn(async (options: any) => ({
timezone: options.timezone,
locale: options.locale,
})),
resolveWebrtcArgs: vi.fn(async (options: any) => options.args),
appendWebrtcExitIp: vi.fn((args: string[] | undefined) => args),
}));
});

afterEach(() => {
vi.doUnmock("../src/geoip.js");
vi.doUnmock("playwright-core");
vi.restoreAllMocks();
vi.resetModules();
if (origBinaryPath) {
process.env.CLOAKBROWSER_BINARY_PATH = origBinaryPath;
} else {
delete process.env.CLOAKBROWSER_BINARY_PATH;
}
});

it("warns when geoip is true and no launch proxy is set", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { launch } = await import("../src/playwright.js");

await launch({ geoip: true });

expect(warnSpy).toHaveBeenCalledWith(warning);
});

it("does not warn when launch proxy is set", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { launch } = await import("../src/playwright.js");

await launch({ geoip: true, proxy: "http://example.com:8080" });

expect(warnSpy).not.toHaveBeenCalledWith(warning);
});
});

// Integration tests require the binary — run with:
// CLOAKBROWSER_BINARY_PATH=/path/to/chrome npm test
describe.skipIf(!process.env.CLOAKBROWSER_BINARY_PATH)(
Expand Down
89 changes: 89 additions & 0 deletions tests/test_geoip_launch_warning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import logging
from types import ModuleType
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from cloakbrowser import launch, launch_async


WARNING = (
"geoip=True without proxy= at launch — timezone/locale will default to "
"UTC/en-US. Per-context proxies don't trigger geoip resolution. Pass "
"proxy= to launch() or set explicit timezone=/locale=."
)


def _mock_sync_playwright():
browser = MagicMock()
pw = MagicMock()
pw.chromium.launch.return_value = browser
pw_cm = MagicMock()
pw_cm.start.return_value = pw
return pw_cm


def _mock_async_playwright():
browser = MagicMock()
browser.close = AsyncMock()
pw = MagicMock()
pw.stop = AsyncMock()
pw.chromium.launch = AsyncMock(return_value=browser)
pw_cm = MagicMock()
pw_cm.start = AsyncMock(return_value=pw)
return pw_cm


def _sync_playwright_modules(pw_cm):
playwright = ModuleType("playwright")
sync_api = ModuleType("playwright.sync_api")
sync_api.sync_playwright = MagicMock(return_value=pw_cm)
return {"playwright": playwright, "playwright.sync_api": sync_api}


def _async_playwright_modules(pw_cm):
playwright = ModuleType("playwright")
async_api = ModuleType("playwright.async_api")
async_api.async_playwright = MagicMock(return_value=pw_cm)
return {"playwright": playwright, "playwright.async_api": async_api}


def test_warns_when_geoip_true_and_no_launch_proxy(caplog):
caplog.set_level(logging.WARNING, logger="cloakbrowser")
pw_cm = _mock_sync_playwright()

with patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome"):
with patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)):
with patch.dict("sys.modules", _sync_playwright_modules(pw_cm)):
launch(geoip=True, stealth_args=False)

assert any(record.getMessage() == WARNING for record in caplog.records)


def test_does_not_warn_when_launch_proxy_is_set(caplog):
caplog.set_level(logging.WARNING, logger="cloakbrowser")
pw_cm = _mock_sync_playwright()

with patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome"):
with patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)):
with patch.dict("sys.modules", _sync_playwright_modules(pw_cm)):
launch(
geoip=True,
proxy="http://example.com:8080",
stealth_args=False,
)

assert all(record.getMessage() != WARNING for record in caplog.records)


@pytest.mark.asyncio
async def test_launch_async_warns_when_geoip_true_and_no_launch_proxy(caplog):
caplog.set_level(logging.WARNING, logger="cloakbrowser")
pw_cm = _mock_async_playwright()

with patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome"):
with patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)):
with patch.dict("sys.modules", _async_playwright_modules(pw_cm)):
await launch_async(geoip=True, stealth_args=False)

assert any(record.getMessage() == WARNING for record in caplog.records)