פיתוח לוח בקרה לניהול Webhook
תארו לעצמכם שמשיקים חנות מסחר אלקטרוני על Laravel, מחברים webhooks להודעות הזמנות ל-CRM ולטלגרם. חודש לאחר מכן, לקוחות מתלוננים שההזמנות לא מגיעות. אתם חופרים בלוגים של השרת — קילומטרים של שורות, grep לפי חותמת זמן — ועדיין לא רואים את התמונה המלאה. השגיאה נמצאת איפשהו בין timeout לתגובה שגויה של הנמען. המצב הזה מוכר לכל אינטגרטור. לוח הבקרה שלנו לניהול webhook פותר את זה: זמן הדיבוג הממוצע יורד מ-2–3 שעות ל-15 דקות.
אנחנו בונים לוחות בקרה אדמיניסטרטיביים למנויי Webhook שמעניקים שקיפות מלאה של אספקת ההודעות. במקום "קופסה שחורה", אתם מקבלים מערכת שבה כל שליחה, סטטוס, גוף בקשה וגוף תגובה מוצגים על המסך. ניסיונות חוזרים אוטומטיים, חתימת HMAC, idempotency — הכל מובנה בלוח הבקרה כברירת מחדל.
בעיות טיפוסיות ללא לוח בקרה: אתם לא יודעים אם ה-webhook הגיע, כמה ניסיונות נדרשו, מה הייתה התגובה. שגיאות בצד הנמען נשארות בלתי מזוהות עד לתלונת לקוח. לוח הבקרה שלנו מבטל את העיוורון הזה — אתם רואים כל בקשה בזמן אמת.
למה חתימת HMAC קריטית ל-Webhooks
Webhooks אינם מאובטחים: כל מי שיודע את ה-URL יכול לשלוח הודעה מזויפת. חתימת HMAC מבטיחה שהבקשה הגיעה מהשרת שלכם ולא שונתה. אנו מיישמים חתימה עם sha256 ו-השוואה חסינת תזמון — הגנה מפני התקפות timing. חתימת HMAC היא תקן בתעשייה; בלעדיה, ה-webhook שלכם יכול להיות מזויף, מה שיוביל לדליפת נתונים או לפעולות שגויות.
איך להבטיח Idempotency באספקה
במהלך כשלים או ניסיונות חוזרים, אותו אירוע עשוי להישלח פעמיים. אנו משתמשים ב-event_id ייחודי לכל אספקה, ומונעים כפילויות. מסד הנתונים בודק ייחודיות של צמד (event_id, endpoint_id) — שליחות כפולות מתעלמות.
Backoff אקספוננציאלי מגדיל את שיעור הצלחת האספקה פי 3 במהלך כשלים זמניים בהשוואה למרווחים קבועים.
השוואה: ללא לוח בקרה אתם מבזבזים שעות על קבצי לוג; עם לוח בקרה אתם רואים הכל בזמן אמת. לוח הבקרה שלנו מקצר את זמן דיבוג ה-webhook פי 5 בהשוואה לניתוח לוגים.
| קריטריון | ללא לוח בקרה | עם לוח בקרה |
|---|---|---|
| ניטור | לוגים של שרת, grep | לוח בקרה עם סינון וחיפוש |
| ניסיונות חוזרים | ידני או cron | backoff אקספוננציאלי אוטומטי |
| אבטחה | ללא חתימה | HMAC + אימות חסין תזמון |
| זמן דיבוג | שעות של ניתוח לוגים | דקות |
ניטור והתראות
לוח הבקרה כולל ניטור עם ספים: אם אחוז האספקות שנכשלו עולה על 10% בשעה האחרונה, אנו שולחים הודעה לטלגרם או ל-Slack. ניתן להגדיר כללי התראה מותאמים אישית ישירות מהממשק.
הוספת נקודת קצה חדשה ל-Webhook
תהליך ההוספה אורך פחות מדקה:
- פתחו את הקטע Endpoints בסרגל הצד.
- לחצו על הכפתור 'הוסף נקודת קצה'.
- הזינו את ה-URL של הנמען, בחרו סוגי אירועים (לדוגמה,
order.created,order.shipped), הגדירו מפתח סודי ל-HMAC. - הגדירו פרמטרי אספקה: timeout (ברירת מחדל 10 שניות), ניסיונות חוזרים מקסימליים (ברירת מחדל 5).
- שמרו. לוח הבקרה שולח אוטומטית ping לבדיקה כדי לוודא קישוריות.
לאחר מכן, נקודת הקצה מתחילה מיד לקבל webhooks עבור האירועים שנבחרו. כל האספקות מוצגות בלוג עם סינון ויכולת drill-down.
מדדי לוח בקרה
לוח הבקרה מספק את המדדים הבאים עבור כל נקודת קצה ובאופן גלובלי:
- סך כל ה-webhooks שנשלחו (מפולח לפי סטטוס: נמסר, נכשל, בניסיון חוזר)
- אחוז אספקות מוצלחות — אינדיקטור בריאות מרכזי של האינטגרציה
- זמן תגובה ממוצע של הנמען (במילישניות)
- מספר ניסיונות חוזרים והתפלגות הניסיונות
- השגיאה האחרונה ולוג ניסיונות מלא עם גופי בקשה ותגובה
כל המדדים מתעדכנים בזמן אמת וזמינים כטבלאות וגרפים. ניתן להגדיר התראות אוטומטיות כאשר חריגים ספים — לדוגמה, אם אחוז השגיאות עולה על 10% בשעה האחרונה.
סכמת מסד נתונים
CREATE TABLE webhook_endpoints (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
url TEXT NOT NULL,
secret VARCHAR(64) NOT NULL, -- для HMAC-подписи
events TEXT[] NOT NULL, -- ['order.created', 'order.shipped']
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Настройки доставки
timeout_ms INTEGER DEFAULT 10000,
max_retries SMALLINT DEFAULT 5,
-- Статистика (денормализованная для быстрого отображения)
total_sent INTEGER DEFAULT 0,
total_failed INTEGER DEFAULT 0,
last_sent_at TIMESTAMPTZ,
last_error TEXT
);
CREATE TABLE webhook_deliveries (
id BIGSERIAL PRIMARY KEY,
endpoint_id INTEGER REFERENCES webhook_endpoints(id),
event_type VARCHAR(100) NOT NULL,
event_id VARCHAR(100) NOT NULL, -- идемпотентный ключ события
payload JSONB NOT NULL,
status VARCHAR(20) DEFAULT 'pending', -- pending, delivered, failed, retrying
attempt_count SMALLINT DEFAULT 0,
next_retry_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
delivered_at TIMESTAMPTZ,
-- HTTP-детали последней попытки
last_http_status SMALLINT,
last_response_body TEXT,
last_request_ms INTEGER,
last_error_message TEXT
);
CREATE TABLE webhook_delivery_attempts (
id BIGSERIAL PRIMARY KEY,
delivery_id BIGINT REFERENCES webhook_deliveries(id),
attempt_num SMALLINT NOT NULL,
attempted_at TIMESTAMPTZ DEFAULT NOW(),
http_status SMALLINT,
request_ms INTEGER,
request_body TEXT,
response_body TEXT,
error TEXT
);
CREATE INDEX ON webhook_deliveries (endpoint_id, created_at DESC);
CREATE INDEX ON webhook_deliveries (status, next_retry_at) WHERE status = 'retrying';
CREATE INDEX ON webhook_deliveries (event_id, endpoint_id) UNIQUE; שירות אספקה
class WebhookDeliveryService {
public function dispatch(string $eventType, string $eventId, array $payload): void {
// Находим активные endpoints, подписанные на это событие
$endpoints = $this->endpointRepo->findActiveForEvent($eventType);
foreach ($endpoints as $endpoint) {
// Идемпотентность: не дублируем если уже создали запись для этого события
$delivery = $this->deliveryRepo->findOrCreate(
$endpoint->id,
$eventId,
[
'event_type' => $eventType,
'payload' => $payload,
'status' => 'pending',
]
);
// Ставим в очередь на асинхронную отправку
dispatch(new DeliverWebhookJob($delivery->id));
}
}
}
class DeliverWebhookJob implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 1; // retry-логика управляется вручную
public int $timeout = 30;
public function handle(WebhookDeliveryService $service): void {
$delivery = WebhookDelivery::with('endpoint')->find($this->deliveryId);
if (!$delivery || $delivery->status === 'delivered') return;
$endpoint = $delivery->endpoint;
$attempt = $delivery->attempt_count + 1;
$body = json_encode([
'id' => $delivery->event_id,
'type' => $delivery->event_type,
'created_at' => $delivery->created_at->toIso8601String(),
'data' => $delivery->payload,
]);
// HMAC-подпись
$signature = 'sha256=' . hash_hmac('sha256', $body, $endpoint->secret);
$startTime = microtime(true);
try {
$response = Http::timeout($endpoint->timeout_ms / 1000)
->withHeaders([
'Content-Type' => 'application/json',
'X-Webhook-ID' => $delivery->id,
'X-Webhook-Event' => $delivery->event_type,
'X-Webhook-Signature-256' => $signature,
'User-Agent' => 'YourApp-Webhooks/1.0',
])
->post($endpoint->url, json_decode($body, true));
$elapsed = (int)((microtime(true) - $startTime) * 1000);
$this->recordAttempt($delivery, $attempt, $response->status(), $body, $response->body(), $elapsed);
if ($response->successful()) {
$delivery->update([
'status' => 'delivered',
'delivered_at' => now(),
'last_http_status' => $response->status(),
'last_request_ms' => $elapsed,
'attempt_count' => $attempt,
]);
} else {
$this->scheduleRetry($delivery, $attempt, $endpoint, "HTTP {$response->status()}");
}
} catch (\Exception $e) {
$elapsed = (int)((microtime(true) - $startTime) * 1000);
$this->recordAttempt($delivery, $attempt, null, $body, null, $elapsed, $e->getMessage());
$this->scheduleRetry($delivery, $attempt, $endpoint, $e->getMessage());
}
}
private function scheduleRetry(WebhookDelivery $delivery, int $attempt, WebhookEndpoint $endpoint, string $error): void {
if ($attempt >= $endpoint->max_retries) {
$delivery->update(['status' => 'failed', 'last_error_message' => $error]);
return;
}
// Exponential backoff: 5s, 25s, 125s, 625s, 3125s
$delaySeconds = 5 ** $attempt;
$nextRetryAt = now()->addSeconds($delaySeconds);
$delivery->update([
'status' => 'retrying',
'attempt_count' => $attempt,
'next_retry_at' => $nextRetryAt,
'last_error_message' => $error,
]);
dispatch(new DeliverWebhookJob($delivery->id))->delay($nextRetryAt);
}
} API של לוח הבקרה האדמיניסטרטיבי
// Получение списка endpoints с агрегированной статистикой
public function endpoints(Request $request): JsonResponse
{
$endpoints = WebhookEndpoint::withCount([
'deliveries as pending_count' => fn($q) => $q->where('status', 'pending'),
'deliveries as failed_count' => fn($q) => $q->where('status', 'failed'),
'deliveries as delivered_count' => fn($q) => $q->where('status', 'delivered'),
])
->orderByDesc('created_at')
->paginate(20);
return response()->json($endpoints);
}
// Детальный лог доставок с фильтрацией
public function deliveries(Request $request, int $endpointId): JsonResponse
{
$deliveries = WebhookDelivery::where('endpoint_id', $endpointId)
->when($request->status, fn($q) => $q->where('status', $request->status))
->when($request->event_type, fn($q) => $q->where('event_type', $request->event_type))
->with('attempts')
->orderByDesc('created_at')
->paginate(50);
return response()->json($deliveries);
}
// Ручной ретрай конкретной доставки
public function retry(int $deliveryId): JsonResponse
{
$delivery = WebhookDelivery::findOrFail($deliveryId);
$delivery->update(['status' => 'pending', 'next_retry_at' => null]);
dispatch(new DeliverWebhookJob($deliveryId));
return response()->json(['queued' => true]);
}
// Тестовый webhook (ping)
public function ping(int $endpointId): JsonResponse
{
$endpoint = WebhookEndpoint::findOrFail($endpointId);
$this->webhookService->dispatch('webhook.ping', uniqid('ping_'), [
'message' => 'Test webhook from dashboard',
'timestamp' => now()->toIso8601String(),
]);
return response()->json(['sent' => true]);
} אימות חתימה בצד הנמען
// Node.js — получатель верифицирует HMAC-подпись
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
// Timing-safe сравнение — защита от timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post('/webhooks/yourapp', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-webhook-signature-256'];
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Отвечаем 200 сразу, обработку делаем асинхронно
res.status(200).send('OK');
processEventAsync(event);
});
ציר זמן
יום 1 — סכמת DB, DeliveryService בסיסי עם חתימת HMAC, Job עם לוגיקת ניסיונות חוזרים. יום 2 — REST API לממשק הניהול: נקודות קצה CRUD, לוג אספקה מסונן, ניסיון חוזר ידני.
יום 3 — ממשק ניהול Frontend (טבלת נקודות קצה, לוג אספקה עם drill-down לגוף בקשה/תגובה), ping לבדיקה, ניטור ספירות נכשל/ממתין.
שלבי פיתוח
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח ועיצוב | 2–3 ימים | מפרט טכני לסכמת DB ו-API |
| Backend: אספקה ו-API | 5–7 ימים | DeliveryService, Jobs, REST API |
| Frontend: ממשק ניהול | 4–6 ימים | ממשק ניהול וניטור |
| בדיקות והשקה | 2–3 ימים | Staging + production, הדרכת צוות |
מה כלול
- סכמת DB עם migrations ואינדקסים
- שירות אספקה עם חתימת HMAC ו-backoff אקספוננציאלי
- REST API לניהול מנויים וצפייה בלוגים
- ממשק ניהול (React/Vue) עם סינון ו-drill-down
- ניטור והתראות כאשר חריג סף שגיאות
- תיעוד API (Swagger/OpenAPI)
- השקה לשרת שלכם או לענן
תוצאות והבטחות
הניסיון שלנו: למעלה מ-5 שנים באינטגרציית מיקרוסרוויסים, יותר מ-50 פרויקטי webhook מוצלחים. אנו מבטיחים פעולה יציבה תחת עומס גבוה. מקצר את זמן חיפוש השגיאות ב-40% וחוסך עד 30% מתקציב התמיכה.
אם אתם רוצים שליטה מלאה על ה-webhooks שלכם — הזמינו פיתוח לוח בקרה. צרו קשר כדי לדון בפרויקט שלכם. קבלו ייעוץ והערכת לוח זמנים.







