[{"data":1,"prerenderedAt":20},["ShallowReactive",2],{"blog:post:en:testing-geo-targeted-feature-flags-with-rotating-residential-proxies":3},{"slug":4,"lang":5,"title":6,"summary":7,"date":8,"tags":9,"tag_slugs":15,"thumbnail_url":16,"translations":17,"body":18,"asset_base":19},"testing-geo-targeted-feature-flags-with-rotating-residential-proxies","en","Testing Geo‑Targeted Feature Flags with Rotating Residential Proxies","Learn how to validate location‑based feature rollouts by driving automated tests through rotating residential proxies, ensuring each variant behaves correctly in its target region.","2026-10-01",[10,11,12,13,14],"proxies","feature-flags","geo-targeting","testing","residential-proxies",[10,11,12,13,14],"https://blog-api.ro-proxy.com/api/blog/posts/testing-geo-targeted-feature-flags-with-rotating-residential-proxies/thumbnail.svg?lang=en",[5],"## Why Geo‑Targeted Feature Flags Need Real IPs\n\nFeature‑flag platforms (LaunchDarkly, Unleash, Split.io, etc.) often gate functionality by the visitor’s country, region, or even city. A flag that enables a new checkout flow for **Germany** but hides it in **France** cannot be verified from a single data‑center IP. You need traffic that *looks* like it originates from the target locale, complete with the correct ISP ASN, residential subnet reputation, and realistic latency.\n\nRotating residential proxies give you exactly that: a pool of real‑user IPs spread across the globe, each request exiting from a different household connection. By routing each test request through a proxy that matches the flag’s targeting rule, you can assert that the flag evaluates to the expected variant without ever leaving your CI pipeline.\n\n## Core Challenges\n\n| Challenge | Why It Matters | Proxy‑Based Fix |\n|-----------|----------------|-----------------|\n| **IP‑based targeting** | Flags read the client IP (or a geo‑lookup service) to decide variant. | Choose a proxy whose exit node resolves to the desired country/region. |\n| **Sticky‑session requirements** | Some flag SDKs cache the variant per session cookie. | Use *sticky* (session‑persistent) proxies for the duration of a user journey. |\n| **Rate limits & bot detection** | Repeated requests from the same IP trigger WAFs. | Rotate IPs per request or per test case; residential pools have high reputation. |\n| **Latency variance** | Real users experience different RTTs; performance flags may depend on it. | Measure latency per proxy and optionally filter by max RTT. |\n\n## Selecting the Right Proxy Type\n\n| Proxy Type | Best For | Drawbacks |\n|------------|----------|-----------|\n| **Rotating Residential** | Broad geo coverage, high trust, low block rate. | Higher cost per GB; variable latency. |\n| **Static ISP (Sticky)** | Long‑lived sessions (login, checkout) where the same IP must persist. | Limited geo granularity; fewer IPs per region. |\n| **Mobile** | Testing carrier‑specific flags (e.g., 5G‑only features). | Smallest pools, highest price. |\n\n**Recommendation:** Start with a rotating residential pool that offers *country‑level targeting* and *session stickiness* (e.g., `proxy.example.com:8000?country=DE&session=12345`). Most providers expose these parameters via query string or header.\n\n## Wiring Proxies Into Your Test Suite\n\nBelow is a **Python** example using `requests` and a hypothetical feature‑flag client (`ldclient`). The same pattern works in Node.js, Go, or any language that can set an HTTP proxy.\n\n```python\n# test_geo_flags.py\nimport os\nimport json\nimport time\nimport requests\nfrom ldclient import LDClient\nfrom ldclient.config import Config\n\n# ------------------------------------------------------------------\n# 1️⃣  Proxy configuration (rotate per test case)\n# ------------------------------------------------------------------\nPROXY_ENDPOINT = os.getenv(\"ROTATING_PROXY\")  # e.g. http://user:pass@proxy.example.com:8000\n\ndef proxy_for_country(country_code: str) -> dict:\n    \"\"\"Return a proxy dict that forces the exit node to the given ISO‑3166‑1 alpha‑2.\"\"\"\n    # Many providers accept a `country=` query param or a custom header.\n    # Adjust to your vendor's API.\n    return {\n        \"http\": f\"{PROXY_ENDPOINT}?country={country_code}\",\n        \"https\": f\"{PROXY_ENDPOINT}?country={country_code}\",\n    }\n\n# ------------------------------------------------------------------\n# 2️⃣  Feature‑flag client (LaunchDarkly shown; swap for your SDK)\n# ------------------------------------------------------------------\nld_client = LDClient(Config(sdk_key=os.getenv(\"LD_SDK_KEY\")))\n\n# ------------------------------------------------------------------\n# 3️⃣  Helper: evaluate a flag through a specific proxy\n# ------------------------------------------------------------------\n\ndef evaluate_flag_via_proxy(flag_key: str, country: str, context: dict) -> bool:\n    \"\"\"Return the boolean variation for `flag_key` as seen from `country`.\"\"\"\n    proxies = proxy_for_country(country)\n    # The SDK may not expose a proxy hook; we fall back to a raw HTTP call\n    # to the flag evaluation endpoint (most SaaS platforms provide one).\n    url = f\"https://app.launchdarkly.com/api/v2/flags/{flag_key}/evaluate\"\n    headers = {\n        \"Authorization\": f\"Bearer {os.getenv('LD_SDK_KEY')}\",\n        \"Content-Type\": \"application/json\",\n    }\n    payload = {\n        \"context\": context,\n        \"environment\": os.getenv(\"LD_ENV\")\n    }\n    resp = requests.post(url, json=payload, headers=headers, proxies=proxies, timeout=10)\n    resp.raise_for_status()\n    return resp.json()[\"value\"]\n\n# ------------------------------------------------------------------\n# 4️⃣  Test matrix – one row per targeted region\n# ------------------------------------------------------------------\nTEST_MATRIX = [\n    {\"country\": \"DE\", \"expected\": True,  \"context\": {\"key\": \"user-1\", \"kind\": \"user\", \"country\": \"DE\"}},\n    {\"country\": \"FR\", \"expected\": False, \"context\": {\"key\": \"user-2\", \"kind\": \"user\", \"country\": \"FR\"}},\n    {\"country\": \"US\", \"expected\": True,  \"context\": {\"key\": \"user-3\", \"kind\": \"user\", \"country\": \"US\"}},\n]\n\ndef run_tests() -> None:\n    for case in TEST_MATRIX:\n        country = case[\"country\"]\n        expected = case[\"expected\"]\n        ctx = case[\"context\"]\n        start = time.time()\n        try:\n            value = evaluate_flag_via_proxy(\"new-checkout-flow\", country, ctx)\n            elapsed = time.time() - start\n            assert value == expected, f\"{country}: got {value}, expected {expected}\"\n            print(f\"✅ {country} – {value} (latency {elapsed:.2f}s)\")\n        except Exception as exc:\n            print(f\"❌ {country} – ERROR: {exc}\")\n\nif __name__ == \"__main__\":\n    run_tests()\n```\n\n### What the script does\n1. **Builds a proxy URL per country** – the provider’s `country=` query param forces the exit node into the target market.\n2. **Calls the flag‑evaluation REST endpoint** through that proxy. Most SaaS flag services expose an evaluation API; if yours only offers a client‑side SDK, you can spin up a tiny HTTP wrapper (Flask/FastAPI) that forwards the SDK call behind the proxy.\n3. **Asserts the returned variation** matches the rollout plan.\n4. **Logs latency** so you can spot outliers (e.g., a proxy in Germany routing through a US data center).\n\n## Sticky Sessions for Multi‑Step Journeys\n\nSome flags depend on a *session* (e.g., a user logs in, then sees a personalized dashboard). Rotating the IP on every request would break the session cookie. Use a **sticky proxy** for the whole journey:\n\n```python\n# sticky_proxy.py\nimport requests\nimport os\n\nSESSION_ID = \"geo-test-session-42\"  # any opaque string\nPROXY_ENDPOINT = os.getenv(\"STICKY_PROXY\")  # e.g. http://user:pass@proxy.example.com:8000\n\ndef sticky_proxy(country: str) -> dict:\n    return {\n        \"http\": f\"{PROXY_ENDPOINT}?country={country}&session={SESSION_ID}\",\n        \"https\": f\"{PROXY_ENDPOINT}?country={country}&session={SESSION_ID}\",\n    }\n\n# Example: login → fetch dashboard → assert flag\nproxies = sticky_proxy(\"DE\")\ns = requests.Session()\ns.proxies.update(proxies)\n\nlogin = s.post(\"https://app.example.com/api/login\", json={\"email\": \"test+de@example.com\", \"pwd\": \"secret\"})\nlogin.raise_for_status()\n\ndashboard = s.get(\"https://app.example.com/api/dashboard\")\nassert dashboard.json()[\"features\"][\"new-checkout-flow\"] is True\n```\n\n*Key point*: keep the same `session=` token for the entire flow; the proxy provider will pin you to a single residential IP for the lifetime of that token (usually 10‑30 min).\n\n## CI/CD Integration\n\n1. **Store proxy credentials** in your secret manager (GitHub Actions `secrets`, GitLab CI variables, CircleCI contexts).\n2. **Spin up a matrix job** – one job per target country. Example for GitHub Actions:\n\n```yaml\n# .github/workflows/geo-flag-test.yml\nname: Geo‑Targeted Feature Flag Tests\non: [push, pull_request]\njobs:\n  test-flag:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        country: [DE, FR, US, BR, JP]\n    env:\n      ROTATING_PROXY: ${{ secrets.ROTATING_PROXY }}\n      LD_SDK_KEY: ${{ secrets.LD_SDK_KEY }}\n      LD_ENV: production\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.11\"\n      - name: Install deps\n        run: pip install -r requirements.txt\n      - name: Run test for ${{ matrix.country }}\n        run: |\n          python - \u003C\u003C'PY'\n          import os, sys\n          sys.path.insert(0, \".\")\n          from test_geo_flags import evaluate_flag_via_proxy\n          ctx = {\"key\": \"ci-user\", \"kind\": \"user\", \"country\": \"${{ matrix.country }}\"}\n          val = evaluate_flag_via_proxy(\"new-checkout-flow\", \"${{ matrix.country }}\", ctx)\n          expected = {\"DE\": True, \"FR\": False, \"US\": True, \"BR\": False, \"JP\": True}[\"${{ matrix.country }}\"]\n          assert val == expected, f\"{matrix.country}: {val} != {expected}\"\n          print(f\"✅ ${{ matrix.country }} -> {val}\")\n          PY\n```\n\nThe matrix runs in parallel, each job grabbing a fresh residential IP for its country. If a job flakes due to a bad proxy, the failure is isolated and you can add a retry step.\n\n## Verifying Results Beyond Boolean Flags\n\nFlags often return **JSON payloads** (e.g., `{ \"variant\": \"beta\", \"config\": { \"maxItems\": 10 } }`). Extend the assertion logic:\n\n```python\ndef evaluate_json_flag(flag_key: str, country: str, context: dict) -> dict:\n    proxies = proxy_for_country(country)\n    url = f\"https://app.launchdarkly.com/api/v2/flags/{flag_key}/evaluate\"\n    resp = requests.post(url, json={\"context\": context, \"environment\": os.getenv(\"LD_ENV\")},\n                         headers={\"Authorization\": f\"Bearer {os.getenv('LD_SDK_KEY')}\"},\n                         proxies=proxies, timeout=10)\n    resp.raise_for_status()\n    return resp.json()  # full payload\n\n# In test:\npayload = evaluate_json_flag(\"pricing-tiers\", \"BR\", ctx)\nassert payload[\"variant\"] == \"premium\"\nassert payload[\"config\"][\"currency\"] == \"BRL\"\n```\n\n## Troubleshooting Checklist\n\n| Symptom | Likely Cause | Fix |\n|---------|--------------|-----|\n| **403/429 from flag service** | Proxy IP reputation low or rate‑limited. | Switch to a higher‑quality residential pool; add `time.sleep(1)` between requests. |\n| **Flag always returns default** | Proxy not actually exiting in target country. | Verify with `https://ipinfo.io/json` through the same proxy; adjust provider’s `country=` param. |\n| **Session cookie lost** | Rotating proxy used where sticky required. | Use sticky proxy endpoint (`session=`) for the whole flow. |\n| **High latency > 2 s** | Proxy node overloaded or far from flag edge. | Filter proxies by `max_latency_ms` if provider supports it; fallback to static ISP for that region. |\n| **TLS handshake failure** | Proxy requires SNI/ALPN that your HTTP client doesn’t send. | Ensure `requests`/`httpx` uses system CA bundle; for Node use `tls.checkServerIdentity = () => undefined` only in test. |\n\n## Extending the Pattern\n\n- **Feature‑flag canary analysis** – run the same matrix before/after a rollout and diff the variant distribution.\n- **A/B test validation** – combine with an analytics event collector (e.g., Segment) proxied through the same IPs to confirm exposure.\n- **Compliance checks** – prove to auditors that a GDPR‑restricted feature never evaluates to `true` for EU IPs.\n\n## TL;DR Checklist for Your Next Geo‑Flag Test Run\n\n1. **Pick a rotating residential provider** with per‑country targeting and optional sticky sessions.\n2. **Create a small wrapper** (Python/Node/Go) that injects the proxy per test case.\n3. **Call the flag evaluation API** (or an internal proxy‑aware SDK shim) through that wrapper.\n4. **Assert the returned variant** matches the rollout spec for each country.\n5. **Run in CI as a matrix** – one job per region – storing proxy secrets safely.\n6. **Monitor latency & error rates**; swap out bad proxies automatically.\n7. **Document the matrix** in your feature‑flag rollout ticket so QA and product can audit the coverage.\n\nBy treating the proxy as a *first‑class test dependency* rather than an after‑thought, you get deterministic, repeatable proof that your geo‑targeted flags behave exactly as designed—no more “it works on my machine” surprises.\n","https://blog-api.ro-proxy.com/api/blog/posts/testing-geo-targeted-feature-flags-with-rotating-residential-proxies/assets",1790838816859]