Tutoriales

Registros de auditoría para resolución CAPTCHA: trazabilidad y cumplimiento

Un registro de auditoría es la respuesta a una pregunta que siempre llega tarde: "¿por qué el mes pasado se resolvieron 40.000 CAPTCHA y de dónde salió ese gasto?". Si cada resolución deja una entrada bien estructurada, respondes en segundos lo que de otro modo te costaría reconstruir una mañana entera:

  • ¿Quién o qué inició la resolución?
  • ¿Para qué sitio fue?
  • ¿Cuánto costó y cuánto tardó?
  • ¿En qué momento ocurrió?

Esta guía muestra cómo construir ese registro para tus operaciones con CaptchaAI, campo por campo y con código listo para producción en Python y JavaScript.

Qué campos capturar en cada resolución

Cada resolución CAPTCHA debería dejar constancia de estos campos:

Campo Para qué sirve Ejemplo
timestamp Momento en que se hizo la solicitud 2026-04-04T14:30:00Z
request_id Identificador único de esta resolución uuid4()
captcha_type Método CAPTCHA utilizado userrecaptcha
target_site URL de la página que se está resolviendo https://staging.example.com/qa-login
task_id ID de tarea de CaptchaAI 73829451
status Resultado solved, failed, timeout
solve_time_ms Tiempo desde el envío hasta el resultado 18432
error_code Error en caso de fallo ERROR_CAPTCHA_UNSOLVABLE
initiator Quién o qué inició la resolución scraper-job-42
cost Coste estimado 0.003

Qué no registrar nunca

Nunca lleves al registro:

  • claves API
  • tokens CAPTCHA (son efímeros)
  • información de identificación personal de los sitios de destino

Este último punto no es solo higiene: el registro debe demostrar actividad sin acumular datos personales que después tengas que proteger. Anota la URL y el resultado; deja fuera cualquier dato del usuario final.

Registro de auditoría en Python

# audit_solver.py
import os
import uuid
import time
import json
import logging
from datetime import datetime, timezone
import requests

API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")

# Configure audit logger — separate from application logs
audit_logger = logging.getLogger("captcha_audit")
audit_logger.setLevel(logging.INFO)

# File handler with rotation
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
    "captcha_audit.jsonl",
    maxBytes=50_000_000,  # 50 MB per file
    backupCount=10,
)
handler.setFormatter(logging.Formatter("%(message)s"))
audit_logger.addHandler(handler)

def log_audit(record):
    """Write a structured audit record."""
    audit_logger.info(json.dumps(record, default=str))

def solve_with_audit(sitekey, pageurl, captcha_type="userrecaptcha",
                      initiator="unknown"):
    """Solve a CAPTCHA with full audit logging."""
    request_id = str(uuid.uuid4())
    start = time.time()

    audit_record = {
        "request_id": request_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "captcha_type": captcha_type,
        "target_site": pageurl,
        "initiator": initiator,
        "status": "submitted",
    }

    session = requests.Session()

    try:
        # Submit
        resp = session.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY,
            "method": captcha_type,
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": "1",
        })
        result = resp.json()

        if result.get("status") != 1:
            audit_record.update({
                "status": "submit_failed",
                "error_code": result.get("request"),
                "solve_time_ms": int((time.time() - start) * 1000),
            })
            log_audit(audit_record)
            return None

        task_id = result["request"]
        audit_record["task_id"] = task_id

        # Poll
        time.sleep(15)
        for _ in range(25):
            poll = session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "get",
                "id": task_id, "json": "1",
            })
            poll_result = poll.json()

            if poll_result.get("status") == 1:
                solve_time = int((time.time() - start) * 1000)
                audit_record.update({
                    "status": "solved",
                    "solve_time_ms": solve_time,
                    "cost_estimate": 0.003,  # Adjust per your rate
                })
                log_audit(audit_record)
                return poll_result["request"]

            if poll_result.get("request") != "CAPCHA_NOT_READY":
                audit_record.update({
                    "status": "failed",
                    "error_code": poll_result.get("request"),
                    "solve_time_ms": int((time.time() - start) * 1000),
                })
                log_audit(audit_record)
                return None

            time.sleep(5)

        audit_record.update({
            "status": "timeout",
            "solve_time_ms": int((time.time() - start) * 1000),
        })
        log_audit(audit_record)
        return None

    except Exception as e:
        audit_record.update({
            "status": "error",
            "error_code": str(e)[:200],
            "solve_time_ms": int((time.time() - start) * 1000),
        })
        log_audit(audit_record)
        raise

# Usage
token = solve_with_audit(
    sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    pageurl="https://www.google.com/recaptcha/api2/demo",
    initiator="price-scraper-v2",
)

La clave está en usar un logger dedicado (captcha_audit), separado de los logs de la aplicación, y escribir el bloque de auditoría en cada rama del flujo: envío fallido, resuelto, fallido, timeout y excepción. Así ninguna resolución se queda sin rastro.

Anatomía de una entrada JSONL

{"request_id":"a1b2c3d4-...","timestamp":"2026-04-04T14:30:00+00:00","captcha_type":"userrecaptcha","target_site":"https://www.google.com/recaptcha/api2/demo","initiator":"price-scraper-v2","status":"solved","task_id":"73829451","solve_time_ms":18432,"cost_estimate":0.003}

Una línea por resolución, JSON en cada línea: es el formato que consumen sin fricción tanto jq como agregadores tipo ELK o Datadog.

El mismo registro en JavaScript

// audit_solver.js
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');

const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';
const AUDIT_FILE = 'captcha_audit.jsonl';

function logAudit(record) {
  fs.appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n');
}

async function solveWithAudit(sitekey, pageurl, initiator = 'unknown') {
  const requestId = uuidv4();
  const start = Date.now();
  const record = {
    request_id: requestId,
    timestamp: new Date().toISOString(),
    captcha_type: 'userrecaptcha',
    target_site: pageurl,
    initiator,
    status: 'submitted',
  };

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

    if (submit.data.status !== 1) {
      record.status = 'submit_failed';
      record.error_code = submit.data.request;
      record.solve_time_ms = Date.now() - start;
      logAudit(record);
      return null;
    }

    record.task_id = submit.data.request;
    await new Promise(r => setTimeout(r, 15000));

    for (let i = 0; i < 25; i++) {
      const poll = await axios.get('https://ocr.captchaai.com/res.php', {
        params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
      });

      if (poll.data.status === 1) {
        record.status = 'solved';
        record.solve_time_ms = Date.now() - start;
        record.cost_estimate = 0.003;
        logAudit(record);
        return poll.data.request;
      }
      if (poll.data.request !== 'CAPCHA_NOT_READY') {
        record.status = 'failed';
        record.error_code = poll.data.request;
        record.solve_time_ms = Date.now() - start;
        logAudit(record);
        return null;
      }
      await new Promise(r => setTimeout(r, 5000));
    }

    record.status = 'timeout';
    record.solve_time_ms = Date.now() - start;
    logAudit(record);
    return null;
  } catch (e) {
    record.status = 'error';
    record.error_code = e.message.slice(0, 200);
    record.solve_time_ms = Date.now() - start;
    logAudit(record);
    throw e;
  }
}

El patrón es idéntico al de Python: un objeto record que se va completando y se persiste con appendFileSync en cada salida del flujo. Con alto volumen, conviene mover esa escritura a un stream en modo append.

Consultas y resúmenes del registro

Un registro solo vale lo que valen las preguntas que puedes hacerle. Con el formato JSONL basta un script corto para sacar el pulso diario.

Resumen diario de actividad

import json
from collections import Counter
from datetime import date

def daily_summary(log_file, target_date=None):
    """Generate a daily summary from audit logs."""
    target = target_date or date.today().isoformat()
    statuses = Counter()
    total_cost = 0
    solve_times = []

    with open(log_file) as f:
        for line in f:
            record = json.loads(line)
            if record["timestamp"].startswith(target):
                statuses[record["status"]] += 1
                total_cost += record.get("cost_estimate", 0)
                if record.get("solve_time_ms"):
                    solve_times.append(record["solve_time_ms"])

    print(f"Date: {target}")
    print(f"Total requests: {sum(statuses.values())}")
    print(f"Statuses: {dict(statuses)}")
    print(f"Estimated cost: ${total_cost:.2f}")
    if solve_times:
        print(f"Median solve time: {sorted(solve_times)[len(solve_times)//2]}ms")

daily_summary("captcha_audit.jsonl")

En una sola pasada obtienes el recuento por estado, el coste estimado del día y la mediana del tiempo de resolución. Es la base de un panel diario o de una alerta cuando la proporción de failed se dispara.

Errores frecuentes al registrar

Problema Causa Solución
El archivo de registro crece demasiado No hay rotación configurada Usa RotatingFileHandler o logrotate
Faltan entradas de auditoría Se lanza una excepción antes de registrar Registra dentro del bloque finally
Escrituras lentas con alto volumen I/O de archivo sincrónico Usa escrituras asíncronas o un búfer
Marcas de tiempo inconsistentes Desfase del reloj del sistema Usa NTP y registra siempre en UTC

Dónde y cuánto tiempo conservar los registros

El tamaño del registro escala de forma lineal con tu volumen, así que la estrategia de almacenamiento depende de cuántas resoluciones haces al día:

Volumen Tamaño diario del registro Almacenamiento mensual Recomendación
100 resoluciones/día ~30 KB ~1 MB Archivo local
1.000 resoluciones/día ~300 KB ~10 MB Archivo local + rotación
10.000 resoluciones/día ~3 MB ~100 MB Envío a un agregador de registros
100.000 resoluciones/día ~30 MB ~1 GB Registro centralizado (ELK, Datadog)

Como referencia de retención, 90 días es lo habitual para registros operativos; para requisitos regulatorios, ajusta el plazo a lo que exija tu sector.

Cómo encaja en tu programa de cumplimiento

Para una agencia o un integrador que factura en USD por el consumo, este registro es tu fuente de verdad a la hora de cuadrar el gasto mensual con la factura de CaptchaAI y de repartir costes por cliente o por proyecto. Además, la disciplina de no guardar datos personales lo convierte en evidencia útil frente a marcos habituales en la región sin crearte un pasivo nuevo:

  • GDPR y LOPDGDD (España y la UE)
  • LFPDPPP (México) y normas equivalentes en Latinoamérica
  • SOC 2, cuando el cliente exige controles operativos auditables

Respeta siempre los términos de servicio y la normativa de protección de datos aplicable; esto no es asesoramiento legal.

Preguntas frecuentes

¿Los registros de auditoría bastan para cumplir con GDPR o SOC 2?

No por sí solos. Son una evidencia sólida de qué se hizo, cuándo y con qué coste, pero el cumplimiento exige además controles de acceso, políticas de retención y trazabilidad de cambios. Trátalos como una pieza del expediente, no como el expediente completo.

¿Cómo cuadro el registro con la factura mensual de CaptchaAI?

Suma el campo cost_estimate de tus entradas del mes y compáralo con el panel de uso de CaptchaAI. Si ajustas ese valor a tu tarifa real, el resumen diario te da un contraste directo contra lo que verás en la factura.

¿Cómo evito que se pierdan entradas si una resolución lanza una excepción?

Registra en el bloque finally, no solo en las rutas de éxito. Así, aunque el flujo termine con un error inesperado, la resolución queda anotada con su estado error y su error_code.

¿Puedo detectar picos de coste anómalos con estos registros?

Sí. Al agrupar por initiator y por día ves qué proceso disparó el gasto. Un job que de pronto multiplica sus resoluciones aparece de inmediato en el resumen diario, antes de que llegue la factura.

Artículos relacionados


¿Listo para dejar rastro de cada resolución? Obtén tu API key de CaptchaAI y añade el registro de auditoría a tu primer flujo hoy mismo.

Guías relacionadas:

Los comentarios están deshabilitados para este artículo.