mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 07:51:13 -05:00
Refactor notification system
This commit is contained in:
304
docs/NOVU_MIGRATION.md
Normal file
304
docs/NOVU_MIGRATION.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# Migrating from Legacy to Novu Notifications
|
||||||
|
|
||||||
|
This guide covers migrating from the old notification system to the new Novu-powered system.
|
||||||
|
|
||||||
|
## Migration Overview
|
||||||
|
|
||||||
|
The new notification system:
|
||||||
|
- Uses Novu for multi-channel delivery
|
||||||
|
- Supports granular preferences per workflow
|
||||||
|
- Includes frequency controls
|
||||||
|
- Provides delivery tracking
|
||||||
|
- Backwards compatible with existing preferences
|
||||||
|
|
||||||
|
## Data Migration
|
||||||
|
|
||||||
|
### Step 1: Migrate Existing User Preferences
|
||||||
|
|
||||||
|
The system automatically migrates old preferences from `user_preferences.email_notifications` and `user_preferences.push_notifications` to the new `user_notification_preferences` table.
|
||||||
|
|
||||||
|
Run this migration script to batch migrate existing users:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Migrate existing users to new notification preferences
|
||||||
|
INSERT INTO user_notification_preferences (
|
||||||
|
user_id,
|
||||||
|
channel_preferences,
|
||||||
|
workflow_preferences,
|
||||||
|
frequency_settings
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
jsonb_build_object(
|
||||||
|
'in_app', true,
|
||||||
|
'email', COALESCE((email_notifications->>'system_announcements')::boolean, true),
|
||||||
|
'push', COALESCE((push_notifications->>'browser_enabled')::boolean, false),
|
||||||
|
'sms', false
|
||||||
|
),
|
||||||
|
jsonb_build_object(
|
||||||
|
'review-reply', COALESCE((email_notifications->>'review_replies')::boolean, true),
|
||||||
|
'new-follower', COALESCE((email_notifications->>'new_followers')::boolean, true),
|
||||||
|
'system-announcement', COALESCE((email_notifications->>'system_announcements')::boolean, true),
|
||||||
|
'weekly-digest', COALESCE((email_notifications->>'weekly_digest')::boolean, false),
|
||||||
|
'monthly-digest', COALESCE((email_notifications->>'monthly_digest')::boolean, true)
|
||||||
|
),
|
||||||
|
jsonb_build_object(
|
||||||
|
'digest', 'daily',
|
||||||
|
'max_per_hour', 10
|
||||||
|
)
|
||||||
|
FROM user_preferences
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM user_notification_preferences
|
||||||
|
WHERE user_notification_preferences.user_id = user_preferences.user_id
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Novu Subscribers for Existing Users
|
||||||
|
|
||||||
|
Create a batch script to register all existing users as Novu subscribers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { notificationService } from '@/lib/notificationService';
|
||||||
|
|
||||||
|
async function migrateUsersToNovu() {
|
||||||
|
const { data: profiles, error } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('user_id, username, display_name')
|
||||||
|
.is('banned', false);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching profiles:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const profile of profiles) {
|
||||||
|
// Get user's auth data for email
|
||||||
|
const { data: { user } } = await supabase.auth.admin.getUserById(profile.user_id);
|
||||||
|
|
||||||
|
if (user?.email) {
|
||||||
|
await notificationService.createSubscriber({
|
||||||
|
subscriberId: profile.user_id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: profile.display_name?.split(' ')[0],
|
||||||
|
lastName: profile.display_name?.split(' ').slice(1).join(' '),
|
||||||
|
data: {
|
||||||
|
username: profile.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Migrated user: ${profile.username}`);
|
||||||
|
|
||||||
|
// Rate limit to avoid overwhelming Novu API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migration complete!');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Migration
|
||||||
|
|
||||||
|
### Updating Notification Triggers
|
||||||
|
|
||||||
|
**Old Way:**
|
||||||
|
```typescript
|
||||||
|
// Direct database insert (legacy)
|
||||||
|
await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.insert({
|
||||||
|
user_id: userId,
|
||||||
|
type: 'review_reply',
|
||||||
|
content: 'Someone replied to your review',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Way:**
|
||||||
|
```typescript
|
||||||
|
// Using Novu via notificationService
|
||||||
|
import { notificationService } from '@/lib/notificationService';
|
||||||
|
|
||||||
|
await notificationService.trigger({
|
||||||
|
workflowId: 'review-reply',
|
||||||
|
subscriberId: userId,
|
||||||
|
payload: {
|
||||||
|
reviewTitle: 'Great ride!',
|
||||||
|
replyAuthor: 'Jane Doe',
|
||||||
|
replyContent: 'Thanks for the review!',
|
||||||
|
reviewUrl: `/reviews/${reviewId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating Preference Management
|
||||||
|
|
||||||
|
**Old Way:**
|
||||||
|
```typescript
|
||||||
|
await supabase
|
||||||
|
.from('user_preferences')
|
||||||
|
.update({
|
||||||
|
email_notifications: {
|
||||||
|
review_replies: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.eq('user_id', userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Way:**
|
||||||
|
```typescript
|
||||||
|
import { notificationService } from '@/lib/notificationService';
|
||||||
|
|
||||||
|
await notificationService.updatePreferences(userId, {
|
||||||
|
channelPreferences: {
|
||||||
|
in_app: true,
|
||||||
|
email: true,
|
||||||
|
push: false,
|
||||||
|
sms: false,
|
||||||
|
},
|
||||||
|
workflowPreferences: {
|
||||||
|
'review-reply': false,
|
||||||
|
},
|
||||||
|
frequencySettings: {
|
||||||
|
digest: 'daily',
|
||||||
|
max_per_hour: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If you need to rollback to the old system:
|
||||||
|
|
||||||
|
1. **Keep Old Tables**: Don't drop `user_preferences.email_notifications` or `push_notifications` columns
|
||||||
|
2. **Disable Novu**: Clear `VITE_NOVU_APPLICATION_IDENTIFIER` environment variable
|
||||||
|
3. **Revert Code**: The notification service gracefully handles missing Novu config
|
||||||
|
|
||||||
|
## Testing the Migration
|
||||||
|
|
||||||
|
### Test Checklist
|
||||||
|
|
||||||
|
- [ ] All existing users have entries in `user_notification_preferences`
|
||||||
|
- [ ] User preferences match their previous settings
|
||||||
|
- [ ] Test users receive notifications on all enabled channels
|
||||||
|
- [ ] Webhook is receiving delivery events
|
||||||
|
- [ ] Notification logs are being created
|
||||||
|
- [ ] User preference changes sync to Novu
|
||||||
|
- [ ] Unsubscribe links work correctly
|
||||||
|
- [ ] Email templates render correctly
|
||||||
|
- [ ] Push notifications work in supported browsers
|
||||||
|
- [ ] In-app notifications display correctly
|
||||||
|
|
||||||
|
### Test Script
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Test notification delivery
|
||||||
|
import { notificationService } from '@/lib/notificationService';
|
||||||
|
|
||||||
|
async function testNotifications(testUserId: string) {
|
||||||
|
console.log('Testing notification delivery...');
|
||||||
|
|
||||||
|
// Test each workflow
|
||||||
|
const workflows = [
|
||||||
|
{
|
||||||
|
id: 'review-reply',
|
||||||
|
payload: {
|
||||||
|
reviewTitle: 'Test Review',
|
||||||
|
replyAuthor: 'Test User',
|
||||||
|
replyContent: 'Test reply content',
|
||||||
|
reviewUrl: '/test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new-follower',
|
||||||
|
payload: {
|
||||||
|
followerName: 'Test Follower',
|
||||||
|
followerProfile: '/profile/test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const workflow of workflows) {
|
||||||
|
const result = await notificationService.trigger({
|
||||||
|
workflowId: workflow.id,
|
||||||
|
subscriberId: testUserId,
|
||||||
|
payload: workflow.payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${workflow.id}: ${result.success ? '✓' : '✗'}`);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
When migrating large numbers of users:
|
||||||
|
|
||||||
|
1. **Rate Limiting**: Add delays between API calls
|
||||||
|
2. **Batch Size**: Process in chunks of 100-500 users
|
||||||
|
3. **Error Handling**: Log failures and retry
|
||||||
|
4. **Progress Tracking**: Store migration state
|
||||||
|
|
||||||
|
### Optimization Tips
|
||||||
|
|
||||||
|
1. **Cache Templates**: Load notification templates once at startup
|
||||||
|
2. **Async Processing**: Use edge functions for heavy operations
|
||||||
|
3. **Database Indexes**: Already created on `user_notification_preferences`
|
||||||
|
4. **Connection Pooling**: Supabase handles this automatically
|
||||||
|
|
||||||
|
## Monitoring Post-Migration
|
||||||
|
|
||||||
|
Track these metrics after migration:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Delivery success rate
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'delivered') * 100.0 / COUNT(*) as success_rate
|
||||||
|
FROM notification_logs
|
||||||
|
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date DESC;
|
||||||
|
|
||||||
|
-- Channel usage
|
||||||
|
SELECT
|
||||||
|
channel,
|
||||||
|
COUNT(*) as total_sent,
|
||||||
|
COUNT(*) FILTER (WHERE status = 'delivered') as delivered,
|
||||||
|
COUNT(*) FILTER (WHERE read_at IS NOT NULL) as read
|
||||||
|
FROM notification_logs
|
||||||
|
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY channel;
|
||||||
|
|
||||||
|
-- User engagement
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT user_id) as active_users,
|
||||||
|
AVG(EXTRACT(EPOCH FROM (read_at - delivered_at))) as avg_time_to_read_seconds
|
||||||
|
FROM notification_logs
|
||||||
|
WHERE read_at IS NOT NULL
|
||||||
|
AND created_at > NOW() - INTERVAL '7 days';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support During Migration
|
||||||
|
|
||||||
|
1. **Backup Data**: Export old notification preferences before migration
|
||||||
|
2. **Phased Rollout**: Enable for a subset of users first
|
||||||
|
3. **Feature Flag**: Use admin settings to control Novu enablement
|
||||||
|
4. **User Communication**: Notify users of new notification features
|
||||||
|
5. **Monitor Logs**: Watch edge function logs during migration
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
Recommended migration timeline:
|
||||||
|
|
||||||
|
- **Week 1**: Test with internal users
|
||||||
|
- **Week 2**: Rollout to 10% of users
|
||||||
|
- **Week 3**: Rollout to 50% of users
|
||||||
|
- **Week 4**: Complete rollout
|
||||||
|
- **Week 5**: Remove legacy code (optional)
|
||||||
238
docs/NOVU_SETUP.md
Normal file
238
docs/NOVU_SETUP.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
# Novu Cloud Integration Setup Guide
|
||||||
|
|
||||||
|
This guide will help you set up and configure Novu Cloud for the ThrillWiki notification system.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The notification system uses Novu Cloud for managing multi-channel notifications including:
|
||||||
|
- In-app notifications
|
||||||
|
- Email notifications
|
||||||
|
- Push notifications
|
||||||
|
- SMS notifications (planned)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A Novu Cloud account (sign up at https://novu.co)
|
||||||
|
2. Access to the Supabase project
|
||||||
|
3. Environment variable configuration access
|
||||||
|
|
||||||
|
## Step 1: Create a Novu Cloud Account
|
||||||
|
|
||||||
|
1. Go to https://novu.co and sign up
|
||||||
|
2. Create a new application in the Novu dashboard
|
||||||
|
3. Note down your **Application Identifier** from the Settings page
|
||||||
|
|
||||||
|
## Step 2: Configure Environment Variables
|
||||||
|
|
||||||
|
Add the following environment variables to your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Novu Configuration
|
||||||
|
VITE_NOVU_APPLICATION_IDENTIFIER="your-app-identifier"
|
||||||
|
VITE_NOVU_SOCKET_URL="wss://ws.novu.co"
|
||||||
|
VITE_NOVU_API_URL="https://api.novu.co"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Add Novu API Key to Supabase Secrets
|
||||||
|
|
||||||
|
The Novu API key needs to be stored as a Supabase secret (already done via the setup):
|
||||||
|
|
||||||
|
1. Go to your Novu dashboard
|
||||||
|
2. Navigate to Settings > API Keys
|
||||||
|
3. Copy your API Key
|
||||||
|
4. The secret `NOVU_API_KEY` should already be configured in Supabase
|
||||||
|
|
||||||
|
## Step 4: Create Novu Workflows
|
||||||
|
|
||||||
|
Create the following workflows in your Novu dashboard to match the templates in the database:
|
||||||
|
|
||||||
|
### 1. Review Reply (`review-reply`)
|
||||||
|
**Trigger Identifier:** `review-reply`
|
||||||
|
**Channels:** Email, In-App
|
||||||
|
**Payload Variables:**
|
||||||
|
- `reviewTitle` - Title of the review
|
||||||
|
- `replyAuthor` - Name of the person who replied
|
||||||
|
- `replyContent` - Content of the reply
|
||||||
|
- `reviewUrl` - Link to the review
|
||||||
|
|
||||||
|
### 2. New Follower (`new-follower`)
|
||||||
|
**Trigger Identifier:** `new-follower`
|
||||||
|
**Channels:** Email, In-App, Push
|
||||||
|
**Payload Variables:**
|
||||||
|
- `followerName` - Name of the new follower
|
||||||
|
- `followerProfile` - Link to the follower's profile
|
||||||
|
|
||||||
|
### 3. System Announcement (`system-announcement`)
|
||||||
|
**Trigger Identifier:** `system-announcement`
|
||||||
|
**Channels:** Email, In-App, Push
|
||||||
|
**Payload Variables:**
|
||||||
|
- `title` - Announcement title
|
||||||
|
- `content` - Announcement content
|
||||||
|
- `priority` - Priority level (info, warning, critical)
|
||||||
|
|
||||||
|
### 4. Weekly Digest (`weekly-digest`)
|
||||||
|
**Trigger Identifier:** `weekly-digest`
|
||||||
|
**Channels:** Email
|
||||||
|
**Payload Variables:**
|
||||||
|
- `userName` - User's name
|
||||||
|
- `weekStart` - Start date of the week
|
||||||
|
- `weekEnd` - End date of the week
|
||||||
|
- `stats` - Activity statistics object
|
||||||
|
- `highlights` - Array of highlighted content
|
||||||
|
|
||||||
|
### 5. Monthly Digest (`monthly-digest`)
|
||||||
|
**Trigger Identifier:** `monthly-digest`
|
||||||
|
**Channels:** Email
|
||||||
|
**Payload Variables:**
|
||||||
|
- `userName` - User's name
|
||||||
|
- `month` - Month name
|
||||||
|
- `stats` - Monthly statistics object
|
||||||
|
- `topContent` - Array of popular content
|
||||||
|
|
||||||
|
### 6. Moderation Alert (`moderation-alert`)
|
||||||
|
**Trigger Identifier:** `moderation-alert`
|
||||||
|
**Channels:** Email, In-App
|
||||||
|
**Payload Variables:**
|
||||||
|
- `itemType` - Type of content (review, photo, etc.)
|
||||||
|
- `itemId` - ID of the item
|
||||||
|
- `submitterName` - Name of the submitter
|
||||||
|
- `moderationUrl` - Link to moderation queue
|
||||||
|
|
||||||
|
### 7. Content Approved (`content-approved`)
|
||||||
|
**Trigger Identifier:** `content-approved`
|
||||||
|
**Channels:** Email, In-App
|
||||||
|
**Payload Variables:**
|
||||||
|
- `contentType` - Type of content
|
||||||
|
- `contentTitle` - Title/name of content
|
||||||
|
- `contentUrl` - Link to the approved content
|
||||||
|
|
||||||
|
### 8. Content Rejected (`content-rejected`)
|
||||||
|
**Trigger Identifier:** `content-rejected`
|
||||||
|
**Channels:** Email, In-App
|
||||||
|
**Payload Variables:**
|
||||||
|
- `contentType` - Type of content
|
||||||
|
- `contentTitle` - Title/name of content
|
||||||
|
- `rejectionReason` - Reason for rejection
|
||||||
|
|
||||||
|
## Step 5: Configure Webhook (Optional)
|
||||||
|
|
||||||
|
To track notification delivery status:
|
||||||
|
|
||||||
|
1. Go to Novu Dashboard > Settings > Webhooks
|
||||||
|
2. Add a new webhook endpoint: `https://ydvtmnrszybqnbcqbdcy.supabase.co/functions/v1/novu-webhook`
|
||||||
|
3. Select events: `notification.sent`, `notification.delivered`, `notification.read`, `notification.failed`
|
||||||
|
|
||||||
|
## Step 6: Test the Integration
|
||||||
|
|
||||||
|
Run these tests to ensure everything works:
|
||||||
|
|
||||||
|
### Test Subscriber Creation
|
||||||
|
```typescript
|
||||||
|
import { notificationService } from '@/lib/notificationService';
|
||||||
|
|
||||||
|
await notificationService.createSubscriber({
|
||||||
|
subscriberId: 'user-id',
|
||||||
|
email: 'user@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Notification Trigger
|
||||||
|
```typescript
|
||||||
|
import { notificationService } from '@/lib/notificationService';
|
||||||
|
|
||||||
|
await notificationService.trigger({
|
||||||
|
workflowId: 'review-reply',
|
||||||
|
subscriberId: 'user-id',
|
||||||
|
payload: {
|
||||||
|
reviewTitle: 'Great ride!',
|
||||||
|
replyAuthor: 'Jane Doe',
|
||||||
|
replyContent: 'Thanks for the review!',
|
||||||
|
reviewUrl: 'https://example.com/review/123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Database Tables
|
||||||
|
|
||||||
|
1. **notification_templates** - Stores workflow configuration
|
||||||
|
2. **notification_logs** - Tracks sent notifications
|
||||||
|
3. **notification_channels** - Manages available channels
|
||||||
|
4. **user_notification_preferences** - User preferences and Novu subscriber mapping
|
||||||
|
|
||||||
|
### Edge Functions
|
||||||
|
|
||||||
|
1. **create-novu-subscriber** - Creates/updates Novu subscribers
|
||||||
|
2. **update-novu-preferences** - Syncs preferences with Novu
|
||||||
|
3. **trigger-notification** - Triggers notification workflows
|
||||||
|
4. **novu-webhook** - Handles Novu webhook events
|
||||||
|
|
||||||
|
### Frontend Components
|
||||||
|
|
||||||
|
1. **NotificationsTab** - User preferences management
|
||||||
|
2. **NotificationCenter** - In-app notification display
|
||||||
|
3. **notificationService** - Core notification logic
|
||||||
|
|
||||||
|
## Switching to Self-Hosted Novu
|
||||||
|
|
||||||
|
To switch to a self-hosted Novu instance:
|
||||||
|
|
||||||
|
1. Update environment variables:
|
||||||
|
```bash
|
||||||
|
VITE_NOVU_SOCKET_URL="wss://your-novu-instance.com"
|
||||||
|
VITE_NOVU_API_URL="https://your-novu-instance.com/api"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update the Novu API key in Supabase secrets to match your self-hosted instance
|
||||||
|
|
||||||
|
3. No code changes required! The system is designed to work with both Cloud and self-hosted versions
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Notifications Not Sending
|
||||||
|
|
||||||
|
1. Check that `VITE_NOVU_APPLICATION_IDENTIFIER` is set
|
||||||
|
2. Verify `NOVU_API_KEY` is configured in Supabase secrets
|
||||||
|
3. Check edge function logs: https://supabase.com/dashboard/project/ydvtmnrszybqnbcqbdcy/functions
|
||||||
|
4. Verify workflow IDs match between database and Novu dashboard
|
||||||
|
|
||||||
|
### User Not Receiving Notifications
|
||||||
|
|
||||||
|
1. Check user's notification preferences in the database
|
||||||
|
2. Verify subscriber was created in Novu
|
||||||
|
3. Check notification logs table for delivery status
|
||||||
|
4. Verify user's email/push permission settings
|
||||||
|
|
||||||
|
### Webhook Not Working
|
||||||
|
|
||||||
|
1. Verify webhook URL is correct in Novu dashboard
|
||||||
|
2. Check that `novu-webhook` edge function has `verify_jwt = false`
|
||||||
|
3. Review edge function logs for errors
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Track notification performance:
|
||||||
|
|
||||||
|
1. View delivery rates in `notification_logs` table
|
||||||
|
2. Monitor edge function logs for errors
|
||||||
|
3. Check Novu dashboard analytics
|
||||||
|
4. Query notification statistics:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
status,
|
||||||
|
COUNT(*) as count,
|
||||||
|
AVG(EXTRACT(EPOCH FROM (delivered_at - created_at))) as avg_delivery_time_seconds
|
||||||
|
FROM notification_logs
|
||||||
|
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY status;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Novu Documentation: https://docs.novu.co
|
||||||
|
- Novu Discord: https://discord.gg/novu
|
||||||
|
- ThrillWiki Support: [Add support contact]
|
||||||
Reference in New Issue
Block a user