Webhooks
Event-driven integrations and notifications (Planned Feature)
๐ Overview
Webhooks enable real-time notifications to external systems when events occur in Izri, such as test runs completing, analyses finishing, or project updates.
๐ฏ Current Status
Status: ๐ Planned (Phase 4)
Webhook infrastructure is not yet implemented. This is planned alongside CI/CD integration features.
๐๏ธ Planned Architecture
Database Schema
webhooks table:
{
id: string
projectId: string? // Project-specific or org-wide
organizationId: string?
url: string // Webhook endpoint URL
events: string[] // Subscribed events
secret: string // For signature verification
active: boolean
createdAt: Date
updatedAt: Date
}
webhookDeliveries table:
{
id: string
webhookId: string
event: string
payload: object // Event data
requestHeaders: object
responseStatus: number?
responseBody: string?
attemptCount: number
deliveredAt: Date?
failedAt: Date?
nextRetryAt: Date?
createdAt: Date
}
๐ก Supported Events (Planned)
Test Events
// test_run.started
{
event: 'test_run.started',
data: {
runId: 'run_123',
projectId: 'proj_456',
type: 'unit',
startedAt: '2024-01-01T00:00:00Z'
}
}
// test_run.completed
{
event: 'test_run.completed',
data: {
runId: 'run_123',
projectId: 'proj_456',
status: 'success',
results: {
passed: 50,
failed: 2,
duration: 15000,
coverage: 85.5
}
}
}
// test_run.failed
{
event: 'test_run.failed',
data: {
runId: 'run_123',
projectId: 'proj_456',
error: 'Test execution timeout'
}
}
Analysis Events
// analysis.completed
{
event: 'analysis.completed',
data: {
analysisId: 'analysis_789',
projectId: 'proj_456',
commitSha: 'abc123...',
testCandidates: 45
}
}
// analysis.failed
{
event: 'analysis.failed',
data: {
projectId: 'proj_456',
error: 'Repository not accessible'
}
}
Project Events
// project.created
{
event: 'project.created',
data: {
projectId: 'proj_456',
name: 'My Project',
repository: 'https://github.com/user/repo'
}
}
// project.updated
// project.deleted
๐ API Reference (Planned)
Create Webhook
const webhook = await trpc.webhooks.create.mutate({
projectId: 'proj_123',
url: 'https://api.example.com/webhooks/izri',
events: ['test_run.completed', 'test_run.failed'],
secret: 'webhook_secret_here'
})
// Response
{
webhook: {
id: 'webhook_456',
url: 'https://api.example.com/webhooks/izri',
events: ['test_run.completed', 'test_run.failed'],
active: true,
createdAt: '2024-01-01T00:00:00Z'
}
}
List Webhooks
const webhooks = await trpc.webhooks.list.query({
projectId: 'proj_123'
})
Update Webhook
await trpc.webhooks.update.mutate({
webhookId: 'webhook_456',
events: ['test_run.completed'], // Change subscriptions
active: false // Disable temporarily
})
Delete Webhook
await trpc.webhooks.delete.mutate({
webhookId: 'webhook_456'
})
Test Webhook
// Send test ping
await trpc.webhooks.test.mutate({
webhookId: 'webhook_456'
})
// Sends:
{
event: 'ping',
data: {
webhookId: 'webhook_456',
timestamp: '2024-01-01T00:00:00Z'
}
}
List Deliveries
const deliveries = await trpc.webhooks.listDeliveries.query({
webhookId: 'webhook_456',
limit: 50
})
// Response
{
deliveries: [
{
id: 'delivery_789',
event: 'test_run.completed',
responseStatus: 200,
deliveredAt: '2024-01-01T00:00:00Z',
attemptCount: 1
}
]
}
Redeliver Webhook
// Retry failed delivery
await trpc.webhooks.redeliver.mutate({
deliveryId: 'delivery_789'
})
๐ Security
Signature Verification
Request headers:
X-Izri-Signature: sha256=abc123...
X-Izri-Event: test_run.completed
X-Izri-Delivery: delivery_789
Verification (in your webhook receiver):
import crypto from 'node:crypto'
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac('sha256', secret)
const digest = 'sha256=' + hmac.update(payload).digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
)
}
// Express/Hono handler
app.post('/webhooks/izri', async (c) => {
const signature = c.req.header('x-izri-signature')
const payload = await c.req.text()
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return c.json({ error: 'Invalid signature' }, 401)
}
const event = JSON.parse(payload)
// Process event...
return c.json({ received: true })
})
๐ Retry Logic (Planned)
Retry Strategy
const RETRY_DELAYS = [
1000, // 1 second
5000, // 5 seconds
30000, // 30 seconds
300000, // 5 minutes
1800000 // 30 minutes
]
async function deliverWebhook(webhook: Webhook, event: Event) {
let attemptCount = 0
while (attemptCount < RETRY_DELAYS.length) {
try {
const response = await sendWebhook(webhook, event)
if (response.status >= 200 && response.status < 300) {
// Success
await recordSuccess(webhook, event, response)
return
}
// Non-2xx response, retry
attemptCount++
await sleep(RETRY_DELAYS[attemptCount - 1])
} catch (error) {
// Network error, retry
attemptCount++
if (attemptCount >= RETRY_DELAYS.length) {
await recordFailure(webhook, event, error)
return
}
await sleep(RETRY_DELAYS[attemptCount - 1])
}
}
}
Automatic Disabling
// Disable webhook after consecutive failures
const FAILURE_THRESHOLD = 10
if (webhook.consecutiveFailures >= FAILURE_THRESHOLD) {
await db.update(webhooks)
.set({ active: false })
.where(eq(webhooks.id, webhook.id))
// Notify owner
await sendEmail({
to: webhook.owner.email,
subject: 'Webhook Disabled',
body: `Webhook ${webhook.url} disabled after ${FAILURE_THRESHOLD} failures`
})
}
๐จ Frontend Implementation (Planned)
Webhook Management
export function WebhookManagement({ projectId }: Props) {
const { data } = trpc.webhooks.list.useQuery({ projectId })
const createWebhook = trpc.webhooks.create.useMutation()
return (
<div className="space-y-4">
<div className="flex justify-between">
<h2 className="text-xl font-bold">Webhooks</h2>
<CreateWebhookDialog
projectId={projectId}
onSubmit={(values) => createWebhook.mutate(values)}
/>
</div>
<div className="space-y-2">
{data?.webhooks.map((webhook) => (
<Card key={webhook.id}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-mono">
{webhook.url}
</CardTitle>
<Badge variant={webhook.active ? 'default' : 'secondary'}>
{webhook.active ? 'Active' : 'Disabled'}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-1 mb-2">
{webhook.events.map((event) => (
<Badge key={event} variant="outline">
{event}
</Badge>
))}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => testWebhook(webhook.id)}
>
Test
</Button>
<Button
variant="outline"
size="sm"
onClick={() => viewDeliveries(webhook.id)}
>
Deliveries
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => deleteWebhook(webhook.id)}
>
Delete
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}
Delivery History
export function DeliveryHistory({ webhookId }: Props) {
const { data } = trpc.webhooks.listDeliveries.useQuery({ webhookId })
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Event</TableHead>
<TableHead>Status</TableHead>
<TableHead>Attempts</TableHead>
<TableHead>Delivered At</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.deliveries.map((delivery) => (
<TableRow key={delivery.id}>
<TableCell>{delivery.event}</TableCell>
<TableCell>
<StatusBadge status={delivery.responseStatus} />
</TableCell>
<TableCell>{delivery.attemptCount}</TableCell>
<TableCell>
{delivery.deliveredAt ? format(delivery.deliveredAt, 'PPp') : '-'}
</TableCell>
<TableCell>
{delivery.failedAt && (
<Button
size="sm"
variant="outline"
onClick={() => redeliver(delivery.id)}
>
Retry
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
๐งช Testing Webhooks
Local Development
# Use ngrok or similar to expose local server
ngrok http 3000
# Add webhook with ngrok URL
https://abc123.ngrok.io/webhooks/izri
Mock Webhook Server
import express from 'express'
const app = express()
app.post('/webhooks/izri', express.json(), (req, res) => {
console.log('Received webhook:', req.body)
console.log('Headers:', req.headers)
// Verify signature
const signature = req.headers['x-izri-signature']
// ... verification logic
res.json({ received: true })
})
app.listen(3000)
๐ Related Documentation
- API Keys: Authentication for webhooks
- Notifications: Alternative notification methods
- GitHub Integration: Incoming GitHub webhooks
Next: Notifications โ