Tutorials

Uji CaptchaAI Sebelum Migrasi Penuh: Panduan Parallel Run

Cara paling aman mengganti layanan solver adalah tidak menggantinya dulu: jalankan penyedia lama dan CaptchaAI berdampingan beberapa hari, kirim tantangan yang sama ke keduanya, lalu putuskan dari angka Anda sendiri. Artikel ini memberi skrip pembanding siap jalan, rencana empat fase yang muat dalam satu sprint, dan daftar metrik yang menentukan keputusan.

Jaring pengamannya tidak pernah lepas: penyedia lama tetap hidup sampai fase terakhir.

Kenapa angka di halaman pemasaran tidak cukup

Tolok ukur publik diukur pada sitekey, wilayah, dan jam sibuk yang bukan milik Anda — cukup untuk menggeser waktu penyelesaian beberapa detik.

Faktor lokal memperbesar selisihnya. Banyak tim automation di Indonesia menjalankan worker di AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta), sementara laptop developer memakai jaringan rumah atau seluler; mengukur dari laptop hanya memotret latensi Anda sendiri. Jalankan skrip pembanding dari region tempat produksi berjalan.

Soal biaya pun bukan sekadar harga satuan. Paket CaptchaAI berbasis thread — Anda membayar berapa banyak CAPTCHA yang boleh diproses bersamaan, bukan per solve. BASIC ($15/bulan, 5 thread) cukup untuk fase pengujian; ADVANCE ($90/bulan, 50 thread) baru relevan setelah traffic penuh berpindah. Kalau penyedia lama menagih per solve, bandingkan total tagihan bulanan.

Rencana pengujian empat fase

Fase Durasi Porsi traffic Yang Anda buktikan
1. Validasi 1 hari 0%, hanya skrip pembanding API kompatibel, token diterima form
2. Shadow test 3 hari 5% ke CaptchaAI, dengan fallback Metrik dasar pada traffic nyata
3. Naik bertahap 1 minggu 25%, lalu 50%, lalu 75% Stabilitas tiap tingkat beban
4. Peralihan penuh seluruh traffic ke CaptchaAI Penyedia lama boleh dimatikan

Sisakan kunci penyedia lama aktif seminggu setelah fase 4; menahannya jauh lebih murah daripada membangun ulang integrasi.

Arsitektur parallel run

Satu router di depan, dua penyedia di belakang, satu pengumpul metrik di bawah — aplikasi Anda tidak perlu tahu solver mana yang dipakai.

                    ┌──────────────┐
                    │ Your App     │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │ CAPTCHA      │
                    │ Router       │
                    └──┬───────┬───┘
                       │       │
              ┌────────▼──┐ ┌──▼────────┐
              │ Current   │ │ CaptchaAI │
              │ Provider  │ │           │
              └────────┬──┘ └──┬────────┘
                       │       │
                    ┌──▼───────▼──┐
                    │ Metrics     │
                    │ Collector   │
                    └─────────────┘

Router inilah titik rollback Anda: mengembalikan traffic cukup satu perubahan konfigurasi, bukan deploy ulang.

Implementasi Python

Bungkus kedua penyedia dalam satu antarmuka

API CaptchaAI memakai pola in.php / res.php yang sama dengan sebagian besar solver lama, jadi satu class bisa melayani keduanya — cukup ganti URL dan API key. Kelas berikut mengirim task, polling, lalu mengembalikan hasil seragam dengan waktu tempuh.

import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor


@dataclass
class SolveResult:
    provider: str
    success: bool
    solution: Optional[str] = None
    error: Optional[str] = None
    elapsed: float = 0.0
    cost: float = 0.0


class CaptchaProvider:
    def __init__(self, name, submit_url, result_url, api_key):
        self.name = name
        self.submit_url = submit_url
        self.result_url = result_url
        self.api_key = api_key
        self.session = requests.Session()

    def solve_recaptcha(self, sitekey, pageurl):
        start = time.time()

        resp = self.session.post(self.submit_url, data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            return SolveResult(
                provider=self.name, success=False,
                error=data.get("request"), elapsed=time.time() - start
            )

        captcha_id = data["request"]

        for _ in range(60):
            time.sleep(5)
            result = self.session.get(self.result_url, params={
                "key": self.api_key, "action": "get",
                "id": captcha_id, "json": 1
            }).json()

            if result.get("status") == 1:
                return SolveResult(
                    provider=self.name, success=True,
                    solution=result["request"], elapsed=time.time() - start
                )
            if result.get("request") != "CAPCHA_NOT_READY":
                return SolveResult(
                    provider=self.name, success=False,
                    error=result.get("request"), elapsed=time.time() - start
                )

        return SolveResult(
            provider=self.name, success=False,
            error="TIMEOUT", elapsed=time.time() - start
        )

Kegagalan tidak dilempar sebagai exception; kegagalan adalah data. SolveResult dengan success=False plus kode error mentah justru paling berguna saat membandingkan pola kesalahan kedua penyedia.

Jalankan keduanya serentak lalu kumpulkan laporan

Runner di bawah mengirim tantangan yang sama ke kedua penyedia sekaligus, lalu meringkas tingkat keberhasilan dan waktu rata-rata, minimum, serta maksimum. Penyedia lama masuk sebagai primary, CaptchaAI sebagai challenger.

class ParallelTestRunner:
    def __init__(self, primary, challenger):
        self.primary = primary
        self.challenger = challenger
        self.results = {"primary": [], "challenger": []}

    def run_test(self, sitekey, pageurl, num_runs=20):
        print(f"Running {num_runs} parallel solves...")

        for i in range(num_runs):
            with ThreadPoolExecutor(max_workers=2) as executor:
                primary_future = executor.submit(
                    self.primary.solve_recaptcha, sitekey, pageurl
                )
                challenger_future = executor.submit(
                    self.challenger.solve_recaptcha, sitekey, pageurl
                )

                primary_result = primary_future.result()
                challenger_result = challenger_future.result()

            self.results["primary"].append(primary_result)
            self.results["challenger"].append(challenger_result)

            print(f"  Run {i+1}/{num_runs}: "
                  f"{self.primary.name}={'OK' if primary_result.success else 'FAIL'} "
                  f"({primary_result.elapsed:.1f}s) | "
                  f"{self.challenger.name}={'OK' if challenger_result.success else 'FAIL'} "
                  f"({challenger_result.elapsed:.1f}s)")

        return self.generate_report()

    def generate_report(self):
        report = {}
        for label, results in self.results.items():
            total = len(results)
            successes = sum(1 for r in results if r.success)
            times = [r.elapsed for r in results if r.success]
            errors = [r.error for r in results if not r.success]

            report[label] = {
                "provider": results[0].provider if results else "unknown",
                "total": total,
                "successes": successes,
                "success_rate": (successes / total * 100) if total else 0,
                "avg_time": sum(times) / len(times) if times else 0,
                "min_time": min(times) if times else 0,
                "max_time": max(times) if times else 0,
                "errors": errors
            }

        return report


# Usage
current = CaptchaProvider(
    name="CurrentProvider",
    submit_url="https://current-provider.com/in.php",
    result_url="https://current-provider.com/res.php",
    api_key="current_key"
)

captchaai = CaptchaProvider(
    name="CaptchaAI",
    submit_url="https://ocr.captchaai.com/in.php",
    result_url="https://ocr.captchaai.com/res.php",
    api_key=os.environ["CAPTCHAAI_API_KEY"]
)

runner = ParallelTestRunner(primary=current, challenger=captchaai)
report = runner.run_test(
    sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    pageurl="https://example.com/form",
    num_runs=20
)

for label, stats in report.items():
    print(f"\n{stats['provider']}:")
    print(f"  Success rate: {stats['success_rate']:.1f}%")
    print(f"  Avg time: {stats['avg_time']:.1f}s")
    print(f"  Min/Max: {stats['min_time']:.1f}s / {stats['max_time']:.1f}s")
    if stats['errors']:
        print(f"  Errors: {stats['errors']}")

Simpan laporan harian ke file terpisah. Satu batch 20 solve hanya gambaran kasar; nilainya muncul setelah beberapa batch dari jam berbeda.

Bagi traffic produksi secara bertahap

Setelah pengujian tertutup terlihat sehat, alirkan sebagian traffic asli — mulai 10% dengan fallback otomatis ke penyedia lama, sehingga pengguna akhir tidak melihat kegagalan.

import random


class TrafficSplitter:
    def __init__(self, primary, challenger, challenger_pct=10):
        self.primary = primary
        self.challenger = challenger
        self.challenger_pct = challenger_pct

    def solve(self, sitekey, pageurl):
        if random.randint(1, 100) <= self.challenger_pct:
            result = self.challenger.solve_recaptcha(sitekey, pageurl)
            if not result.success:
                # Fall back to primary on failure
                return self.primary.solve_recaptcha(sitekey, pageurl)
            return result
        return self.primary.solve_recaptcha(sitekey, pageurl)


# Start with 10%, increase as confidence builds
splitter = TrafficSplitter(current, captchaai, challenger_pct=10)
result = splitter.solve(sitekey="...", pageurl="...")

Naikkan persentasenya hanya setelah satu siklus penuh, termasuk jam sibuk malam, berjalan tanpa lonjakan error.

Implementasi Node.js

Untuk stack JavaScript polanya identik: satu class penyedia, dua instance, Promise.all.

const axios = require("axios");

class CaptchaProvider {
  constructor(name, submitUrl, resultUrl, apiKey) {
    this.name = name;
    this.submitUrl = submitUrl;
    this.resultUrl = resultUrl;
    this.apiKey = apiKey;
  }

  async solveRecaptcha(sitekey, pageurl) {
    const start = Date.now();
    try {
      const submit = await axios.post(this.submitUrl, null, {
        params: { key: this.apiKey, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
      });
      if (submit.data.status !== 1) {
        return { provider: this.name, success: false, error: submit.data.request, elapsed: (Date.now() - start) / 1000 };
      }

      const captchaId = submit.data.request;
      for (let i = 0; i < 60; i++) {
        await new Promise((r) => setTimeout(r, 5000));
        const poll = await axios.get(this.resultUrl, {
          params: { key: this.apiKey, action: "get", id: captchaId, json: 1 },
        });
        if (poll.data.status === 1) {
          return { provider: this.name, success: true, solution: poll.data.request, elapsed: (Date.now() - start) / 1000 };
        }
        if (poll.data.request !== "CAPCHA_NOT_READY") {
          return { provider: this.name, success: false, error: poll.data.request, elapsed: (Date.now() - start) / 1000 };
        }
      }
      return { provider: this.name, success: false, error: "TIMEOUT", elapsed: (Date.now() - start) / 1000 };
    } catch (err) {
      return { provider: this.name, success: false, error: err.message, elapsed: (Date.now() - start) / 1000 };
    }
  }
}

async function parallelTest(current, captchaai, sitekey, pageurl, runs = 20) {
  const results = { current: [], captchaai: [] };

  for (let i = 0; i < runs; i++) {
    const [currentResult, captchaaiResult] = await Promise.all([
      current.solveRecaptcha(sitekey, pageurl),
      captchaai.solveRecaptcha(sitekey, pageurl),
    ]);

    results.current.push(currentResult);
    results.captchaai.push(captchaaiResult);

    console.log(`Run ${i + 1}/${runs}: ${current.name}=${currentResult.success ? "OK" : "FAIL"} ` +
      `(${currentResult.elapsed.toFixed(1)}s) | ${captchaai.name}=${captchaaiResult.success ? "OK" : "FAIL"} ` +
      `(${captchaaiResult.elapsed.toFixed(1)}s)`);
  }

  for (const [label, data] of Object.entries(results)) {
    const successes = data.filter((r) => r.success).length;
    const times = data.filter((r) => r.success).map((r) => r.elapsed);
    const avgTime = times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
    console.log(`\n${label}: ${successes}/${runs} success (${((successes / runs) * 100).toFixed(1)}%), avg ${avgTime.toFixed(1)}s`);
  }
}

// Run
const currentProvider = new CaptchaProvider("CurrentProvider", "https://current-provider.com/in.php", "https://current-provider.com/res.php", "current_key");
const captchaai = new CaptchaProvider("CaptchaAI", "https://ocr.captchaai.com/in.php", "https://ocr.captchaai.com/res.php", process.env.CAPTCHAAI_API_KEY);

parallelTest(currentProvider, captchaai, "SITE_KEY", "https://example.com", 20);

Untuk beban besar, ganti loop berurutan dengan antrean berbatas agar solve serentak tidak melebihi thread paket Anda.

Metrik yang wajib dicatat

Metrik Cara mengukur
Tingkat keberhasilan Solve berhasil dibagi total percobaan
Waktu penyelesaian rata-rata Dari kirim task hingga token diterima
Waktu penyelesaian P95 Persentil ke-95 — ini yang dirasakan pengguna
Sebaran kode error Hitung tiap kode error terpisah
Biaya per solve efektif Tagihan bulanan dibagi solve berhasil
Validitas token Apakah form tujuan benar-benar menerima token

Baris terakhir paling sering dilewatkan: solver bisa mengembalikan token berstatus sukses, tetapi keberhasilan sesungguhnya adalah form Anda menerima token itu. Tutup pengujian dengan submit nyata ke staging.example.com/qa-login.

Contoh: agensi price monitoring di Jakarta

Skenario khas: agensi price monitoring dengan tiga klien e-commerce menjalankan worker Python di ap-southeast-3 dan bertemu reCAPTCHA v2 pada sekitar 8.000 halaman per hari. Fase 1 mereka memakai BASIC ($15/bulan, 5 thread) hanya untuk skrip pembanding: 60 solve, tiga jam berbeda, dari server produksi.

Hasilnya mengubah satu hal sebelum fase 2: jeda polling awal dinaikkan agar tidak ada permintaan res.php yang terbuang. Keputusan akhir diambil dari total tagihan bulanan, bukan selisih satu-dua detik. Kalau portofolio Anda mencakup beberapa tipe tantangan — reCAPTCHA v2 di satu klien, Cloudflare Turnstile atau GeeTest v3 di klien lain — jalankan pembanding terpisah per tipe; rata-rata gabungan menyembunyikan tipe yang bermasalah.

Catatan kepatuhan di Indonesia: UU Pelindungan Data Pribadi (UU 27/2022) membuat prinsip "ambil hanya data yang boleh Anda proses" bukan sekadar saran editorial. Batasi dataset pengujian pada halaman publik dan staging sendiri.

Ketika hasilnya terlihat aneh

Gejala Penyebab yang paling sering Tindakan
CaptchaAI terlihat lebih lambat Skrip dijalankan dari laptop Ulangi dari region produksi
Tingkat keberhasilan naik-turun tajam Sampel terlalu kecil Kumpulkan minimal 50 solve per penyedia
Token diterima satu penyedia, ditolak yang lain Token kedaluwarsa Kirim token segera setelah diterima
Kode CAPCHA_NOT_READY terus muncul Jeda polling terlalu rapat Perbesar jeda, pakai batas waktu total

Pertanyaan yang sering muncul

Berapa lama fase paralel sebaiknya berjalan?

Sekitar dua minggu untuk siklus empat fase. Yang menentukan bukan panjang kalender, melainkan cakupan: pengujian harus melewati minimal satu akhir pekan dan satu jam sibuk, karena pola tantangan berbeda di tiap slot.

Berapa thread yang cukup untuk fase pengujian?

Dua thread cukup untuk skrip pembanding: Anda hanya menjalankan dua solve serentak. BASIC ($15/bulan, 5 thread) menampung fase 1 dan 2 dengan longgar; naikkan paket setelah porsi traffic produksi melewati 50%.

Apakah CaptchaAI mendukung semua tipe CAPTCHA yang saya pakai sekarang?

Periksa sebelum fase 1. CaptchaAI mendukung reCAPTCHA v2 dan v3 termasuk varian Enterprise, Cloudflare Turnstile dan Cloudflare Challenge, GeeTest v3, CAPTCHA gambar/OCR, grid, serta BLS. hCaptcha dan FunCaptcha (Arkose Labs) belum didukung, GeeTest v4 berstatus segera hadir, sedangkan CaptchaFox (beta), Friendly Captcha (beta), dan Lemin (beta) ada dalam status beta. Kalau ada tipe yang belum didukung, rencanakan migrasi sebagian.

Bagaimana cara rollback kalau fase 3 gagal?

Turunkan challenger_pct pada TrafficSplitter menjadi 0 dan traffic kembali ke penyedia lama tanpa deploy. Karena itu porsi traffic harus dibaca dari konfigurasi atau environment variable, jangan ditulis keras di kode.

Apakah pengujian paralel menggandakan biaya saya?

Selama fase paralel, ya — dua penyedia menyelesaikan tantangan yang sama. Karena paket CaptchaAI berbasis thread dengan solve tak terbatas per thread, tambahannya adalah harga paket tetap, bukan kelipatan volume.

Artikel terkait

Langkah selanjutnya

Kalau ini integrasi CaptchaAI pertama Anda, selesaikan satu solve manual dulu, baru bangun harness pembanding. Lalu mulai fase 1: buat akun CaptchaAI, ambil API key, jalankan 50 solve pembanding dari server produksi.

Panduan terkait:

Komentar dinonaktifkan untuk artikel ini.