Tutorials

MongoDB untuk Riwayat Solve CAPTCHA dan Analitik

Kalau worker Anda menjalankan CaptchaAI dalam jumlah besar, pertanyaan yang paling sering muncul bukan lagi "apakah CAPTCHA-nya terselesaikan", tapi "jenis mana yang paling sering error, jam berapa, dan berapa lama rata-rata prosesnya". MongoDB pas untuk menjawab pertanyaan itu: skema dokumennya bebas dari migrasi tiap kali menambah jenis CAPTCHA baru, dan aggregation pipeline bawaannya cukup untuk menghitung tingkat keberhasilan, waktu penyelesaian, serta pola error tanpa perlu data warehouse terpisah.

Panduan ini membangun pipeline pencatatan solve dari nol: skema dokumen, koneksi dan index, fungsi kirim-dan-simpan, sampai kueri analitik siap pakai — dalam Python dan Node.js.

Kenapa Skema Fleksibel MongoDB Cocok untuk Data CAPTCHA

Setiap jenis CAPTCHA punya kolom yang berbeda: reCAPTCHA butuh googlekey, Cloudflare Turnstile butuh sitekey, CAPTCHA gambar/OCR butuh body berisi base64. Kalau Anda memakai database relasional, setiap kali menambah jenis CAPTCHA baru berarti migrasi skema — kolom baru, index baru, downtime kecil. Dokumen tanpa skema MongoDB menghindari masalah ini sepenuhnya: satu koleksi solves menampung reCAPTCHA, Turnstile, GeeTest v3, sampai BLS tanpa perlu ALTER TABLE sama sekali.

Ini juga cocok dengan cara tim automation Indonesia biasanya bekerja — banyak yang mengerjakan beberapa proyek klien sekaligus (freelance scraping lewat Upwork/Fastwork, agensi price-monitoring, tim data startup). Field metadata.project di skema di bawah ini sengaja dibuat generik supaya satu koleksi bisa melacak solve dari beberapa proyek berbeda tanpa perlu database terpisah per klien.

Skema Dokumen per Solve

Simpan setiap upaya solve sebagai satu dokumen, termasuk metadata operasional yang Anda butuhkan untuk debugging dan pelaporan nanti:

{
  "_id": "ObjectId",
  "captcha_id": "12345678",
  "type": "recaptcha_v2",
  "method": "userrecaptcha",
  "sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  "pageurl": "https://example.com/form",
  "status": "solved",
  "solution": "03AGdBq26...",
  "error": null,
  "submitted_at": "2026-04-04T10:15:30.000Z",
  "solved_at": "2026-04-04T10:15:45.000Z",
  "elapsed_ms": 15000,
  "polls": 3,
  "proxy_used": true,
  "cost": 0.00299,
  "metadata": {
    "project": "price-monitor",
    "worker_id": "worker-3",
    "target_domain": "example.com"
  }
}

Membangun Pipeline-nya dengan Python

Ada empat bagian: koneksi ke MongoDB, index untuk performa, fungsi yang mengirim CAPTCHA ke CaptchaAI sambil mencatat hasilnya, lalu kueri untuk membaca datanya kembali.

Koneksi ke MongoDB

Muat URI koneksi dan API key dari environment variable — jangan hardcode di kode sumber:

import os
import time
from datetime import datetime, timezone
from pymongo import MongoClient, ASCENDING, DESCENDING
import requests

MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
API_KEY = os.environ["CAPTCHAAI_API_KEY"]

client = MongoClient(MONGO_URI)
db = client["captcha_tracking"]
solves = db["solves"]

Index yang Wajib Ada Sebelum Production

Tanpa index yang tepat, kueri aggregation di koleksi besar bisa melambat drastis begitu volume solve naik. Empat index di bawah menutupi pola akses yang paling umum: urutan waktu, filter per jenis dan status, filter per proyek, dan filter per domain target. Index terakhir memakai TTL supaya dokumen lama terhapus otomatis — atur expireAfterSeconds sesuai kebijakan retensi Anda (lihat bagian retensi data di bawah):

def setup_indexes():
    solves.create_index([("submitted_at", DESCENDING)])
    solves.create_index([("type", ASCENDING), ("status", ASCENDING)])
    solves.create_index([("metadata.project", ASCENDING)])
    solves.create_index([("metadata.target_domain", ASCENDING)])
    solves.create_index(
        [("submitted_at", ASCENDING)],
        expireAfterSeconds=90 * 24 * 3600,  # Auto-delete after 90 days
        name="ttl_cleanup"
    )

setup_indexes()

Kalau MongoDB Anda berjalan di region Asia Tenggara — AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta) — jalankan worker CaptchaAI di region yang sama untuk memangkas round-trip ke database. Selisihnya kecil per solve, tapi terasa saat volume ribuan solve per hari dan jaringan mobile-first jadi bottleneck di sisi worker.

Kirim CAPTCHA dan Simpan Hasilnya

Fungsi ini menyimpan dokumen submitted lebih dulu, mengirim task ke in.php, lalu polling res.php sampai statusnya berubah — persis pola kirim → simpan task ID → polling → pakai token yang berlaku untuk semua jenis CAPTCHA di CaptchaAI:

def solve_and_store(sitekey, pageurl, captcha_type="recaptcha_v2", metadata=None):
    record = {
        "type": captcha_type,
        "method": "userrecaptcha",
        "sitekey": sitekey,
        "pageurl": pageurl,
        "status": "submitted",
        "submitted_at": datetime.now(timezone.utc),
        "metadata": metadata or {}
    }

    result = solves.insert_one(record)
    doc_id = result.inserted_id

    # Submit to CaptchaAI
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    })
    data = resp.json()

    if data.get("status") != 1:
        solves.update_one(
            {"_id": doc_id},
            {"$set": {"status": "error", "error": data.get("request")}}
        )
        return None

    captcha_id = data["request"]
    solves.update_one(
        {"_id": doc_id},
        {"$set": {"captcha_id": captcha_id, "status": "polling"}}
    )

    # Poll for result
    polls = 0
    for _ in range(60):
        time.sleep(5)
        polls += 1
        poll_resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get",
            "id": captcha_id, "json": 1
        }).json()

        if poll_resp.get("status") == 1:
            solved_at = datetime.now(timezone.utc)
            elapsed_ms = int(
                (solved_at - record["submitted_at"]).total_seconds() * 1000
            )
            solves.update_one({"_id": doc_id}, {"$set": {
                "status": "solved",
                "solution": poll_resp["request"],
                "solved_at": solved_at,
                "elapsed_ms": elapsed_ms,
                "polls": polls
            }})
            return poll_resp["request"]

        if poll_resp.get("request") != "CAPCHA_NOT_READY":
            solves.update_one({"_id": doc_id}, {"$set": {
                "status": "error",
                "error": poll_resp.get("request"),
                "polls": polls
            }})
            return None

    solves.update_one({"_id": doc_id}, {"$set": {
        "status": "timeout", "polls": polls
    }})
    return None

Kueri untuk Analitik

Empat fungsi ini menjawab pertanyaan operasional yang paling sering ditanyakan tim: berapa tingkat keberhasilan belakangan ini, jenis CAPTCHA mana yang paling lambat, bagaimana pola volume per jam, dan error apa yang paling sering muncul:

def get_success_rate(hours=24):
    """Success rate for the last N hours."""
    from datetime import timedelta
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)

    pipeline = [
        {"$match": {"submitted_at": {"$gte": cutoff}}},
        {"$group": {
            "_id": "$status",
            "count": {"$sum": 1}
        }}
    ]
    results = {r["_id"]: r["count"] for r in solves.aggregate(pipeline)}
    total = sum(results.values())
    solved = results.get("solved", 0)
    return (solved / total * 100) if total else 0


def get_avg_solve_time_by_type():
    """Average solve time grouped by CAPTCHA type."""
    pipeline = [
        {"$match": {"status": "solved"}},
        {"$group": {
            "_id": "$type",
            "avg_time_ms": {"$avg": "$elapsed_ms"},
            "min_time_ms": {"$min": "$elapsed_ms"},
            "max_time_ms": {"$max": "$elapsed_ms"},
            "count": {"$sum": 1}
        }},
        {"$sort": {"count": -1}}
    ]
    return list(solves.aggregate(pipeline))


def get_hourly_solve_volume(days=7):
    """Hourly solve volume for charting."""
    from datetime import timedelta
    cutoff = datetime.now(timezone.utc) - timedelta(days=days)

    pipeline = [
        {"$match": {"submitted_at": {"$gte": cutoff}}},
        {"$group": {
            "_id": {
                "date": {"$dateToString": {"format": "%Y-%m-%d", "date": "$submitted_at"}},
                "hour": {"$hour": "$submitted_at"}
            },
            "total": {"$sum": 1},
            "solved": {"$sum": {"$cond": [{"$eq": ["$status", "solved"]}, 1, 0]}}
        }},
        {"$sort": {"_id.date": 1, "_id.hour": 1}}
    ]
    return list(solves.aggregate(pipeline))


def get_error_breakdown(hours=24):
    """Error frequency by error code."""
    from datetime import timedelta
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)

    pipeline = [
        {"$match": {"submitted_at": {"$gte": cutoff}, "status": "error"}},
        {"$group": {"_id": "$error", "count": {"$sum": 1}}},
        {"$sort": {"count": -1}}
    ]
    return list(solves.aggregate(pipeline))

Implementasi Node.js

Logika yang sama dalam JavaScript untuk tim yang stack utamanya Node.js — koneksi, index, fungsi solve-and-store, dan penghitungan tingkat keberhasilan:

const { MongoClient } = require("mongodb");
const axios = require("axios");

const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017";
const API_KEY = process.env.CAPTCHAAI_API_KEY;

let db, solves;

async function connect() {
  const client = await MongoClient.connect(MONGO_URI);
  db = client.db("captcha_tracking");
  solves = db.collection("solves");

  await solves.createIndex({ submitted_at: -1 });
  await solves.createIndex({ type: 1, status: 1 });
  await solves.createIndex({ "metadata.project": 1 });
  await solves.createIndex(
    { submitted_at: 1 },
    { expireAfterSeconds: 90 * 24 * 3600 }
  );
}

async function solveAndStore(sitekey, pageurl, type = "recaptcha_v2", metadata = {}) {
  const submittedAt = new Date();
  const { insertedId } = await solves.insertOne({
    type, method: "userrecaptcha", sitekey, pageurl,
    status: "submitted", submitted_at: submittedAt, metadata,
  });

  const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
  });

  if (submit.data.status !== 1) {
    await solves.updateOne({ _id: insertedId }, { $set: { status: "error", error: submit.data.request } });
    return null;
  }

  const captchaId = submit.data.request;
  await solves.updateOne({ _id: insertedId }, { $set: { captcha_id: captchaId, status: "polling" } });

  let polls = 0;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    polls++;
    const poll = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
    });

    if (poll.data.status === 1) {
      const solvedAt = new Date();
      await solves.updateOne({ _id: insertedId }, { $set: {
        status: "solved", solution: poll.data.request,
        solved_at: solvedAt, elapsed_ms: solvedAt - submittedAt, polls,
      }});
      return poll.data.request;
    }
    if (poll.data.request !== "CAPCHA_NOT_READY") {
      await solves.updateOne({ _id: insertedId }, { $set: { status: "error", error: poll.data.request, polls } });
      return null;
    }
  }

  await solves.updateOne({ _id: insertedId }, { $set: { status: "timeout", polls } });
  return null;
}

async function getSuccessRate(hours = 24) {
  const cutoff = new Date(Date.now() - hours * 3600 * 1000);
  const pipeline = [
    { $match: { submitted_at: { $gte: cutoff } } },
    { $group: { _id: "$status", count: { $sum: 1 } } },
  ];
  const results = await solves.aggregate(pipeline).toArray();
  const total = results.reduce((s, r) => s + r.count, 0);
  const solved = results.find((r) => r._id === "solved")?.count || 0;
  return total ? ((solved / total) * 100).toFixed(1) : 0;
}

Strategi Retensi Data

Jangan simpan semuanya selamanya secara default — putuskan retensi sesuai kebutuhan sebelum koleksi membengkak:

Strategi Index TTL Kasus Pakai
Retensi 30 hari expireAfterSeconds: 2592000 Development/testing
Retensi 90 hari expireAfterSeconds: 7776000 Analitik produksi
Permanen (dengan arsip) Tanpa TTL; pakai capped collection atau cold storage Kepatuhan/audit

Untuk tim yang beroperasi di Indonesia, satu catatan kepatuhan yang relevan: UU Pelindungan Data Pribadi (UU 27/2022) dan UU ITE mengharuskan Anda hanya memproses data yang benar-benar berwenang Anda proses. Skema di atas mencatat metadata operasional (jenis CAPTCHA, waktu, status, domain target) — bukan data pribadi pengguna dari halaman yang di-scrape — tapi tetap tinjau field metadata dan pageurl Anda sendiri kalau ada kemungkinan berisi data pribadi pihak lain. Artikel ini bukan nasihat hukum.

Troubleshooting Umum

Masalah Penyebab Solusi
Kueri aggregation lambat Index pada submitted_at dan type belum dibuat Jalankan setup_indexes() — lihat bagian index di atas
Ukuran dokumen membengkak Solusi lengkap disimpan di setiap catatan Simpan hash solusi, atau potong setelah dipakai
TTL tidak kunjung menghapus catatan lama Monitor TTL MongoDB jalan tiap 60 detik; backlog besar butuh waktu Tunggu proses background; cek index lewat db.solves.getIndexes()
Connection pool habis Terlalu banyak operasi solve berjalan bersamaan Set maxPoolSize di connection string

Pertanyaan Umum

Apakah menyimpan riwayat solve CAPTCHA berisiko soal UU PDP?

Selama yang disimpan adalah metadata operasional (jenis, waktu, status, error) bukan data pribadi pengguna dari halaman target, risikonya rendah — tapi tetap audit field metadata dan pageurl di skema Anda sendiri, karena ini bukan nasihat hukum.

Index mana yang paling penting kalau volume solve-nya besar?

Index gabungan type + status dan index submitted_at menopang hampir semua kueri di panduan ini. Tanpa keduanya, kueri get_avg_solve_time_by_type() dan get_hourly_solve_volume() akan full collection scan begitu koleksi tumbuh ke jutaan dokumen.

Bisa pakai MongoDB Atlas untuk deployment di Asia Tenggara?

Bisa. Atlas mendukung index TTL dan aggregation pipeline penuh. Pilih region Atlas yang dekat dengan worker CaptchaAI Anda — misalnya region Singapura — supaya latensi koneksi tidak menambah waktu penyelesaian yang tercatat.

Berapa lama sebaiknya token solusi CAPTCHA disimpan?

Untuk debugging, 24–48 jam sudah cukup lalu biarkan index TTL yang membersihkan. Untuk analitik jangka panjang, simpan metadata saja (jenis, waktu, status, error) — token sudah tidak berguna setelah masa berlakunya habis.

Bagaimana cara memantau tingkat keberhasilan secara real-time?

Panggil get_success_rate() dengan window pendek (misalnya 1 jam) dari cron job atau endpoint monitoring, lalu bandingkan dengan window 24 jam. Selisih besar antar keduanya biasanya menandakan ada jenis CAPTCHA tertentu yang baru saja mulai bermasalah.

Langkah Selanjutnya

Lacak setiap solve CAPTCHA dan temukan masalah sebelum berdampak ke pipeline Anda — dapatkan API key CaptchaAI Anda.

Panduan terkait:

  • SQLite untuk caching CAPTCHA lokal
  • Manajemen TTL token Redis
  • Tren kinerja solve CAPTCHA dari waktu ke waktu
Komentar dinonaktifkan untuk artikel ini.