diff --git a/README.md b/README.md index a1f18f59..2f768188 100644 --- a/README.md +++ b/README.md @@ -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()` diff --git a/cloakbrowser/browser.py b/cloakbrowser/browser.py index 4f9d92d4..85bedd70 100644 --- a/cloakbrowser/browser.py +++ b/cloakbrowser/browser.py @@ -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) @@ -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) diff --git a/js/README.md b/js/README.md index 7050c9f5..898f2bcc 100644 --- a/js/README.md +++ b/js/README.md @@ -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 diff --git a/js/src/playwright.ts b/js/src/playwright.ts index 09e3366f..894abaee 100644 --- a/js/src/playwright.ts +++ b/js/src/playwright.ts @@ -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); diff --git a/js/tests/launch.test.ts b/js/tests/launch.test.ts index bc6ac6dd..ac1b8812 100644 --- a/js/tests/launch.test.ts +++ b/js/tests/launch.test.ts @@ -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)( diff --git a/tests/test_geoip_launch_warning.py b/tests/test_geoip_launch_warning.py new file mode 100644 index 00000000..f7185e59 --- /dev/null +++ b/tests/test_geoip_launch_warning.py @@ -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)