Skip to content

Commit d0410af

Browse files
committed
feat: Add notification logs retrieval endpoint with pagination support
1 parent 58b8aa3 commit d0410af

File tree

7 files changed

+206
-20
lines changed

7 files changed

+206
-20
lines changed

docs/TESTING_NOTIFICATIONS.md

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,43 @@ curl -X DELETE http://localhost:4000/api/notifications/subscriptions/674f8a1b2c9
141141
}
142142
```
143143

144+
### 6. Get Notification Logs
145+
146+
Retrieve the history of sent notifications with pagination.
147+
148+
```bash
149+
# Get recent logs (default: 50 items)
150+
curl http://localhost:4000/api/notifications/logs \
151+
-H "Cookie: session=YOUR_SESSION_COOKIE"
152+
153+
# With pagination
154+
curl "http://localhost:4000/api/notifications/logs?limit=10&offset=0" \
155+
-H "Cookie: session=YOUR_SESSION_COOKIE"
156+
```
157+
158+
**Expected Response:**
159+
```json
160+
{
161+
"success": true,
162+
"logs": [
163+
{
164+
"id": "674f8a1b2c9d3e4f5a6b7c8d",
165+
"userId": "674e5f6g7h8i9j0k1l2m3n4o",
166+
"eventId": "674f8a1b2c9d3e4f5a6b7c8e",
167+
"subscriptionId": "674f8a1b2c9d3e4f5a6b7c8f",
168+
"status": "sent",
169+
"sentAt": "2025-12-05T10:30:00Z"
170+
}
171+
],
172+
"limit": 10,
173+
"offset": 0
174+
}
175+
```
176+
177+
**Query Parameters:**
178+
- `limit`: Number of logs to return (default: 50, max: 100)
179+
- `offset`: Number of logs to skip for pagination (default: 0)
180+
144181
## Testing with Real Browser Subscriptions
145182

146183
For complete end-to-end testing, you need a real browser subscription:
@@ -403,7 +440,60 @@ Run it:
403440
## Next Steps
404441
405442
After verifying the endpoints work:
406-
1. ✅ Step 6 complete
407-
2. Move to Step 7: Implement test notification endpoint (already done!)
408-
3. Move to Step 8: Integration with event creation
443+
1. ✅ Step 6 complete - Subscription management endpoints
444+
2. Step 7 complete - Test notification and logs endpoints
445+
3. Step 8 complete - Integration with event creation
409446
4. Move to Step 9-11: Frontend implementation
447+
448+
## Testing End-to-End Notification Flow
449+
450+
To test that notifications are sent when events are created:
451+
452+
### 1. Create a Real Browser Subscription
453+
454+
Use the test HTML page or your frontend to create a valid subscription with a real browser.
455+
456+
### 2. Create an Event via API
457+
458+
```bash
459+
# Track an event (this should trigger a notification)
460+
curl -X POST http://localhost:4000/api/track \
461+
-H "Content-Type: application/json" \
462+
-H "X-API-KEY: YOUR_API_KEY" \
463+
-d '{
464+
"project": "my-project",
465+
"channel": "errors",
466+
"title": "Test Error",
467+
"description": "This is a test error that should trigger a notification",
468+
"icon": "🔴",
469+
"tags": {
470+
"severity": "high",
471+
"environment": "production"
472+
}
473+
}'
474+
```
475+
476+
### 3. Verify Notification Received
477+
478+
- Check your browser for the push notification
479+
- The notification should appear even if the browser tab is closed (depending on OS)
480+
481+
### 4. Check Notification Logs
482+
483+
```bash
484+
curl http://localhost:4000/api/notifications/logs \
485+
-H "Cookie: session=YOUR_SESSION_COOKIE"
486+
```
487+
488+
You should see a log entry with:
489+
- `status: "sent"` (if successful)
490+
- `status: "failed"` (if there was an error)
491+
- `eventId` matching the created event
492+
493+
### Key Features Implemented
494+
495+
✅ **Asynchronous Processing**: Notifications are sent in a goroutine so they don't block event creation
496+
✅ **Error Handling**: If notification fails, the event is still created successfully
497+
✅ **Logging**: All notification attempts are logged for debugging
498+
✅ **Automatic**: Every event automatically triggers notifications for the project owner
499+

internal/handler/event.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handler
22

33
import (
4+
"log"
45
"net/http"
56
"strconv"
67

@@ -51,7 +52,7 @@ func (h *EventHandler) CreateEvent(ctx *gin.Context) {
5152
return
5253
}
5354

54-
event, err := h.eventService.CreateEvent(ctx.Request.Context(), userID, req.Project, req.Channel, req.Title, req.Description, req.Icon, req.Tags)
55+
event, notificationCh, err := h.eventService.CreateEvent(ctx.Request.Context(), userID, req.Project, req.Channel, req.Title, req.Description, req.Icon, req.Tags)
5556
if err != nil {
5657
statusCode := http.StatusInternalServerError
5758
if err.Error() == "channel not found" || err.Error() == "project not found" {
@@ -66,11 +67,21 @@ func (h *EventHandler) CreateEvent(ctx *gin.Context) {
6667
return
6768
}
6869

70+
// Respond immediately - don't wait for notification
6971
ctx.JSON(http.StatusCreated, gin.H{
7072
"success": true,
7173
"message": "Event created successfully",
7274
"data": event,
7375
})
76+
77+
// Log notification result in background after response is sent
78+
go func() {
79+
if notificationErr := <-notificationCh; notificationErr != nil {
80+
log.Printf("[Event %s] Notification failed: %v", event.ID.Hex(), notificationErr)
81+
} else {
82+
log.Printf("[Event %s] Notification sent successfully", event.ID.Hex())
83+
}
84+
}()
7485
}
7586

7687
// GetChannelEvents handles GET /api/channels/:channelId/events

internal/handler/notification.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handler
22

33
import (
44
"net/http"
5+
"strconv"
56
"time"
67

78
"trakrlog/internal/middleware"
@@ -274,3 +275,52 @@ func (h *NotificationHandler) SendTestNotification(ctx *gin.Context) {
274275
"message": "Test notification sent successfully",
275276
})
276277
}
278+
279+
// GetNotificationLogs handles GET /api/notifications/logs
280+
func (h *NotificationHandler) GetNotificationLogs(ctx *gin.Context) {
281+
userID, ok := middleware.GetAuthUserID(ctx)
282+
if !ok {
283+
ctx.JSON(http.StatusUnauthorized, gin.H{
284+
"success": false,
285+
"message": "Unauthorized",
286+
})
287+
return
288+
}
289+
290+
// Parse pagination parameters
291+
limit := int64(50) // Default limit
292+
offset := int64(0) // Default offset
293+
294+
if limitStr := ctx.Query("limit"); limitStr != "" {
295+
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil && parsedLimit > 0 {
296+
limit = parsedLimit
297+
if limit > 100 {
298+
limit = 100 // Max limit
299+
}
300+
}
301+
}
302+
303+
if offsetStr := ctx.Query("offset"); offsetStr != "" {
304+
if parsedOffset, err := strconv.ParseInt(offsetStr, 10, 64); err == nil && parsedOffset >= 0 {
305+
offset = parsedOffset
306+
}
307+
}
308+
309+
// Retrieve logs from push service
310+
logs, err := h.pushService.GetNotificationLogs(ctx.Request.Context(), userID, limit, offset)
311+
if err != nil {
312+
ctx.JSON(http.StatusInternalServerError, gin.H{
313+
"success": false,
314+
"message": "Failed to retrieve notification logs",
315+
"error": err.Error(),
316+
})
317+
return
318+
}
319+
320+
ctx.JSON(http.StatusOK, gin.H{
321+
"success": true,
322+
"logs": logs,
323+
"limit": limit,
324+
"offset": offset,
325+
})
326+
}

internal/server/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func (s *Server) RegisterRouter() http.Handler {
9898
api.DELETE("/notifications/subscriptions/:id", notificationHandler.DeleteSubscription)
9999
api.PATCH("/notifications/subscriptions/:id", notificationHandler.UpdateSubscription)
100100
api.POST("/notifications/test", notificationHandler.SendTestNotification)
101+
api.GET("/notifications/logs", notificationHandler.GetNotificationLogs)
101102
}
102103

103104
// Track endpoint - direct route to avoid trailing slash redirects

internal/server/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ func New() *http.Server {
5555
userService := service.NewUserService(userRepo)
5656
projectService := service.NewProjectService(projectRepo, userRepo)
5757
channelService := service.NewChannelService(channelRepo, projectRepo)
58-
eventService := service.NewEventService(eventRepo, channelRepo, projectRepo)
5958

6059
// Initialize subscription service
6160
subscriptionService := service.NewSubscriptionService(subscriptionRepo)
@@ -76,6 +75,9 @@ func New() *http.Server {
7675
channelService,
7776
)
7877

78+
// Initialize event service
79+
eventService := service.NewEventService(eventRepo, channelRepo, projectRepo, notificationService)
80+
7981
NewServer := &Server{
8082
port: port,
8183
db: db,

internal/service/event.go

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,51 @@ package service
33
import (
44
"context"
55
"errors"
6+
"log"
67

78
"trakrlog/internal/model"
89
"trakrlog/internal/repository"
910
)
1011

1112
type EventService struct {
12-
eventRepo repository.EventRepository
13-
channelRepo repository.ChannelRepository
14-
projectRepo repository.ProjectRepository
13+
eventRepo repository.EventRepository
14+
channelRepo repository.ChannelRepository
15+
projectRepo repository.ProjectRepository
16+
notificationService *NotificationService
1517
}
1618

17-
func NewEventService(eventRepo repository.EventRepository, channelRepo repository.ChannelRepository, projectRepo repository.ProjectRepository) *EventService {
19+
func NewEventService(
20+
eventRepo repository.EventRepository,
21+
channelRepo repository.ChannelRepository,
22+
projectRepo repository.ProjectRepository,
23+
notificationService *NotificationService,
24+
) *EventService {
1825
return &EventService{
19-
eventRepo: eventRepo,
20-
channelRepo: channelRepo,
21-
projectRepo: projectRepo,
26+
eventRepo: eventRepo,
27+
channelRepo: channelRepo,
28+
projectRepo: projectRepo,
29+
notificationService: notificationService,
2230
}
2331
}
2432

2533
// CreateEvent creates a new event in a channel
26-
func (s *EventService) CreateEvent(ctx context.Context, userID, projectName, channelName, title, description, icon string, tags map[string]string) (*model.Event, error) {
34+
// Returns the created event and a channel that will receive the notification result
35+
func (s *EventService) CreateEvent(ctx context.Context, userID, projectName, channelName, title, description, icon string, tags map[string]string) (*model.Event, <-chan error, error) {
2736
// Validation
2837
if title == "" {
29-
return nil, errors.New("event title required")
38+
return nil, nil, errors.New("event title required")
3039
}
3140

3241
// Find project by user ID and name
3342
project, err := s.projectRepo.FindByUserIDAndName(ctx, userID, projectName)
3443
if err != nil {
35-
return nil, errors.New("project not found")
44+
return nil, nil, errors.New("project not found")
3645
}
3746

3847
// Find channel by project ID and name
3948
channel, err := s.channelRepo.FindByProjectIDAndName(ctx, project.ID.Hex(), channelName)
4049
if err != nil {
41-
return nil, errors.New("channel not found")
50+
return nil, nil, errors.New("channel not found")
4251
}
4352

4453
// Create event
@@ -52,10 +61,28 @@ func (s *EventService) CreateEvent(ctx context.Context, userID, projectName, cha
5261
}
5362

5463
if err := s.eventRepo.Create(ctx, event); err != nil {
55-
return nil, err
56-
}
57-
58-
return event, nil
64+
return nil, nil, err
65+
}
66+
67+
// Send push notification asynchronously (don't block event creation)
68+
// Return a channel that the caller can use to wait for notification completion
69+
notificationDone := make(chan error, 1)
70+
71+
go func() {
72+
defer close(notificationDone)
73+
if s.notificationService != nil {
74+
if err := s.notificationService.ProcessEventNotification(context.Background(), event); err != nil {
75+
// Log error but don't fail the event creation
76+
log.Printf("Failed to send notification for event %s: %v", event.ID.Hex(), err)
77+
notificationDone <- err
78+
return
79+
}
80+
log.Printf("Successfully sent notification for event %s", event.ID.Hex())
81+
notificationDone <- nil
82+
}
83+
}()
84+
85+
return event, notificationDone, nil
5986
}
6087

6188
// GetEventByID retrieves an event by ID

internal/service/push.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,8 @@ func (s *PushService) SendToUser(ctx context.Context, userID string, payload *mo
169169
log.Printf("Successfully sent notification to %d/%d subscriptions for user %s", successCount, len(subscriptions), userID)
170170
return nil
171171
}
172+
173+
// GetNotificationLogs retrieves notification logs for a user with pagination
174+
func (s *PushService) GetNotificationLogs(ctx context.Context, userID string, limit, offset int64) ([]*model.NotificationLog, error) {
175+
return s.logRepo.FindByUserID(ctx, userID, limit, offset)
176+
}

0 commit comments

Comments
 (0)