О том, что конвейер решения CAPTCHA сломался, команда обычно узнаёт последней — от аналитика, у которого утренняя выгрузка пришла пустой. Чтобы этого не происходило, достаточно трёх типов сообщений в Slack: сбой отдельной задачи, падение баланса ниже порога и рост доли ошибок на скользящем окне. Ниже — готовый код на Python и Node.js, который ставится на действующий конвейер без внешних систем мониторинга.
Slack здесь удобен не тем, что он красивее логов, а тем, что он уже открыт у дежурного. Incoming Webhook — это один POST-запрос с JSON, без SDK, без OAuth и без отдельного сервиса, который потом придётся поддерживать.
Что вообще стоит отправлять в канал
Перед кодом полезно договориться о границе между «событием для лога» и «событием для человека». В Slack идёт только второе:
- Состояние счёта. Баланс ниже порога — это событие, которое решается только человеком и только заранее.
- Сломанная интеграция. Неверный
sitekey, истёкший API-ключ, смена типа CAPTCHA на целевой странице. - Статистика с трендом. Не каждая ошибка, а доля ошибок за последние N попыток.
Отдельный тайм-аут при решении — нормальная часть работы сети, его место в логе. Если отправлять в канал всё подряд, через неделю его просто замьютят.
Шаг 1: создайте webhook в Slack
- Откройте api.slack.com/apps → Create New App
- Выберите Incoming Webhooks → Activate
- Нажмите Add New Webhook to Workspace → укажите канал
- Скопируйте URL webhook
URL равносилен праву писать в канал, поэтому храните его рядом с API-ключом — в переменных окружения, а не в репозитории.
Шаг 2: базовая функция отправки на Python
Все три алерта строятся над одним помощником. Цвет вложения задаёт визуальный приоритет, а fields раскладывает контекст по колонкам, чтобы сообщение читалось с телефона.
import requests
import json
from datetime import datetime
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T00/B00/xxx"
def send_slack_alert(title, message, color="#ff0000", fields=None):
"""Send a formatted Slack alert."""
attachment = {
"color": color,
"title": title,
"text": message,
"ts": int(datetime.now().timestamp()),
}
if fields:
attachment["fields"] = [
{"title": k, "value": str(v), "short": True}
for k, v in fields.items()
]
payload = {"attachments": [attachment]}
resp = requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=10)
return resp.status_code == 200
Тайм-аут 10 с здесь не декорация: без него висящий запрос к Slack способен заблокировать воркер, который должен был решать задачи.
Шаг 3: алерт о сбое задачи
Первый тип сообщения — конкретный код ошибки по ID задачи. Он нужен в основном во время выкатки новой интеграции, когда важно видеть каждый промах.
def notify_solve_failure(task_id, captcha_type, error_code, site_url):
send_slack_alert(
title="CAPTCHA Solve Failed",
message=f"Task `{task_id}` failed with `{error_code}`",
color="#ff0000",
fields={
"Type": captcha_type,
"Error": error_code,
"Site": site_url,
"Time": datetime.now().strftime("%H:%M:%S"),
},
)
# Use after a failed solve
result = poll_for_result(task_id)
if result.get("error"):
notify_solve_failure(task_id, "recaptcha_v2", result["error"], "https://example.com")
В боевом режиме этот алерт обычно сужают до невосстанавливаемых ошибок — неверный ключ, нулевой баланс, неподдерживаемый тип. Здесь же полезно свериться с матрицей поддержки: CaptchaAI решает reCAPTCHA v2 и v3, Cloudflare Turnstile и Challenge, GeeTest v3, текстовые и grid-капчи, BLS; CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) пока доступны только в бета-режиме. hCaptcha и FunCaptcha не поддерживаются, поддержка GeeTest v4 заявлена как «скоро». Если сайт молча переехал на неподдерживаемый тип, поток ошибок в Slack станет первым сигналом.
Шаг 4: контроль баланса через res.php
Баланс читается одним GET-запросом к res.php с action=getbalance. Опрос раз в пять минут покрывает практически любой темп расхода.
def check_balance_alert(api_key, threshold=5.0):
"""Alert when balance drops below threshold."""
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key, "action": "getbalance", "json": "1"
}).json()
balance = float(resp.get("request", 0))
if balance < threshold:
send_slack_alert(
title="Low CaptchaAI Balance",
message=f"Balance is ${balance:.2f} (threshold: ${threshold:.2f})",
color="#ff9900",
fields={
"Current Balance": f"${balance:.2f}",
"Threshold": f"${threshold:.2f}",
},
)
return balance
# Run periodically
import threading
def balance_monitor(api_key, interval=300):
"""Check balance every 5 minutes."""
check_balance_alert(api_key)
timer = threading.Timer(interval, balance_monitor, args=[api_key, interval])
timer.daemon = True
timer.start()
balance_monitor("YOUR_API_KEY")
Тарифы CaptchaAI считаются по потокам, а не по числу решений: BASIC ($15/мес, 5 потоков), STANDARD ($30/мес, 15 потоков), ADVANCE ($90/мес, 50 потоков), дальше PREMIUM ($170/мес, 100 потоков), CORPORATE ($240/мес, 150 потоков), ENTERPRISE ($300/мес, 200 потоков) и уровни VIP-1 ($1,500), VIP-2 ($4,500), VIP-3 ($7,500). Поэтому алерт по балансу по сути говорит о сроке продления подписки, а не об «остатке решений».
Для команд в Минске, Алматы или Тбилиси, где трансграничный платёж по карте может пройти не с первого раза, именно этот алерт обычно окупается быстрее остальных. Ставьте порог с запасом на несколько дней работы, а не на несколько часов.
Шаг 5: доля ошибок на скользящем окне
Самый полезный сигнал — не единичная ошибка, а изменение фона. Класс ниже держит окно из 50 последних попыток, не стреляет раньше 20 замеров и соблюдает паузу в 300 с между сообщениями.
from collections import deque
class ErrorRateNotifier:
def __init__(self, window=50, threshold=0.3, cooldown=300):
self.results = deque(maxlen=window)
self.threshold = threshold
self.cooldown = cooldown
self.last_alert = 0
def record(self, success):
self.results.append(success)
if len(self.results) < 20:
return
error_rate = 1 - sum(self.results) / len(self.results)
import time
now = time.time()
if error_rate > self.threshold and (now - self.last_alert) > self.cooldown:
self.last_alert = now
send_slack_alert(
title="High CAPTCHA Error Rate",
message=f"Error rate: {error_rate:.0%} over last {len(self.results)} tasks",
color="#ff0000",
fields={
"Error Rate": f"{error_rate:.1%}",
"Window": f"{len(self.results)} tasks",
"Threshold": f"{self.threshold:.0%}",
},
)
notifier = ErrorRateNotifier()
# After each solve attempt
notifier.record(success=True) # solved
notifier.record(success=False) # failed
Порог 0,3 — разумное начальное значение, но его стоит подобрать по своей статистике за пару недель. Если вы работаете с несколькими типами сразу, заведите отдельный экземпляр ErrorRateNotifier на каждый — иначе стабильный поток reCAPTCHA v2 замаскирует проблему с Cloudflare Turnstile или GeeTest v3.
То же самое на Node.js
Если конвейер живёт в Node.js, логика повторяется один в один: тот же формат вложения, те же два алерта и setInterval вместо threading.Timer.
const axios = require('axios');
const SLACK_WEBHOOK = 'https://hooks.slack.com/services/T00/B00/xxx';
async function sendSlackAlert(title, message, color = '#ff0000', fields = {}) {
const attachment = {
color,
title,
text: message,
ts: Math.floor(Date.now() / 1000),
fields: Object.entries(fields).map(([k, v]) => ({
title: k, value: String(v), short: true,
})),
};
await axios.post(SLACK_WEBHOOK, { attachments: [attachment] });
}
// Failure alert
async function notifySolveFailure(taskId, type, error) {
await sendSlackAlert(
'CAPTCHA Solve Failed',
`Task \`${taskId}\` failed: \`${error}\``,
'#ff0000',
{ Type: type, Error: error }
);
}
// Balance alert
async function checkBalance(apiKey, threshold = 5.0) {
const resp = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: apiKey, action: 'getbalance', json: 1 },
});
const balance = parseFloat(resp.data.request);
if (balance < threshold) {
await sendSlackAlert(
'Low CaptchaAI Balance',
`Balance: $${balance.toFixed(2)}`,
'#ff9900',
{ Balance: `$${balance.toFixed(2)}`, Threshold: `$${threshold.toFixed(2)}` }
);
}
return balance;
}
// Periodic check
setInterval(() => checkBalance('YOUR_API_KEY'), 5 * 60 * 1000);
Ежедневная сводка вместо потока сообщений
Дайджест раз в сутки снимает большую часть шума: команда видит динамику, а канал остаётся читаемым.
def send_daily_summary(stats):
"""Send a daily digest to Slack."""
send_slack_alert(
title="Daily CAPTCHA Summary",
message=f"{stats['total']} tasks processed",
color="#36a64f",
fields={
"Solved": stats["solved"],
"Failed": stats["failed"],
"Avg Solve Time": f"{stats['avg_time_ms']}ms",
"Total Cost": f"${stats['total_cost']:.2f}",
"Success Rate": f"{stats['success_rate']:.1%}",
},
)
Поля в сводке заполняются из вашей собственной телеметрии — тех же счётчиков, что и в структурированном журналировании операций CAPTCHA. Если телеметрии пока нет, сначала заведите её, а потом возвращайтесь к дайджесту.
Где обычно ломается
| Проблема | Причина | Что сделать |
|---|---|---|
| Webhook отвечает 403 | URL неверен или отозван | Пересоздайте webhook в Slack |
| Слишком много сообщений | Нет паузы между алертами | Добавьте cooldown от 300 с |
| Сообщения приходят с задержкой | Запрос без тайм-аута | Задайте timeout=10 в клиенте |
| Канал пустой | Webhook привязан к другому каналу | Проверьте привязку в настройках приложения |
| Алерты игнорируют | Порог выставлен слишком низко | Поднимите порог и увеличьте окно |
Отдельно про содержимое сообщений: канал Slack — это ещё одно место хранения данных. Если в вашем контуре есть персональные данные, проверьте требования, под которые вы попадаете, — для проектов в РФ это 152-ФЗ «О персональных данных». На практике безопаснее отправлять в канал ID задачи, тип и код ошибки, а не полезную нагрузку целиком.
Часто задаваемые вопросы
Насколько часто безопасно опрашивать баланс?
Раз в 5 мин более чем достаточно: это 288 запросов в сутки и практически нулевая нагрузка. Запрос баланса не занимает поток и не мешает решать задачи параллельно.
Можно ли обойтись без Slack и писать в Telegram?
Да. Логика порогов от транспорта не зависит: замените тело send_slack_alert на вызов Bot API и соберите те же поля в обычный текст. Остальной код менять не нужно.
Куда ставить проверку баланса при запуске в Docker?
В тот же процесс, что и воркеры, но отдельным фоновым потоком (daemon=True в примере выше). Если контейнеров несколько, оставьте проверку баланса только в одном — иначе одно событие превратится в N одинаковых сообщений.
Как понять, что пороги настроены верно?
Простое правило: если за неделю ни один алерт не привёл к действию, пороги слишком чувствительны. Если команда узнала о сбое из другого источника — слишком высокие.
Нужен ли отдельный канал под эти сообщения?
Да, и желательно два: один для срочных алертов с уведомлениями, второй — для ежедневных сводок без звука. Так дежурный видит только то, на что надо реагировать сейчас.
Подключите мониторинг к своему конвейеру
Получите API-ключ на captchaai.com, подставьте URL webhook в примеры выше — и первые алерты придут в тот же день.