מערכת Webhook עם תיעוד וניסיונות חוזרים: התקנה סוהר
השקתם אינטגרציה עם שותף CRM. האירועים הראשונים עוברים, אבל שעה לאחר מכן הלקוח מתלונן שחצי מההודעות מעולם לא התקבלו. היומנים שותקים — המקלט החזיר 200, אבל לא עיבד את הנתונים. ללא פרטים על כל ניסיון, גילוי הגורם הוא ניחוש. אנו בונים מערכות Webhook עם תיעוד מלא וניסיונות חוזרים: לא רק 'ירה ושכח', אלא שליטה על כל אירוע. 10+ שנים באינטגרציות B2B, 100+ פרויקטים — אנו מבטיחים שקיפות ואמינות.
Webhook הוא בקשת HTTP יוצאת שאינכם שולטים בה לחלוטין. המקלט יכול להחזיר 200 מבלי לעבד את הנתונים. הוא יכול לקרוס 9 שניות לאחר הקבלה. הוא יכול לפספס אירוע ללא עקבות. ללא תיעוד מפורט של כל הניסיונות והיכולת לשלוח מחדש ידנית, ניפוי באגים באינטגרציה הוא כמעט בלתי אפשרי.
למה תיעוד כל ניסיון הוא הבסיס למערכת webhook אמינה
נניח שאירוע נכשל בניסיון השלישי לאחר פסק זמן. אם אינכם מאחסנים היסטוריה, אתם רואים רק את הסטטוס הסופי. עם תיעוד מלא, אתם מקבלים את התמונה המלאה: הניסיון הראשון נכשל עם 500, השני עם פסק זמן של 8 שניות, השלישי עם 502. זה מצביע מיד על בעיות בצד המקלט. מערכות ללא תיעוד לכל ניסיון מאלצות מפתחים להשקיע שעות בשחזור. הגישה שלנו מקצרת את זמן הניפוי בממוצע פי 5 בהשוואה לניטור מסורתי.
אילו נתונים אנו מתעדים וכיצד זה מיושם
קבוצת נתונים מינימלית לכל ניסיון משלוח:
| שדה | תיאור |
|---|---|
| delivery_id | UUID של המשלוח — מקשר את כל הניסיונות |
| attempt_number | מספר ניסיון (1, 2, 3...) |
| started_at | זמן התחלת הניסיון |
| duration_ms | משך זמן — חשוב לזיהוי פסקי זמן |
| request_headers | כותרות הבקשה (ללא סודות בטקסט פשוט) |
| request_body | גוף הבקשה (מטען האירוע) |
| response_code | סטטוס HTTP של התגובה |
| response_headers | כותרות התגובה |
| response_body | 2 KB הראשונים של גוף התגובה — לניפוי |
| error | טקסט השגיאה ב-ConnectionException / Timeout |
אנו מאחסנים ניסיונות בנפרד ממשלוחים — משלוח אחד יכול לכלול עד 8 ניסיונות. זה מאפשר צפייה בהיסטוריה המלאה והבנה באיזה שלב דברים השתבשו.
CREATE TABLE webhook_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
delivery_id UUID NOT NULL REFERENCES webhook_deliveries(id) ON DELETE CASCADE,
attempt_number INTEGER NOT NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_ms INTEGER,
request_body JSONB,
request_headers JSONB,
response_code INTEGER,
response_headers JSONB,
response_body TEXT, -- обрезается до 2000 символов
error_message TEXT,
success BOOLEAN NOT NULL DEFAULT false
);
CREATE INDEX idx_attempts_delivery ON webhook_attempts(delivery_id);
CREATE INDEX idx_attempts_started ON webhook_attempts(started_at DESC);
יישום התיעוד:
class WebhookAttemptLogger {
public function log( WebhookDelivery $delivery, int $attempt, WebhookAttemptData $data ): WebhookAttempt {
return WebhookAttempt::create([
'delivery_id' => $delivery->id,
'attempt_number' => $attempt,
'started_at' => $data->startedAt,
'duration_ms' => $data->durationMs,
'request_body' => $delivery->payload,
'request_headers' => $data->requestHeaders,
'response_code' => $data->responseCode,
'response_headers' => $data->responseHeaders,
'response_body' => $data->responseBody ? mb_substr($data->responseBody, 0, 2000) : null,
'error_message' => $data->errorMessage,
'success' => $data->success,
]);
}
}
class SendWebhookJob implements ShouldQueue {
public function handle( WebhookAttemptLogger $logger ): void {
$startedAt = now();
$requestHeaders = $this->buildHeaders();
try {
$response = Http::timeout(15)
->withHeaders($requestHeaders)
->post($this->delivery->subscription->endpoint_url, $this->delivery->payload);
$durationMs = (int)(microtime(true) * 1000 - $startedAt->timestamp * 1000);
$logger->log($this->delivery, $this->delivery->attempt_count, new WebhookAttemptData(
startedAt: $startedAt,
durationMs: $durationMs,
requestHeaders: $requestHeaders,
responseCode: $response->status(),
responseHeaders: $response->headers(),
responseBody: $response->body(),
success: $response->successful(),
));
if ($response->successful()) {
$this->delivery->markDelivered();
} else {
$this->delivery->scheduleRetry();
}
} catch (\Throwable $e) {
$durationMs = (int)(microtime(true) * 1000 - $startedAt->timestamp * 1000);
$logger->log($this->delivery, $this->delivery->attempt_count, new WebhookAttemptData(
startedAt: $startedAt,
durationMs: $durationMs,
requestHeaders: $requestHeaders,
errorMessage: get_class($e) . ': ' . $e->getMessage(),
success: false,
));
$this->delivery->scheduleRetry();
}
}
} כיצד מאורגן שליחה חוזרת ידנית
מנהל או מפתח חייבים להיות מסוגלים לשלוח מחדש כל אירוע ללא שינוי קוד. זה קריטי לניפוי אינטגרציות ולשחזור מכשלים.
class WebhookDeliveryController extends Controller {
// Повторить конкретную доставку
public function resend(WebhookDelivery $delivery): JsonResponse {
abort_if(
$delivery->status === 'delivered',
422,
'Delivery already succeeded'
);
$delivery->update([
'status' => 'pending',
'attempt_count' => 0,
'next_attempt_at' => now(),
]);
SendWebhookJob::dispatch($delivery);
return response()->json(['queued' => true]);
}
// Повторить все упавшие доставки подписки
public function resendFailed(WebhookSubscription $subscription): JsonResponse {
$count = WebhookDelivery::where('subscription_id', $subscription->id)
->where('status', 'failed')
->count();
WebhookDelivery::where('subscription_id', $subscription->id)
->where('status', 'failed')
->update([
'status' => 'pending',
'attempt_count' => 0,
'next_attempt_at' => now(),
]);
WebhookDelivery::where('subscription_id', $subscription->id)
->where('status', 'pending')
->each(fn($d) => SendWebhookJob::dispatch($d));
return response()->json(['requeued' => $count]);
}
// История попыток для конкретной доставки
public function attempts(WebhookDelivery $delivery): JsonResponse {
return response()->json(
$delivery->attempts()
->orderBy('attempt_number')
->get(['attempt_number', 'started_at', 'duration_ms', 'response_code', 'response_body', 'error_message', 'success'])
);
}
} תוכנית יישום שלב אחר שלב למערכת webhook עם תיעוד
| שלב | תיאור | משך זמן |
|---|---|---|
| ניתוח אינטגרציות קיימות | זיהוי סוגי אירועים ומקלטים | יום אחד |
| עיצוב סכמה | טבלאות: webhook_subscriptions, webhook_deliveries, webhook_attempts | יום אחד |
| יישום תיעוד | מחלקת WebhookAttemptLogger והתאמות ל-SendWebhookJob | יומיים |
| הגדרת מדיניות ניסיונות חוזרים | מרווחים, מספר ניסיונות מקסימלי (אנו ממליצים על 5-8) | יום אחד |
| יצירת לוח מחוונים | סינון לפי סטטוס, סוג אירוע, תאריך. מצטברים: ספירה ב-24 שעות, זמן משלוח P95 | יומיים |
| תיעוד API לשליחה חוזרת ידנית | Swagger/OpenAPI | יום אחד |
| בדיקות | סימולציית כשלים באמצעות stubs | יום אחד |
אסטרטגיית שמירת יומנים: ניסיונות מוצלחים — 30 יום עם גוף, לאחר מכן רק מטא-דאטה. ניסיונות כושלים — 90 יום לביקורת. גוף תגובת שגיאה — מקסימום 2 KB, נתונים בינאריים אינם נשמרים.
למה המערכת שלנו חוסכת עד 80% מזמן הניפוי
קובץ יומן טיפוסי חסר הקשר: אתם רואים שגיאה אבל לא מה הוביל אליה. המערכת שלנו מאחסנת את הכרונולוגיה המלאה של כל משלוח, ומקשרת את כל הניסיונות. זה מקצר את זמן החקירה משעות לדקות. מצטברים מובנים (ממוצע ניסיונות, זמן משלוח P95) מאפשרים גילוי מוקדם של אינטגרציות בעייתיות. השוו: ללא תיעוד — חיפוש ידני ביומני שרת, ניחושים, הפעלה מחדש של אינטגרציות. עם המערכת שלנו — פתחו את לוח המחוונים, סננו לפי סטטוס, ראו את ההיסטוריה של כל ניסיון. לחצו על "שלח מחדש" ואירועים כושלים נכנסים שוב לתור. אנו מיישמים זאת על תורי Laravel עם PostgreSQL. התוצאה היא חיסכון של עד 80% בזמן הניפוי.
מה כלול וציר זמן
- פיתוח מודול תיעוד ניסיונות ושליחה חוזרת
- הגדרת תור ומדיניות ניסיונות חוזרים
- יצירת לוח מחוונים עם סינון ומצטברים
- תיעוד API וסכמת נתונים
- הדרכת צוות על שימוש במערכת
- חודש אחד של תמיכה טכנית
ציר זמן: מערכת תיעוד ניסיונות ושליחה חוזרת ידנית — 3 עד 5 ימים. עם לוח מחוונים, מצטברים, סינון ומדיניות שמירה — 1 עד 1.5 שבועות.
הזמינו מערכת סוהר — צרו קשר להערכה מדויקת של הפרויקט שלכם. קבלו ייעוץ על היישום.







