Notifications
In-app and external notifications (Planned Feature)
๐ Overview
Notifications keep users informed about important events such as test failures, analysis completions, and project updates through multiple channels including in-app, email, and Slack.
๐ฏ Current Status
Status: ๐ Planned (Phase 4)
Notification system is not yet implemented. This is planned as part of the advanced features phase.
๐๏ธ Planned Architecture
Database Schema
notifications table:
{
id: string
userId: string
type: 'test_failure' | 'analysis_complete' | 'project_update' | 'mention'
title: string
message: string
data: object // Additional context
read: boolean
readAt: Date?
actionUrl: string? // Link to relevant page
createdAt: Date
}
notificationSettings table:
{
id: string
userId: string
channel: 'in_app' | 'email' | 'slack' | 'webhook'
events: string[] // Which events to notify about
enabled: boolean
config: object // Channel-specific config
createdAt: Date
updatedAt: Date
}
๐ฌ Notification Channels (Planned)
In-App Notifications
Features:
- Real-time updates via WebSocket
- Notification bell with badge count
- Toast notifications for important events
- Notification center/inbox
Implementation:
// WebSocket connection
const ws = new WebSocket(`wss://api.yourdomain.com/notifications`)
ws.onmessage = (event) => {
const notification = JSON.parse(event.data)
// Show toast
toast({
title: notification.title,
description: notification.message,
action: notification.actionUrl ? (
<Button asChild variant="outline">
<Link to={notification.actionUrl}>View</Link>
</Button>
) : undefined
})
// Update badge count
setBadgeCount(prev => prev + 1)
}
Email Notifications
Templates:
<!-- Test Failure Email -->
<html>
<body>
<h1>Test Run Failed</h1>
<p>Your test run for <strong>{{ projectName }}</strong> has failed.</p>
<table>
<tr>
<td>Total Tests:</td>
<td>{{ totalTests }}</td>
</tr>
<tr>
<td>Failed:</td>
<td>{{ failedTests }}</td>
</tr>
<tr>
<td>Pass Rate:</td>
<td>{{ passRate }}%</td>
</tr>
</table>
<a href="{{ actionUrl }}">View Details</a>
</body>
</html>
Configuration:
# .env
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=SG.xxxxx
SMTP_FROM=notifications@yourdomain.com
Slack Notifications
Setup:
- Create Slack app
- Add incoming webhook
- Store webhook URL in user settings
Message format:
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "๐จ Test Run Failed"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Project:*\nMy Project"
},
{
"type": "mrkdwn",
"text": "*Failed Tests:*\n5/50"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Details"
},
"url": "https://app.yourdomain.com/runs/123"
}
]
}
]
}
๐ API Reference (Planned)
Get Notifications
const notifications = await trpc.notifications.list.query({
read: false, // unread only
limit: 20
})
// Response
{
notifications: [
{
id: 'notif_123',
type: 'test_failure',
title: 'Test Run Failed',
message: 'Your test run for Project X failed with 5 errors',
data: {
runId: 'run_456',
projectId: 'proj_789'
},
actionUrl: '/dashboard/projects/proj_789/runs/run_456',
read: false,
createdAt: '2024-01-01T00:00:00Z'
}
],
unreadCount: 12
}
Mark as Read
await trpc.notifications.markRead.mutate({
notificationId: 'notif_123'
})
// Or mark all as read
await trpc.notifications.markAllRead.mutate()
Get Notification Settings
const settings = await trpc.notifications.getSettings.query()
// Response
{
settings: [
{
channel: 'email',
enabled: true,
events: ['test_failure', 'analysis_complete'],
config: {
email: 'user@example.com'
}
},
{
channel: 'slack',
enabled: false,
events: ['test_failure'],
config: {
webhookUrl: 'https://hooks.slack.com/...'
}
}
]
}
Update Settings
await trpc.notifications.updateSettings.mutate({
channel: 'email',
enabled: true,
events: ['test_failure', 'test_success', 'analysis_complete']
})
๐จ Frontend Implementation (Planned)
Notification Bell
export function NotificationBell() {
const { data } = trpc.notifications.list.useQuery({
read: false
})
const unreadCount = data?.unreadCount || 0
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<BellIcon />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<div className="flex justify-between items-center">
<h3 className="font-semibold">Notifications</h3>
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => markAllRead()}
>
Mark all read
</Button>
)}
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{data?.notifications.map((notification) => (
<NotificationItem
key={notification.id}
notification={notification}
/>
))}
</div>
</div>
</PopoverContent>
</Popover>
)
}
Notification Item
interface NotificationItemProps {
notification: Notification
}
export function NotificationItem({ notification }: NotificationItemProps) {
const markRead = trpc.notifications.markRead.useMutation()
const handleClick = () => {
if (!notification.read) {
markRead.mutate({ notificationId: notification.id })
}
if (notification.actionUrl) {
navigate(notification.actionUrl)
}
}
return (
<div
onClick={handleClick}
className={cn(
'p-3 rounded-lg cursor-pointer hover:bg-muted',
!notification.read && 'bg-blue-50'
)}
>
<div className="flex items-start gap-2">
<NotificationIcon type={notification.type} />
<div className="flex-1 space-y-1">
<p className="text-sm font-medium">{notification.title}</p>
<p className="text-sm text-muted-foreground">
{notification.message}
</p>
<p className="text-xs text-muted-foreground">
{formatDistanceToNow(notification.createdAt)} ago
</p>
</div>
{!notification.read && (
<div className="w-2 h-2 bg-blue-500 rounded-full" />
)}
</div>
</div>
)
}
Notification Settings Page
export function NotificationSettings() {
const { data } = trpc.notifications.getSettings.useQuery()
const updateSettings = trpc.notifications.updateSettings.useMutation()
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Notification Settings</h1>
<Card>
<CardHeader>
<CardTitle>Email Notifications</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label>Enable Email Notifications</Label>
<Switch
checked={data?.email?.enabled}
onCheckedChange={(enabled) =>
updateSettings.mutate({
channel: 'email',
enabled
})
}
/>
</div>
<div className="space-y-2">
<Label>Notify me about:</Label>
{['test_failure', 'test_success', 'analysis_complete'].map((event) => (
<div key={event} className="flex items-center space-x-2">
<Checkbox
id={event}
checked={data?.email?.events.includes(event)}
onCheckedChange={(checked) => {
const events = checked
? [...data.email.events, event]
: data.email.events.filter(e => e !== event)
updateSettings.mutate({
channel: 'email',
events
})
}}
/>
<label htmlFor={event}>{formatEventName(event)}</label>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Slack Notifications</CardTitle>
</CardHeader>
<CardContent>
{data?.slack?.webhookUrl ? (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>Slack Connected</Label>
<Button variant="outline" size="sm">
Disconnect
</Button>
</div>
{/* Event selection similar to email */}
</div>
) : (
<Button>Connect Slack</Button>
)}
</CardContent>
</Card>
</div>
)
}
๐ Event Types (Planned)
Test-Related
test_run.startedtest_run.completedtest_run.failedtest_coverage.threshold_failed
Analysis-Related
analysis.completedanalysis.failedtest_candidates.found
Project-Related
project.createdproject.updatedproject.deletedproject.member_added
Organization-Related
org.member_addedorg.member_removedorg.role_changed
๐งช Testing Notifications
describe('Notifications', () => {
it('creates notification on test failure', async () => {
await createTestRun({
status: 'FAILED',
projectId: 'proj_123'
})
const notifications = await db.query.notifications.findMany({
where: eq(notifications.type, 'test_failure')
})
expect(notifications.length).toBeGreaterThan(0)
})
})
๐ Related Documentation
- Webhooks: Programmatic notifications
- Projects: Project settings
- Test Execution: Test run events
Features section complete! Continue with Contributing โ