Skip to content

Conversation

@PopcornDev1
Copy link
Contributor

@PopcornDev1 PopcornDev1 commented Jan 27, 2026

Summary

  • Adds IPGeolocationService that automatically detects timezone and coordinates from proxy exit IP
  • Auto-updates profile geolocation when proxy IP changes (handles IP rotation)
  • Caches geo lookups (24h) and exit IPs (30min) to minimize API calls
  • Integrates with setFingerprintingProperties() to set window.setTimezone() and window.setGeolocation()

Changes

  • New ip-geolocation.ts service using ip-api.com Pro API
  • launchProfile() now checks for IP changes and updates geo data automatically
  • createProfile() now accepts optional proxyState parameter (for proxy rotation compatibility)

Summary by CodeRabbit

  • New Features

    • Profiles now include timezone, latitude/longitude, country code, and last-known IP.
    • Automatic IP detection and proxy-aware geolocation with caching and change detection.
    • Screen fingerprinting enhancements: screen dimensions and audio fingerprint seed applied on launch.
    • Geolocation service and related exports made available.
  • Chores

    • Added https-proxy-agent dependency.

@PopcornDev1 PopcornDev1 requested a review from icepaq January 27, 2026 00:25
@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

This PR adds an IP geolocation service and integrates it into profile creation to detect exit IPs via proxies and persist timezone, coordinates, country code, and lastKnownIP. The createProfile signature now accepts proxyId. Screen/audio fingerprinting and storage types were extended.

Changes

Cohort / File(s) Summary
Geolocation service & manifest
src/ip-geolocation.ts, package.json
New IPGeolocationService module with proxy exit-IP discovery (uses https-proxy-agent), caching, rate-limiting, lookup via ip-api.com, and cache management APIs. Dependency https-proxy-agent added.
Client integration
src/client.ts
Integrated geo lookup into profile creation and one-time launches (uses ProxyConfig from proxy details), stores proxyId, timezone, geo_latitude, geo_longitude, countryCode, lastKnownIP; changed createProfile signature to include proxyId.
Public exports
src/index.ts
Re-exported IPGeolocationService, getGeoService, and types GeoLocationData, ProxyConfig.
Storage & types
src/storage-manager.ts, src/types.ts
Extended ProfileStorageData with audioFingerprintSeed, screenDimensions, timezone, geolocation, countryCode, lastKnownIP. setFingerprintingProperties now attempts to apply audio seed, screen dimensions, timezone, and geolocation via optional window setters.

Sequence Diagram(s)

sequenceDiagram
    participant Client as RoverfoxClient
    participant ProxySvc as Proxy Service
    participant GeoSvc as IPGeolocationService
    participant IpApi as ip-api.com
    participant DB as Database
    participant Storage as StorageManager

    Client->>ProxySvc: Request proxy details (proxyId)
    ProxySvc-->>Client: Return ProxyConfig (host,port,cred)
    Client->>GeoSvc: lookupThroughProxy(ProxyConfig)
    GeoSvc->>GeoSvc: Check per-proxy IP cache
    alt cache miss
        GeoSvc->>GeoSvc: Resolve exit IP via proxy (https-proxy-agent)
        GeoSvc->>IpApi: Query geolocation for exit IP
        IpApi-->>GeoSvc: Return geo data (timezone, lat, lon, countryCode,...)
        GeoSvc->>GeoSvc: Cache IP & geo result
    end
    GeoSvc-->>Client: { ip, geo }
    Client->>DB: Insert/Update profile (proxyId, timezone, lat, lon, countryCode, lastKnownIP)
    DB-->>Client: Confirm saved
    Client->>Storage: setFingerprintingProperties(page, profile)
    Storage->>Storage: Call page setters: setScreenDimensions, setAudioFingerprintSeed, setGeolocation, setTimezone
    Storage-->>Client: Fingerprinting applied
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit taps the proxy key, hops through IP and sea,
It maps the world by timezone, lat, and longitude with glee. 🌍
Caches snuggle data tight, changes tracked by moonlight,
Profiles bloom with screen and sound — a tidy, clever sight. 🐰

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add IP geolocation auto-detection for timezone and coordinates' directly and accurately describes the main change in the pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fingerprint-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client.ts (1)

78-103: Guard against empty proxyId results before querying proxies.

proxyId is an array; an empty array is truthy and leads to .eq("id", undefined). This can trigger a bad query and skip proxy setup.

🐛 Proposed fix
-    const { data: proxyId } = await this.supabaseClient
+    const { data: account } = await this.supabaseClient
       .from("accounts")
       .select("proxyId")
-      .eq("browserId", browserId);
+      .eq("browserId", browserId)
+      .single();

+    const proxyId = account?.proxyId ?? null;

-    if (proxyId) {
+    if (proxyId != null) {
       const { data: proxyData } = await this.supabaseClient
         .from("proxies")
         .select("entry, port, username, password")
-        .eq("id", proxyId?.[0]?.proxyId)
+        .eq("id", proxyId)
         .single();
🤖 Fix all issues with AI agents
In `@src/client.ts`:
- Around line 113-136: Before overwriting profile.data.lastKnownIP, save the
existing value to a temp variable and use that saved value in the debug log so
the "old -> new" message is accurate; specifically, in the block that handles
currentIP changes (the code referencing profile.data.lastKnownIP, currentIP,
geoData and calling this.supabaseClient.from("redrover_profile_data").update),
capture previousIP = profile.data.lastKnownIP before assigning
profile.data.lastKnownIP = currentIP and then use previousIP in the console.log
when changed is true.

In `@src/ip-geolocation.ts`:
- Around line 52-67: In getExitIP, the cache key currently uses only
proxy.host:proxy.port which can collide across different credentials and the
proxy URL uses raw credentials that can break with special chars; update the
cacheKey to incorporate the proxy.username and proxy.password (e.g., include
encodedUsername:encodedPassword) and URL-encode the credential parts when
building auth (use encodeURIComponent on proxy.username and proxy.password)
before composing proxyUrl and creating the HttpsProxyAgent so cached entries and
the proxy URL are both credential-aware and safe for special characters.
- Around line 69-95: The fetch call in the ipify lookup currently passes an
`agent` (and uses `AbortController`/`IPIFY_TIMEOUT_MS`) but Node's built-in
fetch ignores `agent`, causing proxy bypass; replace the `agent` usage with
undici's `ProxyAgent` (or `ProxyAgent` instance) and pass it to fetch as the
`dispatcher` option instead of `agent` (create the ProxyAgent where `agent` is
currently set and use `dispatcher: proxyAgent` in the fetch call), and move
`clearTimeout(timeout)` into a `finally` block surrounding the try/catch so the
timer is always cleared even when fetch throws; refer to the existing symbols
`agent`, `AbortController`, `IPIFY_TIMEOUT_MS`, and the fetch call to locate the
changes.
🧹 Nitpick comments (1)
src/ip-geolocation.ts (1)

152-175: Add a timeout to the ip-api request as well.

fetchFromAPI can hang indefinitely on network stalls. Mirror the ipify timeout to keep profile creation/launch resilient.

⏱️ Suggested pattern
-      const response = await fetch(url);
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), IPIFY_TIMEOUT_MS);
+      const response = await fetch(url, { signal: controller.signal });
+      clearTimeout(timeout);

Comment on lines +113 to +136
if (currentIP && (changed || !profile.data.timezone)) {
const geoData = await this.geoService.lookup(currentIP);
if (geoData) {
profile.data.timezone = geoData.timezone;
profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
profile.data.countryCode = geoData.countryCode;
profile.data.lastKnownIP = currentIP;

// Persist to database
await this.supabaseClient
.from("redrover_profile_data")
.update({ data: profile.data })
.eq("browser_id", browserId);

if (this.debug) {
if (changed) {
console.log(
`IP changed for ${browserId}: ${profile.data.lastKnownIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
);
} else {
console.log(
`Geolocation set for ${browserId}: ${geoData.timezone}`,
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Preserve the previous IP for accurate debug logs.

profile.data.lastKnownIP is overwritten before logging, so the “old → new” message becomes misleading.

💡 Suggested tweak
-        if (geoData) {
+        if (geoData) {
+          const previousIP = profile.data.lastKnownIP ?? null;
           profile.data.timezone = geoData.timezone;
           profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
           profile.data.countryCode = geoData.countryCode;
           profile.data.lastKnownIP = currentIP;
...
-              `IP changed for ${browserId}: ${profile.data.lastKnownIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
+              `IP changed for ${browserId}: ${previousIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (currentIP && (changed || !profile.data.timezone)) {
const geoData = await this.geoService.lookup(currentIP);
if (geoData) {
profile.data.timezone = geoData.timezone;
profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
profile.data.countryCode = geoData.countryCode;
profile.data.lastKnownIP = currentIP;
// Persist to database
await this.supabaseClient
.from("redrover_profile_data")
.update({ data: profile.data })
.eq("browser_id", browserId);
if (this.debug) {
if (changed) {
console.log(
`IP changed for ${browserId}: ${profile.data.lastKnownIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
);
} else {
console.log(
`Geolocation set for ${browserId}: ${geoData.timezone}`,
);
}
if (currentIP && (changed || !profile.data.timezone)) {
const geoData = await this.geoService.lookup(currentIP);
if (geoData) {
const previousIP = profile.data.lastKnownIP ?? null;
profile.data.timezone = geoData.timezone;
profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
profile.data.countryCode = geoData.countryCode;
profile.data.lastKnownIP = currentIP;
// Persist to database
await this.supabaseClient
.from("redrover_profile_data")
.update({ data: profile.data })
.eq("browser_id", browserId);
if (this.debug) {
if (changed) {
console.log(
`IP changed for ${browserId}: ${previousIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
);
} else {
console.log(
`Geolocation set for ${browserId}: ${geoData.timezone}`,
);
}
🤖 Prompt for AI Agents
In `@src/client.ts` around lines 113 - 136, Before overwriting
profile.data.lastKnownIP, save the existing value to a temp variable and use
that saved value in the debug log so the "old -> new" message is accurate;
specifically, in the block that handles currentIP changes (the code referencing
profile.data.lastKnownIP, currentIP, geoData and calling
this.supabaseClient.from("redrover_profile_data").update), capture previousIP =
profile.data.lastKnownIP before assigning profile.data.lastKnownIP = currentIP
and then use previousIP in the console.log when changed is true.

Comment on lines +52 to +67
async getExitIP(proxy: ProxyConfig): Promise<string | null> {
const cacheKey = `${proxy.host}:${proxy.port}`;

// Check IP cache (shorter TTL since IPs can rotate)
const cached = this.ipCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < IP_CACHE_TTL_MS) {
return cached.ip;
}

try {
// Build proxy URL for the agent
const auth = proxy.username && proxy.password
? `${proxy.username}:${proxy.password}@`
: "";
const proxyUrl = `http://${auth}${proxy.host}:${proxy.port}`;
const agent = new HttpsProxyAgent(proxyUrl);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Include proxy credentials in the cache key and URL-encode auth.

Caching only by host:port can collide across different proxy credentials (common with session-based usernames), and raw credentials in the URL will break on special characters.

🐛 Proposed fix
-    const cacheKey = `${proxy.host}:${proxy.port}`;
+    const user = proxy.username ?? "";
+    const pass = proxy.password ?? "";
+    const cacheKey = `${proxy.host}:${proxy.port}:${user}:${pass}`;

-    const auth = proxy.username && proxy.password
-      ? `${proxy.username}:${proxy.password}@`
+    const auth = proxy.username && proxy.password
+      ? `${encodeURIComponent(proxy.username)}:${encodeURIComponent(proxy.password)}@`
       : "";
🤖 Prompt for AI Agents
In `@src/ip-geolocation.ts` around lines 52 - 67, In getExitIP, the cache key
currently uses only proxy.host:proxy.port which can collide across different
credentials and the proxy URL uses raw credentials that can break with special
chars; update the cacheKey to incorporate the proxy.username and proxy.password
(e.g., include encodedUsername:encodedPassword) and URL-encode the credential
parts when building auth (use encodeURIComponent on proxy.username and
proxy.password) before composing proxyUrl and creating the HttpsProxyAgent so
cached entries and the proxy URL are both credential-aware and safe for special
characters.

Comment on lines +69 to +95
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), IPIFY_TIMEOUT_MS);

const response = await fetch("https://api.ipify.org?format=json", {
// @ts-ignore - Node.js fetch supports agent
agent,
signal: controller.signal,
});

clearTimeout(timeout);

if (!response.ok) {
console.warn(`ipify request failed: ${response.status}`);
return null;
}

const data = await response.json();
const ip = data.ip;

if (ip) {
this.ipCache.set(cacheKey, { ip, timestamp: Date.now() });
}

return ip || null;
} catch (error) {
console.error(`Failed to get exit IP through proxy:`, error);
return null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Node.js built-in fetch agent option proxy support undici ProxyAgent

💡 Result:

Node’s built-in fetch (Node 18+) is implemented on top of undici, and it intentionally does not support the classic { agent } option from http(s).request / node-fetch. Instead, undici uses a Dispatcher model, and undici’s fetch supports a non-standard dispatcher option. [1]

Use a proxy with built-in fetch (recommended: set a global dispatcher)

Install undici and set a ProxyAgent as the global dispatcher:

import { ProxyAgent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new ProxyAgent('http://user:pass@proxy-host:8080'));

const res = await fetch('https://example.com');
console.log(await res.text());

ProxyAgent is a Dispatcher implementation designed for proxying. [2] setGlobalDispatcher() sets the dispatcher used by undici APIs (and is shared with Node’s bundled undici used by global fetch). [1]

Per-request proxy (works with undici’s fetch option shape)

import { ProxyAgent } from 'undici';

const proxy = new ProxyAgent('http://proxy-host:8080');

const res = await fetch('https://example.com', {
  dispatcher: proxy,
});

The dispatcher option is part of undici’s fetch API. [1]

Proxy via environment variables (HTTP_PROXY / HTTPS_PROXY / NO_PROXY)

Use EnvHttpProxyAgent, which reads http_proxy / https_proxy / no_proxy (and uppercase variants) and routes accordingly:

import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new EnvHttpProxyAgent());

const res = await fetch('https://example.com');

Behavior and env var support are documented in EnvHttpProxyAgent. [3]

Note (TypeScript): because dispatcher is not part of the web-standard fetch types, you may need to cast the init object (or call undici.fetch directly) when doing per-request dispatcher usage. [1]

Sources:
[1] https://github.com/nodejs/undici (docs: fetch(..., { dispatcher }), setGlobalDispatcher)
[2] https://undici-docs.vramana.dev/docs/next/api/ProxyAgent
[3] https://undici-docs.vramana.dev/docs/api/EnvHttpProxyAgent


Node's built-in fetch doesn't support the agent option; proxy request will bypass the proxy.

Node.js's built-in fetch intentionally ignores the agent option and instead uses undici's Dispatcher model. This means the current HttpsProxyAgent won't be honored, and the request will connect directly, leaking the server's IP instead of your proxy's IP. Use undici's ProxyAgent with the dispatcher option instead. Also move clearTimeout to a finally block to prevent timer leaks if fetch fails.

Fix using undici ProxyAgent
-import { HttpsProxyAgent } from "https-proxy-agent";
+import { ProxyAgent } from "undici";

-      const agent = new HttpsProxyAgent(proxyUrl);
+      const dispatcher = new ProxyAgent(proxyUrl);

-      const response = await fetch("https://api.ipify.org?format=json", {
-        // `@ts-ignore` - Node.js fetch supports agent
-        agent,
-        signal: controller.signal,
-      });
-
-      clearTimeout(timeout);
+      let response;
+      try {
+        response = await fetch("https://api.ipify.org?format=json", {
+          dispatcher,
+          signal: controller.signal,
+        });
+      } finally {
+        clearTimeout(timeout);
+      }
🤖 Prompt for AI Agents
In `@src/ip-geolocation.ts` around lines 69 - 95, The fetch call in the ipify
lookup currently passes an `agent` (and uses
`AbortController`/`IPIFY_TIMEOUT_MS`) but Node's built-in fetch ignores `agent`,
causing proxy bypass; replace the `agent` usage with undici's `ProxyAgent` (or
`ProxyAgent` instance) and pass it to fetch as the `dispatcher` option instead
of `agent` (create the ProxyAgent where `agent` is currently set and use
`dispatcher: proxyAgent` in the fetch call), and move `clearTimeout(timeout)`
into a `finally` block surrounding the try/catch so the timer is always cleared
even when fetch throws; refer to the existing symbols `agent`,
`AbortController`, `IPIFY_TIMEOUT_MS`, and the fetch call to locate the changes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client.ts (1)

105-130: ⚠️ Potential issue | 🟡 Minor

Guard against empty proxyId results before querying proxies.

The first .select() returns an array; when no rows match, it returns an empty array [] which is truthy in JavaScript. This causes the if (proxyId) branch to execute with proxyId as [], making proxyId?.[0]?.proxyId resolve to undefined. The subsequent proxy query then runs with .eq("id", undefined), which is semantically incorrect.

Use .single() or .maybeSingle() on the initial query to normalize the result (or check proxyId.length > 0 instead).

🔧 Suggested fix
-    const { data: proxyId } = await this.supabaseClient
+    const { data: account } = await this.supabaseClient
       .from("accounts")
       .select("proxyId")
-      .eq("browserId", browserId);
+      .eq("browserId", browserId)
+      .single();
 
-    let proxyObject: RoverfoxProxyObject = null;
+    const proxyId = account?.proxyId ?? null;
+    let proxyObject: RoverfoxProxyObject = null;
     let proxyConfig: ProxyConfig | null = null;
 
-    if (proxyId) {
+    if (proxyId !== null) {
       const { data: proxyData } = await this.supabaseClient
         .from("proxies")
         .select("entry, port, username, password")
-        .eq("id", proxyId?.[0]?.proxyId)
+        .eq("id", proxyId)
         .single();
🧹 Nitpick comments (1)
src/storage-manager.ts (1)

95-106: Consider explicit undefined checks for numeric fields.
Using truthy checks can skip valid values like 0.

♻️ Suggested tweak
-          if (audioFingerprintSeed && _window.setAudioFingerprintSeed) {
+          if (audioFingerprintSeed !== undefined && _window.setAudioFingerprintSeed) {
             _window.setAudioFingerprintSeed(audioFingerprintSeed);
           }
@@
-            if (screenDimensions.colorDepth && _window.setScreenColorDepth) {
+            if (screenDimensions.colorDepth !== undefined && _window.setScreenColorDepth) {
               _window.setScreenColorDepth(screenDimensions.colorDepth);
             }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants