Skip to content

Conversation

@chrisgacsal
Copy link
Collaborator

@chrisgacsal chrisgacsal commented Nov 7, 2025

Overview

  • improve state synchronization
  • add delivery attempts to delivery status
  • improve error handling

Summary by CodeRabbit

  • New Features

    • Per-attempt delivery tracking with detailed responses and next-retry scheduling; message retrieval can optionally include delivery-status details and annotations.
    • Webhook message retrieval endpoint added; configurable pending and sending timeouts for delivery workflows.
  • Bug Fixes

    • Reconcile now filters eligible retries and improves timeout-driven state transitions.
    • Better distinction between transient, recoverable, and unrecoverable dispatch outcomes.
  • Improvements

    • Enhanced validation requiring at least one channel for rules; new error types and annotation helpers for clearer diagnostics.

@chrisgacsal chrisgacsal self-assigned this Nov 7, 2025
@chrisgacsal chrisgacsal requested a review from a team as a code owner November 7, 2025 10:45
@chrisgacsal chrisgacsal added the release-note/misc Miscellaneous changes label Nov 7, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 2025

📝 Walkthrough

Walkthrough

Adds per-attempt delivery records and next-attempt scheduling to notification delivery statuses; introduces configurable pending/sending timeouts and defaults; refactors reconciliation to channel-type (webhook) processing; updates DB schema, adapters, API mappings, webhook errors/handlers, svix integration, wiring, and tests.

Changes

Cohort / File(s) Summary
API Spec
api/spec/src/notification/event.tsp
Added nextAttempt?: DateTime and attempts: Array<NotificationEventDeliveryAttempt> on NotificationEventDeliveryStatus; new models EventDeliveryAttemptResponse and NotificationEventDeliveryAttempt.
Config & Tests
app/config/notification.go, app/config/config_test.go, app/config/testdata/complete.yaml
Added ReconcileInterval, SendingTimeout, PendingTimeout to NotificationConfiguration and test fixtures; defaults wired from notification package.
Handler Init & Wiring
app/common/notification.go, cmd/*/wire_gen.go (cmd/balance-worker/..., cmd/jobs/internal/..., cmd/notification-service/..., cmd/server/...), openmeter/notification/eventhandler/handler.go
NewNotificationEventHandler now receives notification config; Handler/Config gain sending/pending timeout fields with defaulting; all wire call sites updated to pass config.
Event handler core
openmeter/notification/eventhandler/reconcile.go, openmeter/notification/eventhandler/dispatch.go, openmeter/notification/eventhandler/webhook.go, openmeter/notification/eventhandler/deliverystatus.go
Replaced per-status dispatch with channel-type reconciliation; added webhook-centric reconcile flow, timeouts, state transitions, attempt recording, filtering/sorting helpers.
Delivery status domain
openmeter/notification/deliverystatus.go, openmeter/notification/event.go
Added EventDeliveryAttempt and EventDeliveryAttemptResponse types; added Attempts []EventDeliveryAttempt and NextAttempt *time.Time to status and update input; ListEventsInput gains NextAttemptBefore.
DB schema & adapters
openmeter/ent/schema/notification.go, tools/migrate/migrations/*notification_deliverystatus*.{up.sql,down.sql}, openmeter/notification/adapter/deliverystatus.go, openmeter/notification/adapter/entitymapping.go, openmeter/notification/adapter/event.go
DB: added next_attempt_at (timestamptz) and attempts (jsonb) with composite index; adapters set/get NextAttempt and Attempts; ListEvents supports NextAttemptBefore filtering.
HTTP mapping
openmeter/notification/httpdriver/mapping.go
FromEvent now includes Annotations, Attempts, NextAttempt, Reason; added AsEventDeliveryAttempts helper to map internal attempts to API objects.
Webhook types & errors
openmeter/notification/webhook/errors.go, openmeter/notification/webhook/handler.go, openmeter/notification/webhook/noop/noop.go, openmeter/notification/webhook/secret/secret.go
Added RetryableError, MessageNotReadyError, UnrecoverableError; switched to models.Validator; added Payload alias, Message and MessageDeliveryStatus types, ExpandParams/GetMessageInput and GetMessage support; noop implements GetMessage; signing-secret validation moved under secret package and uses models.NewNillableGenericValidationError.
SVIX integration
openmeter/notification/webhook/svix/annotations.go, openmeter/notification/webhook/svix/message.go, openmeter/notification/webhook/svix/webhook.go, openmeter/notification/webhook/svix/internal/error.go
Added SVIX annotation constants; GetMessage exposed and can return DeliveryStatuses and Annotations when expanded; added ListAttemptLimit; Svix error mapping gains RetryAfter and classifies 429/5xx as retryable, 400/409/422 as unrecoverable; telemetry keys updated to annotation constants.
Payload utilities
openmeter/notification/eventpayload.go
Introduced RawPayload and AsRawPayload, replacing previous PayloadToMapInterface.
Rule validation & service
openmeter/notification/rule.go, openmeter/notification/service/rule.go
Require at least one channel for rule create/update; added generic ValidateRuleChannels helper used by CreateRule/UpdateRule.
Annotations utilities
pkg/models/annotation.go
Added Annotations.GetString, GetInt, Clone, and Merge helpers.
Tests & small fixes
test/notification/channel.go, test/notification/rule.go
Updated imports to webhook/secret, adjusted tests to pass namespace and channels, and updated expected config.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler
    participant Reconciler
    participant Repo as EventRepo
    participant WebhookHandler
    participant Adapter

    Scheduler->>Reconciler: Trigger Reconcile(now)
    Reconciler->>Repo: ListEvents(NextAttemptBefore=now)
    Repo-->>Reconciler: events[]
    loop per channel-type
        Reconciler->>WebhookHandler: reconcileWebhookEvent(event, channelType)
        WebhookHandler->>WebhookHandler: evaluate delivery statuses (pending/sending)
        alt needs transition -> sending
            WebhookHandler->>Adapter: Update status to Sending, set Attempts/NextAttempt
        end
        alt send attempt
            WebhookHandler->>WebhookHandler: send, record attempt (response,duration,url)
            alt success
                WebhookHandler->>Adapter: Update status=Success, Attempts
            else retryable
                WebhookHandler->>Adapter: Append Attempt, set NextAttempt
            else unrecoverable
                WebhookHandler->>Adapter: Update status=Failed, Attempts
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus during review:

  • openmeter/notification/eventhandler/webhook.go — reconciliation, timeouts, attempt recording, error classification.
  • openmeter/notification/eventhandler/reconcile.go — ListEvents NextAttemptBefore filtering and channel-type batching.
  • openmeter/notification/webhook/svix/* — Svix error mapping, paging, getDeliveryStatus and GetMessage changes.
  • DB migration + adapter/entitymapping — JSON attempts encoding/decoding and nullable timestamp handling.
  • Wiring updates in cmd/*/wire_gen.go and app/common/notification.go — ensure signature and order consistency.

Possibly related PRs

Suggested labels

kind/refactor, area/api

Suggested reviewers

  • tothandras
  • turip

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: a refactoring of notification event reconciling logic, which is the core theme across multiple files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/reconcile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
openmeter/notification/service/rule.go (1)

41-43: Optional: Duplicate validation check.

This check for empty channels is already performed in CreateRuleInput.Validate() and UpdateRuleInput.Validate() (lines 212-214, 274-276 in rule.go), which are called before this validator. You could simplify by removing this duplicate check.

openmeter/notification/webhook/handler.go (1)

242-249: Add validation to GetMessageInput for consistency.

Good catch! The search confirms that GetMessageInput currently lacks struct-level validation, while other input types in the same file implement models.Validator. The validation is happening at the handler layer in the svix implementation (where it checks that either ID or EventID is provided), but it should really live on the struct itself to match the codebase's existing patterns and catch issues earlier.

Your suggested validation looks solid—it'll enforce that Namespace and at least one of ID/EventID are provided, which aligns with how the field is actually used across the codebase.

pkg/models/annotation.go (1)

23-39: GetString conflates missing keys with empty values.

The method returns ("", false) both when a key is missing and when it exists with an empty string value. This makes it impossible to distinguish the two cases. If you need to differentiate, consider adjusting the logic—but if empty strings are equivalent to missing keys for your use case, this works fine.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee22348 and a310b71.

⛔ Files ignored due to path filters (15)
  • api/client/go/client.gen.go is excluded by !api/client/**
  • api/client/javascript/src/client/schemas.ts is excluded by !api/client/**
  • api/client/python/openmeter/_generated/models/__init__.py is excluded by !**/_generated/**, !api/client/**
  • api/client/python/openmeter/_generated/models/_models.py is excluded by !**/_generated/**, !api/client/**
  • api/openapi.cloud.yaml is excluded by !**/openapi.cloud.yaml
  • api/openapi.yaml is excluded by !**/openapi.yaml
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/notificationeventdeliverystatus.go is excluded by !**/ent/db/**
  • openmeter/ent/db/notificationeventdeliverystatus/notificationeventdeliverystatus.go is excluded by !**/ent/db/**
  • openmeter/ent/db/notificationeventdeliverystatus/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/notificationeventdeliverystatus_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/notificationeventdeliverystatus_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (37)
  • api/spec/src/notification/event.tsp (1 hunks)
  • app/common/notification.go (2 hunks)
  • app/config/notification.go (3 hunks)
  • cmd/balance-worker/wire_gen.go (1 hunks)
  • cmd/jobs/internal/wire_gen.go (1 hunks)
  • cmd/notification-service/wire_gen.go (1 hunks)
  • cmd/server/wire_gen.go (1 hunks)
  • openmeter/ent/schema/notification.go (2 hunks)
  • openmeter/notification/adapter/deliverystatus.go (1 hunks)
  • openmeter/notification/adapter/entitymapping.go (2 hunks)
  • openmeter/notification/adapter/event.go (1 hunks)
  • openmeter/notification/annotations.go (1 hunks)
  • openmeter/notification/channel.go (2 hunks)
  • openmeter/notification/deliverystatus.go (3 hunks)
  • openmeter/notification/event.go (1 hunks)
  • openmeter/notification/eventhandler.go (1 hunks)
  • openmeter/notification/eventhandler/deliverystatus.go (1 hunks)
  • openmeter/notification/eventhandler/dispatch.go (1 hunks)
  • openmeter/notification/eventhandler/handler.go (4 hunks)
  • openmeter/notification/eventhandler/reconcile.go (4 hunks)
  • openmeter/notification/eventhandler/webhook.go (1 hunks)
  • openmeter/notification/eventpayload.go (1 hunks)
  • openmeter/notification/httpdriver/mapping.go (2 hunks)
  • openmeter/notification/rule.go (2 hunks)
  • openmeter/notification/service/rule.go (4 hunks)
  • openmeter/notification/webhook/errors.go (2 hunks)
  • openmeter/notification/webhook/handler.go (9 hunks)
  • openmeter/notification/webhook/noop/noop.go (1 hunks)
  • openmeter/notification/webhook/secret/secret.go (2 hunks)
  • openmeter/notification/webhook/svix/annotations.go (1 hunks)
  • openmeter/notification/webhook/svix/internal/error.go (5 hunks)
  • openmeter/notification/webhook/svix/message.go (2 hunks)
  • openmeter/notification/webhook/svix/webhook.go (4 hunks)
  • pkg/models/annotation.go (1 hunks)
  • test/notification/channel.go (2 hunks)
  • tools/migrate/migrations/20251106124357_notification_deliverystatus.down.sql (1 hunks)
  • tools/migrate/migrations/20251106124357_notification_deliverystatus.up.sql (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/notification/event.go
  • openmeter/notification/annotations.go
  • openmeter/notification/rule.go
  • app/config/notification.go
  • test/notification/channel.go
  • openmeter/notification/webhook/noop/noop.go
  • openmeter/notification/adapter/deliverystatus.go
  • cmd/jobs/internal/wire_gen.go
  • app/common/notification.go
  • pkg/models/annotation.go
  • openmeter/notification/eventhandler/handler.go
  • cmd/notification-service/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/notification/httpdriver/mapping.go
  • openmeter/notification/service/rule.go
  • openmeter/ent/schema/notification.go
  • openmeter/notification/eventhandler/dispatch.go
  • openmeter/notification/eventhandler/deliverystatus.go
  • openmeter/notification/adapter/event.go
  • openmeter/notification/channel.go
  • openmeter/notification/webhook/handler.go
  • openmeter/notification/webhook/svix/annotations.go
  • openmeter/notification/webhook/svix/webhook.go
  • openmeter/notification/eventhandler/webhook.go
  • openmeter/notification/eventhandler/reconcile.go
  • openmeter/notification/webhook/errors.go
  • openmeter/notification/adapter/entitymapping.go
  • openmeter/notification/webhook/secret/secret.go
  • openmeter/notification/webhook/svix/message.go
  • openmeter/notification/eventhandler.go
  • openmeter/notification/eventpayload.go
  • openmeter/notification/deliverystatus.go
  • cmd/balance-worker/wire_gen.go
  • openmeter/notification/webhook/svix/internal/error.go
**/*.tsp

⚙️ CodeRabbit configuration file

**/*.tsp: Review the TypeSpec code for conformity with TypeSpec best practices. When recommending changes also consider the fact that multiple codegeneration toolchains depend on the TypeSpec code, each of which have their idiosyncrasies and bugs.

The declared API should be accurate, in parity with the actual implementation, and easy to understand for the user.

Files:

  • api/spec/src/notification/event.tsp
🧬 Code graph analysis (25)
app/config/notification.go (1)
openmeter/notification/eventhandler.go (3)
  • DefaultReconcileInterval (9-9)
  • DefaultDeliveryStateSendingTimeout (12-12)
  • DefaultDeliveryStatePendingTimeout (11-11)
test/notification/channel.go (1)
openmeter/notification/webhook/secret/secret.go (1)
  • NewSigningSecretWithDefaultSize (24-26)
openmeter/notification/webhook/noop/noop.go (2)
openmeter/notification/webhook/handler.go (3)
  • Handler (288-292)
  • GetMessageInput (242-249)
  • Message (187-204)
openmeter/notification/webhook/errors.go (1)
  • ErrNotImplemented (9-9)
openmeter/notification/adapter/deliverystatus.go (3)
api/client/javascript/src/client/schemas.ts (1)
  • Annotations (11692-11692)
api/api.gen.go (1)
  • Annotations (1048-1048)
api/client/go/client.gen.go (1)
  • Annotations (962-962)
cmd/jobs/internal/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
app/common/notification.go (1)
app/config/notification.go (1)
  • NotificationConfiguration (21-29)
openmeter/notification/eventhandler/handler.go (1)
openmeter/notification/eventhandler.go (2)
  • DefaultDeliveryStatePendingTimeout (11-11)
  • DefaultDeliveryStateSendingTimeout (12-12)
cmd/notification-service/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
cmd/server/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
openmeter/notification/httpdriver/mapping.go (2)
api/api.gen.go (3)
  • NotificationEventDeliveryStatusState (5426-5426)
  • NotificationEventDeliveryAttempt (5390-5399)
  • EventDeliveryAttemptResponse (3464-3476)
openmeter/notification/deliverystatus.go (2)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
openmeter/notification/service/rule.go (5)
openmeter/notification/rule.go (2)
  • CreateRuleInput (168-185)
  • UpdateRuleInput (228-245)
openmeter/notification/service/service.go (1)
  • Service (20-29)
openmeter/notification/channel.go (2)
  • ListChannelsInput (118-127)
  • Channel (37-51)
pkg/pagination/page.go (1)
  • Page (14-17)
pkg/sortx/order.go (1)
  • Order (3-3)
openmeter/ent/schema/notification.go (1)
openmeter/notification/deliverystatus.go (1)
  • EventDeliveryAttempt (56-60)
openmeter/notification/eventhandler/deliverystatus.go (5)
openmeter/notification/event.go (1)
  • Event (51-67)
openmeter/notification/channel.go (1)
  • ChannelType (19-19)
openmeter/notification/deliverystatus.go (6)
  • EventDeliveryStatus (77-93)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSuccess (14-14)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateSending (16-16)
openmeter/notification/rule.go (1)
  • Rule (17-33)
openmeter/ent/db/notificationeventdeliverystatus/where.go (1)
  • ChannelID (90-92)
openmeter/notification/adapter/event.go (3)
openmeter/ent/db/notificationevent/where.go (1)
  • HasDeliveryStatusesWith (376-385)
openmeter/ent/db/notificationeventdeliverystatus/where.go (3)
  • StateNotIn (411-417)
  • NextAttemptAtIsNil (535-537)
  • NextAttemptAtLTE (530-532)
openmeter/notification/deliverystatus.go (2)
  • EventDeliveryStatusStateSuccess (14-14)
  • EventDeliveryStatusStateFailed (15-15)
openmeter/notification/channel.go (1)
openmeter/notification/webhook/secret/secret.go (1)
  • ValidateSigningSecret (32-45)
openmeter/notification/webhook/handler.go (3)
openmeter/notification/webhook/secret/secret.go (1)
  • ValidateSigningSecret (32-45)
openmeter/notification/webhook/errors.go (1)
  • NewValidationError (37-43)
openmeter/notification/deliverystatus.go (2)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryAttempt (56-60)
openmeter/notification/webhook/svix/webhook.go (2)
openmeter/notification/webhook/secret/secret.go (1)
  • NewSigningSecretWithDefaultSize (24-26)
openmeter/notification/webhook/svix/svix.go (1)
  • NullChannel (25-25)
openmeter/notification/eventhandler/webhook.go (9)
openmeter/notification/eventhandler/handler.go (2)
  • New (113-152)
  • Handler (54-71)
openmeter/notification/webhook/handler.go (8)
  • Handler (288-292)
  • MessageDeliveryStatus (180-185)
  • Message (187-204)
  • GetMessageInput (242-249)
  • ExpandParams (235-240)
  • SendMessageInput (208-215)
  • EventType (261-268)
  • Payload (178-178)
openmeter/notification/event.go (2)
  • Event (51-67)
  • EventType (31-31)
openmeter/notification/webhook/errors.go (4)
  • IsNotFoundError (69-71)
  • IsMessageNotReadyError (134-136)
  • IsUnrecoverableError (150-152)
  • RetryableError (79-83)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/notification/deliverystatus.go (5)
  • UpdateEventDeliveryStatusInput (180-193)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusStateSuccess (14-14)
pkg/framework/tracex/tracex.go (2)
  • StartWithNoValue (75-82)
  • Start (32-39)
openmeter/notification/httpdriver/mapping.go (4)
  • FromEventAsBalanceThresholdPayload (581-599)
  • FromEventAsEntitlementResetPayload (601-618)
  • FromEventAsInvoiceCreatedPayload (620-636)
  • FromEventAsInvoiceUpdatedPayload (638-654)
openmeter/notification/eventpayload.go (1)
  • AsRawPayload (14-26)
openmeter/notification/eventhandler/reconcile.go (5)
openmeter/notification/eventhandler/handler.go (1)
  • Handler (54-71)
openmeter/notification/event.go (2)
  • Event (51-67)
  • ListEventsInput (74-97)
openmeter/notification/channel.go (3)
  • Channel (37-51)
  • ChannelType (19-19)
  • ChannelTypeWebhook (14-14)
openmeter/notification/deliverystatus.go (3)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateSending (16-16)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/notification/adapter/entitymapping.go (1)
openmeter/ent/db/notificationeventdeliverystatus/where.go (1)
  • NextAttemptAt (100-102)
openmeter/notification/webhook/secret/secret.go (1)
pkg/models/errors.go (1)
  • NewNillableGenericValidationError (129-135)
openmeter/notification/webhook/svix/message.go (6)
openmeter/notification/webhook/handler.go (5)
  • EventType (261-268)
  • Message (187-204)
  • Payload (178-178)
  • MessageDeliveryStatus (180-185)
  • GetMessageInput (242-249)
openmeter/notification/webhook/svix/internal/error.go (1)
  • WrapSvixError (68-110)
openmeter/notification/webhook/errors.go (2)
  • NewMessageNotReadyError (127-132)
  • NewValidationError (37-43)
openmeter/notification/webhook/svix/annotations.go (4)
  • AnnotationMessageID (4-4)
  • AnnotationMessageEventID (5-5)
  • AnnotationEventType (6-6)
  • AnnotationApplicationUID (8-8)
pkg/framework/tracex/tracex.go (1)
  • Start (32-39)
openmeter/notification/deliverystatus.go (6)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStateSuccess (14-14)
openmeter/notification/deliverystatus.go (5)
api/client/javascript/src/client/schemas.ts (2)
  • EventDeliveryAttemptResponse (11918-11919)
  • Annotations (11692-11692)
api/api.gen.go (2)
  • EventDeliveryAttemptResponse (3464-3476)
  • Annotations (1048-1048)
api/client/go/client.gen.go (2)
  • EventDeliveryAttemptResponse (3177-3189)
  • Annotations (962-962)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/ent/db/notificationeventdeliverystatus/where.go (5)
  • EventID (85-87)
  • ChannelID (90-92)
  • Reason (95-97)
  • CreatedAt (75-77)
  • UpdatedAt (80-82)
cmd/balance-worker/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
openmeter/notification/webhook/svix/internal/error.go (1)
openmeter/notification/webhook/errors.go (4)
  • NewUnrecoverableError (154-162)
  • NewValidationError (37-43)
  • NewNotFoundError (61-67)
  • NewRetryableError (103-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Test
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Code Generators
  • GitHub Check: Build
  • GitHub Check: Repository Scan
  • GitHub Check: Analyze (go)
🔇 Additional comments (27)
openmeter/notification/eventpayload.go (2)

12-13: Nice addition for type clarity!

Introducing the RawPayload type alias makes the intent clearer and improves type documentation. Good refactoring.


14-26: Breaking change is all set—nice work on the complete migration!

Verified that all call sites have been updated to use AsRawPayload. The new RawPayload type is properly defined, and there are zero remaining references to the old PayloadToMapInterface function anywhere in the codebase. The logic is sound and the migration is complete.

openmeter/notification/rule.go (2)

212-214: Nice addition!

This validation ensures rules can't be created without channels, catching the issue early at the model level before hitting the service/database layers.


274-276: Consistent validation, looks good!

The same channel requirement is enforced for updates, maintaining consistency with the create operation.

openmeter/notification/service/rule.go (2)

96-99: Good addition for validation flow!

Adding channel existence validation before creating the rule is a solid improvement. Once the pagination bug in ValidateRuleChannels is fixed, this will properly prevent rules from being created with non-existent channels.


214-220: Clearer error message and consistent validation!

The error message on line 214 is now more specific ("invalid rule type"), and the channel validation addition mirrors the CreateRule implementation nicely. Once the pagination issue is resolved, this will work as intended.

openmeter/notification/webhook/noop/noop.go (1)

73-77: LGTM! Consistent noop implementation.

The GetMessage method follows the established pattern in this file perfectly—logging the request and returning the not-implemented error. Nice and clean!

openmeter/notification/webhook/handler.go (7)

30-30: Nice refactor to public validator interface!

Switching from a local validator interface to models.Validator is clean and consistent across all input types. This makes the validation contract more explicit and reusable.

Also applies to: 50-50, 87-87, 130-130, 154-154, 206-206


79-79: Secret validation looks good!

Delegating to secret.ValidateSigningSecret keeps the validation logic centralized and properly checks the secret format. The nil checks before validation are appropriate too.

Also applies to: 122-122


178-178: Clean type alias for payload!

Using Payload = map[string]any gives semantic meaning to the type while keeping the flexibility of a map. Nice and idiomatic!


180-185: Solid delivery status struct!

The MessageDeliveryStatus design looks great—the NextAttempt pointer handles optional retry timing nicely, and tracking multiple attempts aligns perfectly with the PR's goal of adding delivery attempts to delivery status.


187-204: Well-structured message expansion!

The addition of Annotations and the optional expanded fields (Payload and DeliveryStatuses) as pointers is spot-on. This pattern makes it clear when data was requested vs. not included, avoiding confusion with empty values.


235-240: Clean expansion control!

The ExpandParams struct gives callers a straightforward way to request optional data. The boolean flags are simple and effective.


283-286: Interface extension looks good!

Adding GetMessage to the MessageHandler interface completes the message lifecycle nicely—now you can both send and retrieve messages. The method signature is consistent with the rest of the interface.

openmeter/notification/adapter/event.go (1)

51-59: LGTM! Retry eligibility filter looks solid.

The filter correctly narrows down events for reconciliation by excluding finalized deliveries and selecting those ready for a next attempt. The Or condition elegantly handles both cases: events without a scheduled next attempt and events whose next attempt time has arrived.

openmeter/notification/annotations.go (1)

21-24: Nice! Annotation constants look good.

The new constants follow existing naming conventions, and I appreciate the explicit deprecated annotation for backward compatibility during migration.

openmeter/notification/eventhandler.go (1)

9-13: LGTM! Timeout constants are sensible.

The new timeout values (3h for pending, 48h for sending) provide reasonable guardrails for delivery state transitions. The formatting alignment is clean and consistent with the existing constants.

pkg/models/annotation.go (2)

59-71: Clone looks good!

Clean shallow copy implementation that handles nil receivers correctly.


73-89: Merge implementation is solid.

The nil-safe merge logic is clean, and the semantics (later values overwrite earlier ones) are intuitive and conventional.

openmeter/notification/webhook/svix/annotations.go (1)

1-10: LGTM! SVIX annotation constants are well-organized.

Nice job keeping the SVIX-specific constants in their own package with a consistent svix. prefix. Clean and self-documenting.

test/notification/channel.go (2)

11-11: Import refactor looks good.

The webhook secret functionality has been properly moved to its own package with a clear alias.


111-111: Usage updated correctly.

Test code properly reflects the new import path and alias.

tools/migrate/migrations/20251106124357_notification_deliverystatus.up.sql (1)

1-4: Migration looks solid.

The new columns and index align well with the retry eligibility query filter added in the adapter. The composite index on (namespace, state, next_attempt_at) should efficiently support the reconciliation queries filtering by state and next attempt time.

openmeter/notification/channel.go (2)

7-7: Import refactor applied consistently.

The webhook secret package import matches the refactoring seen in the test files.


105-105: Validation call updated correctly.

Proper usage of the refactored signing secret validation function.

openmeter/notification/httpdriver/mapping.go (1)

547-563: Love the richer attempt payload mapping. Pulling duration, status, body, and URL into the API model keeps the story tight—looks great.

openmeter/notification/deliverystatus.go (1)

80-93: The review comment is incorrect—EventDeliveryStatus is never marshaled directly to JSON.

I searched for direct JSON marshaling of EventDeliveryStatus and found none. Instead, the codebase uses an explicit mapper pattern: httpdriver/mapping.go transforms the domain struct into a separate api.NotificationEventDeliveryStatus struct before serialization. This means the domain struct's JSON tags are irrelevant—the API response shape is controlled entirely by the API struct's tags.

This is a clean domain/API model separation pattern, so dropping JSON tags from the domain struct has zero impact on serialization behavior or downstream consumers. No regressions will occur.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/notification/webhook/svix/internal/error.go (1)

19-25: Now let me verify if the Svix SDK error object provides access to response headers:Let me search more specifically for the Svix Go SDK Error type to understand what methods/fields it exposes:

Based on my verification of the code, the review comment correctly identifies a real issue. The RetryAfter field is defined in the SvixError struct and is actually being used in the Wrap() method (lines 46, 48) to pass retry timing information to webhook.NewRetryableError(). However, the field is never populated when SvixError instances are created.

Both instantiation points (lines 86 and 102) only set HTTPStatus, Code, and Details — leaving RetryAfter at its zero value (0 duration). The Svix SDK error object appears to only expose Status() and Body() methods, with no apparent header access to extract the Retry-After header that would typically come from HTTP 429 or 5xx responses.


Wire up RetryAfter field extraction from response headers.

The RetryAfter field in SvixError is defined and used in the Wrap() method for retryable errors (429, 500, 502, 503, 504), but it's never populated when the error is constructed. You'll need to extract this from HTTP response headers (typically Retry-After or X-RateLimit-Reset) when handling error responses. Verify that the Svix SDK error object exposes the response headers, then parse and populate RetryAfter accordingly in the error construction at lines 86 and 102.

♻️ Duplicate comments (4)
openmeter/notification/service/rule.go (1)

45-49: Critical: Pagination parameters are swapped!

As previously flagged, this will only fetch 1 channel from page N instead of fetching all N channels from page 1. If you're validating 3 channels, this grabs 1 item from page 3, completely skipping the first two pages. Existing channels will be incorrectly reported as missing.

Swap them:

-			Page: pagination.Page{
-				PageSize:   1,
-				PageNumber: len(channels),
-			},
+			Page: pagination.Page{
+				PageSize:   len(channels),
+				PageNumber: 1,
+			},
openmeter/notification/webhook/svix/webhook.go (1)

253-257: Please restore the event-type-only flow.

Line 253 currently drops NullChannel into every update when Channels is empty—even if EventTypes is populated. Svix requires both filters to match, so event-type-only endpoints fall silent after this update. Let’s gate the fallback behind both len(params.Channels) == 0 and len(params.EventTypes) == 0, just like we used to.

-	if len(params.Channels) == 0 {
+	if len(params.Channels) == 0 && len(params.EventTypes) == 0 {
 		params.Channels = []string{
 			NullChannel,
 		}
 	}
openmeter/notification/eventhandler/reconcile.go (1)

35-49: Don’t trust rule channels for reconciliation.

Line 35 gathers channel types from event.Rule.Channels only. If the repo doesn’t eager-load channels or one was deleted post-event, that slice won’t include the channel ID, so we never call reconcileWebhookEvent. Any pending/sending statuses tied to that channel just sit there forever. Please iterate the delivery statuses (or fall back to them) so we always reconcile what the DB holds.

-	channelTypes := lo.UniqMap(event.Rule.Channels, func(item notification.Channel, _ int) notification.ChannelType {
-		return item.Type
-	})
-
-	for _, channelType := range channelTypes {
-		switch channelType {
-		case notification.ChannelTypeWebhook:
-			if err := h.reconcileWebhookEvent(ctx, event); err != nil {
-				errs = append(errs, err)
-			}
-		default:
-			h.logger.ErrorContext(ctx, "unsupported channel type", "type", channelType)
-		}
+	for _, status := range event.DeliveryStatus {
+		switch status.ChannelType {
+		case notification.ChannelTypeWebhook:
+			if err := h.reconcileWebhookEvent(ctx, event); err != nil {
+				errs = append(errs, err)
+			}
+		default:
+			h.logger.ErrorContext(ctx, "unsupported channel type", "type", status.ChannelType)
+		}
openmeter/notification/eventhandler/webhook.go (1)

334-337: Critical: Same bug flagged in past review—this prevents message creation.

Line 335 has if err != nil || webhook.IsNotFoundError(err), which returns an error whenever the message is not found. This prevents the function from falling through to the send path (lines 343-348), so brand-new messages can never be created. This is the exact issue raised in the past review comment.

Apply this fix:

-	msg, err := h.getWebhookMessage(ctx, event)
-	if err != nil || webhook.IsNotFoundError(err) {
-		return nil, fmt.Errorf("failed to get webhook message: %w", err)
-	}
+	msg, err := h.getWebhookMessage(ctx, event)
+	if err != nil && !webhook.IsNotFoundError(err) {
+		return nil, fmt.Errorf("failed to get webhook message: %w", err)
+	}

Note: Check if this function is actually used—if it's dead code, consider removing it instead.

🧹 Nitpick comments (2)
openmeter/notification/webhook/svix/message.go (1)

105-246: Consider making rate-limit sleep configurable.

The hardcoded 100ms sleep (lines 173, 234) between pagination requests is reasonable for avoiding rate limits, but it could add noticeable latency when fetching many pages of delivery attempts. For high-volume scenarios, this might become a bottleneck in the event ingestion path.

Consider extracting the sleep duration to a configurable constant at the package level:

+const (
+	ListAttemptLimit = 25
+	// PaginationSleepDuration is the delay between paginated API calls to avoid rate limits
+	PaginationSleepDuration = 100 * time.Millisecond
+)

-	time.Sleep(100 * time.Millisecond)
+	time.Sleep(PaginationSleepDuration)

This makes it easier to tune performance vs. rate-limit trade-offs in the future. Based on coding guidelines.

openmeter/notification/eventhandler/webhook.go (1)

231-248: Hardcoded 1-hour threshold for stuck messages.

The logic to revert a message stuck in "sending" back to "pending" after 1 hour (line 240) is a reasonable safety net, but the duration is hardcoded. Consider making this configurable alongside sendingTimeout and pendingTimeout for consistency and easier tuning.

+	stuckSendingThreshold time.Duration // e.g., in Handler struct

-	if clock.Now().Sub(stuckInSendingSince) > time.Hour {
+	if clock.Now().Sub(stuckInSendingSince) > h.stuckSendingThreshold {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a310b71 and ec4a0ce.

📒 Files selected for processing (34)
  • app/common/notification.go (2 hunks)
  • app/config/config_test.go (1 hunks)
  • app/config/notification.go (3 hunks)
  • app/config/testdata/complete.yaml (1 hunks)
  • cmd/balance-worker/wire_gen.go (1 hunks)
  • cmd/jobs/internal/wire_gen.go (1 hunks)
  • cmd/notification-service/wire_gen.go (1 hunks)
  • cmd/server/wire_gen.go (1 hunks)
  • openmeter/notification/adapter/deliverystatus.go (1 hunks)
  • openmeter/notification/adapter/entitymapping.go (2 hunks)
  • openmeter/notification/adapter/event.go (1 hunks)
  • openmeter/notification/channel.go (2 hunks)
  • openmeter/notification/deliverystatus.go (3 hunks)
  • openmeter/notification/event.go (1 hunks)
  • openmeter/notification/eventhandler.go (1 hunks)
  • openmeter/notification/eventhandler/deliverystatus.go (1 hunks)
  • openmeter/notification/eventhandler/dispatch.go (1 hunks)
  • openmeter/notification/eventhandler/handler.go (4 hunks)
  • openmeter/notification/eventhandler/reconcile.go (4 hunks)
  • openmeter/notification/eventhandler/webhook.go (1 hunks)
  • openmeter/notification/eventpayload.go (1 hunks)
  • openmeter/notification/httpdriver/mapping.go (2 hunks)
  • openmeter/notification/rule.go (2 hunks)
  • openmeter/notification/service/rule.go (4 hunks)
  • openmeter/notification/webhook/errors.go (2 hunks)
  • openmeter/notification/webhook/handler.go (9 hunks)
  • openmeter/notification/webhook/noop/noop.go (1 hunks)
  • openmeter/notification/webhook/secret/secret.go (2 hunks)
  • openmeter/notification/webhook/svix/annotations.go (1 hunks)
  • openmeter/notification/webhook/svix/internal/error.go (5 hunks)
  • openmeter/notification/webhook/svix/message.go (2 hunks)
  • openmeter/notification/webhook/svix/webhook.go (4 hunks)
  • test/notification/channel.go (2 hunks)
  • test/notification/rule.go (5 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/config/testdata/complete.yaml
🚧 Files skipped from review as they are similar to previous changes (13)
  • openmeter/notification/event.go
  • test/notification/channel.go
  • cmd/balance-worker/wire_gen.go
  • openmeter/notification/adapter/deliverystatus.go
  • app/config/notification.go
  • openmeter/notification/channel.go
  • openmeter/notification/eventhandler.go
  • openmeter/notification/webhook/secret/secret.go
  • openmeter/notification/eventhandler/handler.go
  • openmeter/notification/webhook/errors.go
  • openmeter/notification/webhook/svix/annotations.go
  • openmeter/notification/eventpayload.go
  • openmeter/notification/eventhandler/deliverystatus.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/notification/webhook/noop/noop.go
  • openmeter/notification/webhook/svix/webhook.go
  • app/common/notification.go
  • openmeter/notification/httpdriver/mapping.go
  • openmeter/notification/deliverystatus.go
  • openmeter/notification/service/rule.go
  • test/notification/rule.go
  • openmeter/notification/eventhandler/dispatch.go
  • openmeter/notification/adapter/entitymapping.go
  • openmeter/notification/webhook/handler.go
  • openmeter/notification/eventhandler/reconcile.go
  • app/config/config_test.go
  • cmd/jobs/internal/wire_gen.go
  • openmeter/notification/eventhandler/webhook.go
  • cmd/server/wire_gen.go
  • openmeter/notification/webhook/svix/internal/error.go
  • openmeter/notification/rule.go
  • openmeter/notification/adapter/event.go
  • openmeter/notification/webhook/svix/message.go
  • cmd/notification-service/wire_gen.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.
When appropriate, recommend e2e tests for critical changes.

Files:

  • app/config/config_test.go
🧠 Learnings (1)
📚 Learning: 2025-04-29T19:53:31.951Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 2746
File: openmeter/notification/adapter/rule.go:155-161
Timestamp: 2025-04-29T19:53:31.951Z
Learning: The `GetRule` method in the notification rule repository is intentionally designed to return rules even if they have been soft-deleted (`deleted_at` is set), while `ListRules` filters out deleted rules with `Where(ruledb.DeletedAtIsNil())`. This difference in behavior between get and list operations is by design.

Applied to files:

  • test/notification/rule.go
🧬 Code graph analysis (17)
openmeter/notification/webhook/noop/noop.go (2)
openmeter/notification/webhook/handler.go (3)
  • Handler (288-292)
  • GetMessageInput (242-249)
  • Message (187-204)
openmeter/notification/webhook/errors.go (1)
  • ErrNotImplemented (9-9)
openmeter/notification/webhook/svix/webhook.go (2)
openmeter/notification/webhook/secret/secret.go (1)
  • NewSigningSecretWithDefaultSize (24-26)
openmeter/notification/webhook/svix/svix.go (1)
  • NullChannel (25-25)
app/common/notification.go (1)
app/config/notification.go (1)
  • NotificationConfiguration (21-29)
openmeter/notification/httpdriver/mapping.go (2)
api/api.gen.go (4)
  • Annotations (1048-1048)
  • NotificationEventDeliveryStatusState (5426-5426)
  • NotificationEventDeliveryAttempt (5390-5399)
  • EventDeliveryAttemptResponse (3464-3476)
openmeter/notification/deliverystatus.go (2)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
openmeter/notification/deliverystatus.go (5)
api/client/javascript/src/client/schemas.ts (2)
  • EventDeliveryAttemptResponse (11918-11919)
  • Annotations (11692-11692)
api/api.gen.go (2)
  • EventDeliveryAttemptResponse (3464-3476)
  • Annotations (1048-1048)
api/client/go/client.gen.go (2)
  • EventDeliveryAttemptResponse (3177-3189)
  • Annotations (962-962)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/ent/db/notificationeventdeliverystatus/where.go (5)
  • EventID (85-87)
  • ChannelID (90-92)
  • Reason (95-97)
  • CreatedAt (75-77)
  • UpdatedAt (80-82)
openmeter/notification/service/rule.go (5)
openmeter/notification/rule.go (2)
  • CreateRuleInput (168-185)
  • UpdateRuleInput (228-245)
openmeter/notification/service/service.go (1)
  • Service (20-29)
openmeter/notification/channel.go (2)
  • ListChannelsInput (118-127)
  • Channel (37-51)
pkg/pagination/page.go (1)
  • Page (14-17)
pkg/sortx/order.go (1)
  • Order (3-3)
test/notification/rule.go (2)
openmeter/ent/db/notificationchannel/where.go (1)
  • Namespace (70-72)
openmeter/ent/db/notificationrule/where.go (1)
  • Namespace (70-72)
openmeter/notification/adapter/entitymapping.go (1)
openmeter/ent/db/notificationeventdeliverystatus/where.go (1)
  • NextAttemptAt (100-102)
openmeter/notification/webhook/handler.go (3)
openmeter/notification/webhook/secret/secret.go (1)
  • ValidateSigningSecret (32-45)
openmeter/notification/webhook/errors.go (1)
  • NewValidationError (37-43)
openmeter/notification/deliverystatus.go (2)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryAttempt (56-60)
openmeter/notification/eventhandler/reconcile.go (6)
openmeter/notification/eventhandler/handler.go (1)
  • Handler (54-71)
openmeter/notification/event.go (2)
  • Event (51-67)
  • ListEventsInput (74-97)
openmeter/notification/rule.go (1)
  • Rule (17-33)
openmeter/notification/channel.go (3)
  • Channel (37-51)
  • ChannelType (19-19)
  • ChannelTypeWebhook (14-14)
openmeter/notification/deliverystatus.go (3)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateSending (16-16)
pkg/clock/clock.go (1)
  • Now (14-21)
cmd/jobs/internal/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
openmeter/notification/eventhandler/webhook.go (9)
openmeter/notification/eventhandler/handler.go (2)
  • New (113-152)
  • Handler (54-71)
openmeter/notification/webhook/handler.go (8)
  • Handler (288-292)
  • MessageDeliveryStatus (180-185)
  • Message (187-204)
  • GetMessageInput (242-249)
  • ExpandParams (235-240)
  • SendMessageInput (208-215)
  • EventType (261-268)
  • Payload (178-178)
openmeter/notification/event.go (2)
  • Event (51-67)
  • EventType (31-31)
openmeter/notification/webhook/errors.go (4)
  • IsNotFoundError (69-71)
  • IsMessageNotReadyError (134-136)
  • IsUnrecoverableError (150-152)
  • RetryableError (79-83)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/notification/deliverystatus.go (5)
  • UpdateEventDeliveryStatusInput (180-193)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusStateSuccess (14-14)
pkg/framework/tracex/tracex.go (2)
  • StartWithNoValue (75-82)
  • Start (32-39)
openmeter/notification/httpdriver/mapping.go (4)
  • FromEventAsBalanceThresholdPayload (581-599)
  • FromEventAsEntitlementResetPayload (601-618)
  • FromEventAsInvoiceCreatedPayload (620-636)
  • FromEventAsInvoiceUpdatedPayload (638-654)
openmeter/notification/eventpayload.go (1)
  • AsRawPayload (14-26)
cmd/server/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
openmeter/notification/webhook/svix/internal/error.go (1)
openmeter/notification/webhook/errors.go (4)
  • NewUnrecoverableError (154-162)
  • NewValidationError (37-43)
  • NewNotFoundError (61-67)
  • NewRetryableError (103-116)
openmeter/notification/adapter/event.go (3)
openmeter/ent/db/notificationevent/where.go (1)
  • HasDeliveryStatusesWith (376-385)
openmeter/ent/db/notificationeventdeliverystatus/where.go (3)
  • StateNotIn (411-417)
  • NextAttemptAtIsNil (535-537)
  • NextAttemptAtLTE (530-532)
openmeter/notification/deliverystatus.go (2)
  • EventDeliveryStatusStateSuccess (14-14)
  • EventDeliveryStatusStateFailed (15-15)
openmeter/notification/webhook/svix/message.go (3)
openmeter/notification/webhook/handler.go (5)
  • EventType (261-268)
  • Message (187-204)
  • Payload (178-178)
  • MessageDeliveryStatus (180-185)
  • GetMessageInput (242-249)
openmeter/notification/webhook/svix/internal/error.go (1)
  • WrapSvixError (68-110)
openmeter/notification/deliverystatus.go (6)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStateSuccess (14-14)
cmd/notification-service/wire_gen.go (1)
app/common/notification.go (1)
  • NewNotificationEventHandler (63-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Artifacts / Benthos Collector Container image
  • GitHub Check: Code Generators
  • GitHub Check: Migration Checks
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Build
  • GitHub Check: SAST (semgrep)
  • GitHub Check: Analyze (go)
🔇 Additional comments (34)
openmeter/notification/rule.go (2)

212-214: LGTM! Good validation addition.

This ensures rules have at least one channel, which makes sense given the channel validation happening downstream.


274-276: LGTM! Consistent validation.

Mirrors the CreateRuleInput validation, ensuring both create and update paths enforce the channel requirement.

test/notification/rule.go (2)

171-171: LGTM! Consistent test updates.

All call sites properly updated to pass the namespace parameter.

Also applies to: 176-176, 214-214, 264-264, 281-281


239-239: LGTM! Necessary test update.

This aligns with the new requirement that UpdateRuleInput must include channels.

openmeter/notification/service/rule.go (3)

60-76: LGTM! Solid validation logic.

The approach of building a channel ID map and checking membership is clean and efficient. Once the pagination bug is fixed, this will correctly validate channel existence.


96-99: LGTM! Good validation placement.

Running channel validation before rule creation makes sense, and the error wrapping is consistent with the feature validation above.


214-220: LGTM! Clear separation of validation concerns.

The distinction between rule type validation and channel validation is clean, with specific error messages for each.

app/config/config_test.go (1)

384-386: LGTM! New timeout fields look good.

The new notification configuration fields (ReconcileInterval, SendingTimeout, PendingTimeout) are properly wired into the test expectations. The values are sensible for a delivery retry system.

openmeter/notification/webhook/svix/internal/error.go (2)

38-51: Nice error classification improvements!

The expanded error mapping looks solid:

  • Treating 400 and 409 as unrecoverable validation errors makes sense
  • Handling 429 and 5xx as retryable with backoff is the right pattern
  • The use of RetryAfter field aligns with standard retry semantics

One quick note: the 422 case on line 40 is now treated differently than before (as unrecoverable validation). Just want to confirm this is intentional and aligns with how Svix uses 422.


86-93: Cleaner validation error formatting.

The new format [location=.. type=..]: message is more structured and parseable. Good improvement for debugging!

openmeter/notification/adapter/event.go (1)

51-59: Solid retry eligibility filter!

The logic for filtering events by NextAttemptBefore looks correct:

  • Excludes terminal states (Success/Failed) ✓
  • Handles both unscheduled attempts (NextAttemptAt is nil) and scheduled attempts that are due ✓
  • Proper UTC conversion ✓

This should efficiently find events ready for retry without pulling in already-completed deliveries.

openmeter/notification/webhook/noop/noop.go (1)

73-77: LGTM! Consistent noop implementation.

The new GetMessage method follows the same pattern as the other noop handlers in this file. Logging + returning ErrNotImplemented is exactly what we'd expect here.

openmeter/notification/adapter/entitymapping.go (1)

136-143: Clean mapping for new delivery fields.

The UTC pointer handling for NextAttempt is solid—proper nil check and conversion. Direct assignment of Attempts looks good too. The use of lo helpers keeps this readable.

cmd/notification-service/wire_gen.go (1)

152-152: Generated code reflects wiring changes correctly.

The updated call to NewNotificationEventHandler now passes notificationConfiguration as the first parameter, aligning with the new signature in app/common/notification.go. This enables the handler to use the new timeout configurations (ReconcileInterval, SendingTimeout, PendingTimeout).

Since this is Wire-generated code, no action needed here—just confirming it matches the expected signature.

openmeter/notification/eventhandler/dispatch.go (1)

13-18: Nice refactor to unified reconciliation!

Simplifying the dispatch logic to delegate to reconcileEvent is a solid move. This should make the event handling flow more consistent and easier to maintain. The goroutine, timeout, and tracing setup all look good.

cmd/jobs/internal/wire_gen.go (1)

406-406: Generated code updated correctly.

Same as the notification-service wiring—the call to NewNotificationEventHandler now includes notificationConfiguration as the first parameter. This is expected Wire-generated code reflecting the signature change.

openmeter/notification/webhook/svix/message.go (4)

49-59: Nice improvement to span attribute naming!

The hierarchical dot-notation (e.g., svix.event.id instead of svix.event_id) is clearer and more consistent. Moving the idempotency_key to the AddEvent call keeps it scoped to the specific operation without cluttering all child spans.


248-305: Clean implementation of conditional expansion!

The flexible ID/EventID coalescing (line 250) and the conditional expansion of payload and delivery status (lines 269-285) make the API both convenient and efficient. The annotations population ensures full context is available for downstream processing.


307-320: LGTM!

The helper cleanly maps Svix message statuses to internal delivery states. Returning an empty string for unknown statuses allows callers to detect and handle unmapped cases appropriately.


81-95: Good traceability with annotations.

Populating annotations immediately on message creation (lines 89-94) ensures the message is traceable throughout its lifecycle. Keeping DeliveryStatuses nil initially makes sense since no delivery attempts have occurred yet.

openmeter/notification/deliverystatus.go (4)

49-60: Well-structured delivery attempt types.

The EventDeliveryAttempt and EventDeliveryAttemptResponse structs capture all the necessary details for tracking delivery attempts. Using time.Duration instead of milliseconds as an integer is cleaner and more type-safe.


62-75: LGTM!

The sorting function correctly implements descending order by timestamp using the idiomatic slices.SortFunc. The logic is clear and handles equal timestamps properly.


180-193: Good extension of the input struct.

Adding NextAttempt and Attempts to UpdateEventDeliveryStatusInput aligns with the new delivery status fields and enables the reconciliation logic to track retry schedules and attempt history.


77-93: No action needed—the JSON tag removal is safe.

The code has a proper mapping layer: FromEvent() explicitly transforms the domain notification.EventDeliveryStatus into a separate api.NotificationEventDeliveryStatus struct (line ~160 in mapping.go). Each field is manually mapped, so removing JSON tags from the domain model doesn't affect the API response contract, which is controlled by the api.* types.

Likely an incorrect or invalid review comment.

openmeter/notification/eventhandler/webhook.go (6)

20-25: Clear error categorization.

The error constants nicely distinguish between user-side recipient failures and system-side issues, and they explicitly state retry behavior. This makes the reconciliation logic's intent clearer.


27-60: Good defensive setup and the past review concern is addressed.

The nil check, early return for no active statuses, and filtering logic (lines 29-53) are solid. The error handling on line 58 now correctly returns only on real errors (not NotFoundError), which allows the reconciliation to proceed to sending the message when it doesn't exist yet—fixing the issue raised in the past review comment.


79-97: Double-check the pending timeout calculation.

On line 86, you're calculating the timeout as nextAttempt.Sub(status.CreatedAt), which measures the total time elapsed since the status was created. This works if nextAttempt is only set once, but if it's updated multiple times during retries, this calculation might not accurately represent how long the status has been pending. Consider whether you want to measure "time since creation" or "time since last retry attempt."

If nextAttempt is updated on each retry, the timeout check may not behave as intended. Please verify the intended semantics.


278-306: Solid error aggregation and completion.

The handling of terminal states (lines 278-282) and the error aggregation pattern (lines 297-302) ensure that all delivery statuses are processed even if some updates fail. This resilience is important for the reconciliation loop.


354-442: Defensive message sending with proper error handling.

The sendWebhookMessage function wisely checks if the message already exists (lines 407-420) before attempting to send, which avoids duplicate sends. The error handling correctly distinguishes between "not found" (expected) and real errors (line 414).


444-483: Clean event-to-payload serialization.

The switch statement handles all known event types and delegates to the appropriate mapping functions from httpdriver. The explicit error for unknown event types (line 472) ensures new event types won't silently fail.

openmeter/notification/webhook/handler.go (4)

30-30: Nice standardization on models.Validator!

Switching all input structs to implement models.Validator creates consistency across the codebase and makes it easier to handle validation uniformly.

Also applies to: 50-50, 87-87, 130-130, 154-154, 206-206


178-204: Well-designed message structure with optional expansion.

The Message struct now supports optional expansion of payload and delivery statuses through pointer fields (lines 200, 203), which keeps the default response lean while allowing clients to request additional details when needed. The Payload type alias (line 178) also makes the API cleaner.


206-249: Good API design for message retrieval and expansion.

The ExpandParams struct (lines 235-240) provides a clean way to control what data is included in message responses. Supporting lookup by either ID or EventID in GetMessageInput (lines 242-249) gives clients flexibility.


283-286: Interface extension looks good.

Adding GetMessage to the MessageHandler interface completes the message lifecycle API and enables the reconciliation logic to retrieve message status from the webhook provider.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
openmeter/notification/httpdriver/mapping.go (2)

547-566: Consider side effects: the input slice is modified in place.

The function calls SortEventDeliveryAttemptsInDescOrder(attempts), which sorts the input slice directly. This could surprise callers who don't expect their slice to be reordered. Consider making a copy before sorting to avoid this side effect:

 func AsEventDeliveryAttempts(attempts []notification.EventDeliveryAttempt) []api.NotificationEventDeliveryAttempt {
 	result := make([]api.NotificationEventDeliveryAttempt, 0, len(attempts))
 
-	notification.SortEventDeliveryAttemptsInDescOrder(attempts)
+	attemptsCopy := slices.Clone(attempts)
+	notification.SortEventDeliveryAttemptsInDescOrder(attemptsCopy)
 
-	for _, attempt := range attempts {
+	for _, attempt := range attemptsCopy {
 		result = append(result, api.NotificationEventDeliveryAttempt{

558-558: Minor: Duration conversion to int might overflow.

The conversion from int64 (returned by Milliseconds()) to int could theoretically overflow on 32-bit systems for very long durations (though unlikely in practice for delivery attempts). If you want to be extra safe, consider using int64 in the API type or adding bounds checking.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec4a0ce and 7809943.

📒 Files selected for processing (3)
  • openmeter/notification/deliverystatus.go (3 hunks)
  • openmeter/notification/httpdriver/mapping.go (2 hunks)
  • openmeter/notification/service/rule.go (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/notification/service/rule.go
  • openmeter/notification/deliverystatus.go
  • openmeter/notification/httpdriver/mapping.go
🧬 Code graph analysis (3)
openmeter/notification/service/rule.go (5)
openmeter/notification/rule.go (2)
  • CreateRuleInput (168-185)
  • UpdateRuleInput (228-245)
openmeter/notification/service/service.go (1)
  • Service (20-29)
openmeter/notification/channel.go (2)
  • ListChannelsInput (118-127)
  • Channel (37-51)
pkg/pagination/page.go (1)
  • Page (14-17)
pkg/sortx/order.go (1)
  • Order (3-3)
openmeter/notification/deliverystatus.go (5)
api/client/javascript/src/client/schemas.ts (2)
  • EventDeliveryAttemptResponse (11918-11919)
  • Annotations (11692-11692)
api/api.gen.go (2)
  • EventDeliveryAttemptResponse (3464-3476)
  • Annotations (1048-1048)
api/client/go/client.gen.go (2)
  • EventDeliveryAttemptResponse (3177-3189)
  • Annotations (962-962)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/ent/db/notificationeventdeliverystatus/where.go (5)
  • EventID (85-87)
  • ChannelID (90-92)
  • Reason (95-97)
  • CreatedAt (75-77)
  • UpdatedAt (80-82)
openmeter/notification/httpdriver/mapping.go (2)
api/api.gen.go (3)
  • NotificationEventDeliveryStatusState (5426-5426)
  • NotificationEventDeliveryAttempt (5390-5399)
  • EventDeliveryAttemptResponse (3464-3476)
openmeter/notification/deliverystatus.go (3)
  • EventDeliveryAttempt (56-60)
  • SortEventDeliveryAttemptsInDescOrder (63-67)
  • EventDeliveryAttemptResponse (49-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Lint
  • GitHub Check: Migration Checks
  • GitHub Check: Test
  • GitHub Check: Code Generators
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (8)
openmeter/notification/service/rule.go (4)

45-55: Nice fix on the pagination! 🎉

Great job addressing the previous review - the pagination parameters are now correct (PageSize: len(channels), PageNumber: 1). This will properly fetch all requested channels in a single page.

One tiny optimization: the OrderBy and Order parameters don't really matter here since we're just checking existence. You could omit them to simplify the call, but honestly it's not a big deal.


96-99: Solid validation addition!

Good call adding channel validation before rule creation. This prevents creating rules with non-existent channels, which would break at runtime.


214-215: More specific error message - nice improvement!

Changing from a generic error to "invalid rule type" makes it clearer what went wrong when someone tries to change a rule's type.


217-220: Double database call for channels - is this intentional?

I noticed that UpdateRule now calls ListChannels twice:

  1. Here via ValidateRuleChannels (fetches all channels in params.Channels to validate they exist)
  2. Later at lines 245-255 (fetches channels in the diff to update webhooks)

The two calls fetch different channel sets and serve different purposes, so I understand why they're separate. The validation call ensures we fail fast before making any changes, which is good. The second call is also conditional (only runs if channels changed).

That said, since this is in a critical path (per your coding guidelines), just wanted to verify this is the intended approach and the performance impact is acceptable.

openmeter/notification/httpdriver/mapping.go (1)

466-474: Nice mapping of the new delivery status fields!

The mapping logic looks solid. All the new fields (Annotations, Attempts, NextAttempt, Reason) are properly populated from the delivery status.

openmeter/notification/deliverystatus.go (3)

49-67: New delivery attempt types look good!

The EventDeliveryAttemptResponse and EventDeliveryAttempt structs are well-structured with appropriate JSON tags. The sorting function correctly uses time.Compare and reverses the order for descending sort.


181-184: New fields added cleanly to update input.

The NextAttempt and Attempts fields are added with clear comments. Looks good!


73-84: No issues found. The JSON tag removal is safe.

Here's why: EventDeliveryStatus is never directly JSON marshaled. The codebase uses an explicit mapping pattern in openmeter/notification/httpdriver/mapping.go (lines 463–475) where it field-by-field constructs the API response type (api.NotificationEventDeliveryStatus) from the domain model. The API response type has its own JSON tags, so consumers see the correct field names.

The Event struct does include DeliveryStatus []EventDeliveryStatus, but when Event is serialized for API responses, it goes through httpdriver mapping—not direct marshaling.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
openmeter/notification/eventhandler/webhook.go (3)

232-248: Reconsider the "stuck in SENDING" recovery logic.

When the message is not found while in SENDING state, the code waits an hour before moving back to PENDING. A few things feel off here:

  • If we're in SENDING but the provider has no record of the message, that's a pretty serious inconsistency—not just a stuck state
  • The hardcoded 1-hour wait doesn't align with the configurable sendingTimeout
  • This looks like a defensive workaround that might paper over an upstream bug

Would it make more sense to either:

  1. Treat this as an unrecoverable error (the message mysteriously vanished), or
  2. Move immediately back to PENDING to retry sending?

254-265: Consider treating missing channel status as transient rather than unrecoverable.

Right now, if the provider returns a message without a matching channelID in its delivery statuses, we immediately mark it as FAILED with an unrecoverable error. But this could just be a timing issue—the provider might not have synced that channel's status yet.

Failing permanently on what might be a transient condition feels too aggressive. Maybe log a warning and schedule a retry instead?


371-384: Remove redundant GetMessage call.

The caller (reconcileWebhookEvent) already checked whether the message exists before calling sendWebhookMessage. This extra GetMessage call just duplicates that work.

-	msg, err := h.webhook.GetMessage(ctx, webhook.GetMessageInput{
-		Namespace: event.Namespace,
-		EventID:   event.ID,
-		Expand: webhook.ExpandParams{
-			DeliveryStatus: true,
-		},
-	})
-	if err != nil && !webhook.IsNotFoundError(err) {
-		return nil, fmt.Errorf("failed to get message for webhook channel: %w", err)
-	}
-
-	if msg != nil {
-		return msg, nil
-	}
-
 	payload, err := eventAsPayload(event)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7809943 and b664c23.

📒 Files selected for processing (2)
  • openmeter/notification/eventhandler/webhook.go (1 hunks)
  • openmeter/notification/webhook/svix/message.go (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/notification/eventhandler/webhook.go
  • openmeter/notification/webhook/svix/message.go
🧠 Learnings (1)
📚 Learning: 2025-11-07T11:28:40.399Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3581
File: openmeter/notification/webhook/svix/webhook.go:253-257
Timestamp: 2025-11-07T11:28:40.399Z
Learning: In the openmeter notification system (specifically in openmeter/notification/webhook/svix/webhook.go), event type filtering for webhooks is not allowed or supported. Only channel-based filtering is used.

Applied to files:

  • openmeter/notification/eventhandler/webhook.go
🧬 Code graph analysis (2)
openmeter/notification/eventhandler/webhook.go (10)
openmeter/notification/eventhandler/handler.go (2)
  • New (113-152)
  • Handler (54-71)
openmeter/notification/webhook/handler.go (8)
  • Handler (288-292)
  • MessageDeliveryStatus (180-185)
  • Message (187-204)
  • GetMessageInput (242-249)
  • ExpandParams (235-240)
  • SendMessageInput (208-215)
  • EventType (261-268)
  • Payload (178-178)
openmeter/notification/event.go (2)
  • Event (51-67)
  • EventType (31-31)
openmeter/notification/webhook/errors.go (4)
  • IsNotFoundError (69-71)
  • IsMessageNotReadyError (134-136)
  • IsUnrecoverableError (150-152)
  • RetryableError (79-83)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/notification/deliverystatus.go (5)
  • UpdateEventDeliveryStatusInput (172-185)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusStateSuccess (14-14)
openmeter/notification/webhook/svix/svix.go (1)
  • Error (28-28)
pkg/framework/tracex/tracex.go (2)
  • StartWithNoValue (75-82)
  • Start (32-39)
openmeter/notification/httpdriver/mapping.go (4)
  • FromEventAsBalanceThresholdPayload (583-601)
  • FromEventAsEntitlementResetPayload (603-620)
  • FromEventAsInvoiceCreatedPayload (622-638)
  • FromEventAsInvoiceUpdatedPayload (640-656)
openmeter/notification/eventpayload.go (1)
  • AsRawPayload (14-26)
openmeter/notification/webhook/svix/message.go (4)
openmeter/notification/webhook/handler.go (5)
  • EventType (261-268)
  • Message (187-204)
  • Payload (178-178)
  • MessageDeliveryStatus (180-185)
  • GetMessageInput (242-249)
openmeter/notification/webhook/svix/internal/error.go (1)
  • WrapSvixError (68-110)
openmeter/notification/webhook/svix/annotations.go (4)
  • AnnotationMessageID (4-4)
  • AnnotationMessageEventID (5-5)
  • AnnotationEventType (6-6)
  • AnnotationApplicationUID (8-8)
openmeter/notification/deliverystatus.go (6)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStateSuccess (14-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Quickstart
  • GitHub Check: E2E
  • GitHub Check: Build
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Code Generators
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
openmeter/notification/webhook/svix/message.go (4)

61-87: Nice fix on the payload handling!

Good call using &params.Payload from the input instead of &out.Payload from the Svix response. Since WithContent: false is set to optimize the API call, the response payload would be empty anyway. This addresses the concern from the previous review.


173-173: Verify the sleep timing is appropriate for expected load.

The 100ms sleep between pagination requests is rate-limit friendly, but since this is in a message processing path, it could add latency when dealing with many delivery attempts. Is this timing appropriate for your expected volume of endpoints and attempts per message?

Consider making the sleep duration configurable or documenting the expected latency characteristics.


206-210: Nice handling of the transient failure state.

Good catch treating FAILED with a non-nil NextAttempt as SENDING. The comment clearly explains why Svix might return FAIL even while still attempting delivery. This makes the status more accurate for end users.


269-294: Clean conditional expansion logic.

The implementation properly respects the Expand parameters for both payload and delivery statuses. Setting WithContent based on params.Expand.Payload and conditionally fetching delivery statuses keeps the API efficient while providing flexibility. Nice work!

openmeter/notification/eventhandler/webhook.go (1)

211-229: Verify the sending timeout calculation logic.

Same concern as with the PENDING timeout: using nextAttempt.Sub(status.CreatedAt) might not accurately reflect how long the status has been in SENDING state. Consider verifying whether clock.Now().Sub(status.CreatedAt) is the intended check.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
openmeter/notification/webhook/svix/message.go (1)

261-261: Span attribute doesn't reflect the actual message ID used.

You're setting the span attribute to params.ID, but the actual message lookup on line 250 uses msgID (which is CoalesceOrEmpty(params.ID, params.EventID)). If params.ID is empty and params.EventID is used, the trace won't show what was actually queried.

Apply this diff to use the coalesced value:

 		msgID := lo.CoalesceOrEmpty(params.ID, params.EventID)
 
 		if msgID == "" {
 			return nil, webhook.NewValidationError(
 				errors.New("either svix message id or event id must be provided"),
 			)
 		}
 
 		span := trace.SpanFromContext(ctx)
 
 		spanAttrs := []attribute.KeyValue{
-			attribute.String(AnnotationMessageEventID, params.ID),
+			attribute.String(AnnotationMessageEventID, msgID),
 			attribute.String(AnnotationApplicationUID, params.Namespace),
 		}
🧹 Nitpick comments (2)
openmeter/notification/webhook/svix/message.go (2)

108-109: Consider more specific error messages.

The generic "invalid params" message doesn't tell the caller which parameter is missing. While it works, something like "namespace and eventID are required" would be more helpful for debugging.

-		return nil, fmt.Errorf("invalid params")
+		return nil, fmt.Errorf("namespace and eventID are required")

173-173: Consider making rate-limit sleep duration configurable.

The hardcoded 100ms sleeps work, but making this configurable (or at least a package constant) would make it easier to tune for different rate limits or testing scenarios. Not critical for now, but worth considering if you need to adjust these values later.

You could add a constant at the package level:

const (
	ListAttemptLimit      uint64 = 25
	RateLimitSleepDuration = 100 * time.Millisecond
)

Then use time.Sleep(RateLimitSleepDuration) instead of the hardcoded values.

Also applies to: 234-234

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b664c23 and 5f342f6.

📒 Files selected for processing (6)
  • openmeter/notification/eventhandler/webhook.go (1 hunks)
  • openmeter/notification/webhook/svix/annotations.go (1 hunks)
  • openmeter/notification/webhook/svix/application.go (1 hunks)
  • openmeter/notification/webhook/svix/event.go (1 hunks)
  • openmeter/notification/webhook/svix/message.go (2 hunks)
  • openmeter/notification/webhook/svix/webhook.go (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/notification/webhook/svix/webhook.go
  • openmeter/notification/webhook/svix/event.go
  • openmeter/notification/eventhandler/webhook.go
  • openmeter/notification/webhook/svix/message.go
  • openmeter/notification/webhook/svix/application.go
  • openmeter/notification/webhook/svix/annotations.go
🧠 Learnings (2)
📚 Learning: 2025-11-07T12:29:09.025Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3581
File: openmeter/notification/eventhandler/webhook.go:391-396
Timestamp: 2025-11-07T12:29:09.025Z
Learning: In the OpenMeter notification system (specifically in openmeter/notification/eventhandler/webhook.go), webhook channels are equivalent to notification rules. When sending webhook messages, the rule ID (event.Rule.ID) should be used as the channel identifier in the Channels field of SendMessageInput.

Applied to files:

  • openmeter/notification/webhook/svix/webhook.go
  • openmeter/notification/eventhandler/webhook.go
  • openmeter/notification/webhook/svix/message.go
📚 Learning: 2025-11-07T11:28:40.399Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3581
File: openmeter/notification/webhook/svix/webhook.go:253-257
Timestamp: 2025-11-07T11:28:40.399Z
Learning: In the openmeter notification system (specifically in openmeter/notification/webhook/svix/webhook.go), event type filtering for webhooks is not allowed or supported. Only channel-based filtering is used.

Applied to files:

  • openmeter/notification/webhook/svix/webhook.go
  • openmeter/notification/webhook/svix/event.go
  • openmeter/notification/eventhandler/webhook.go
🧬 Code graph analysis (5)
openmeter/notification/webhook/svix/webhook.go (3)
openmeter/notification/webhook/secret/secret.go (1)
  • NewSigningSecretWithDefaultSize (24-26)
openmeter/notification/webhook/svix/annotations.go (3)
  • AnnotationApplicationUID (10-10)
  • AnnotationEndpointUID (13-13)
  • AnnotationEndpointURL (14-14)
openmeter/notification/webhook/svix/svix.go (1)
  • NullChannel (25-25)
openmeter/notification/webhook/svix/event.go (1)
openmeter/notification/webhook/svix/annotations.go (2)
  • AnnotationEventType (7-7)
  • AnnotationEventGroupName (8-8)
openmeter/notification/eventhandler/webhook.go (9)
openmeter/notification/eventhandler/handler.go (2)
  • New (113-152)
  • Handler (54-71)
openmeter/notification/webhook/handler.go (8)
  • Handler (288-292)
  • MessageDeliveryStatus (180-185)
  • Message (187-204)
  • GetMessageInput (242-249)
  • ExpandParams (235-240)
  • SendMessageInput (208-215)
  • EventType (261-268)
  • Payload (178-178)
openmeter/notification/event.go (2)
  • Event (51-67)
  • EventType (31-31)
openmeter/notification/webhook/errors.go (4)
  • IsNotFoundError (69-71)
  • IsMessageNotReadyError (134-136)
  • IsUnrecoverableError (150-152)
  • RetryableError (79-83)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/notification/deliverystatus.go (5)
  • UpdateEventDeliveryStatusInput (172-185)
  • EventDeliveryStatusStatePending (17-17)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusStateSuccess (14-14)
pkg/framework/tracex/tracex.go (2)
  • StartWithNoValue (75-82)
  • Start (32-39)
openmeter/notification/httpdriver/mapping.go (4)
  • FromEventAsBalanceThresholdPayload (583-601)
  • FromEventAsEntitlementResetPayload (603-620)
  • FromEventAsInvoiceCreatedPayload (622-638)
  • FromEventAsInvoiceUpdatedPayload (640-656)
openmeter/notification/eventpayload.go (1)
  • AsRawPayload (14-26)
openmeter/notification/webhook/svix/message.go (4)
openmeter/notification/webhook/svix/annotations.go (5)
  • AnnotationMessageEventID (5-5)
  • AnnotationEventType (7-7)
  • AnnotationApplicationUID (10-10)
  • AnnotationMessageID (4-4)
  • AnnotationMessageAttemptsCount (6-6)
openmeter/notification/webhook/handler.go (5)
  • EventType (261-268)
  • Message (187-204)
  • Payload (178-178)
  • MessageDeliveryStatus (180-185)
  • GetMessageInput (242-249)
openmeter/notification/webhook/svix/internal/error.go (1)
  • WrapSvixError (68-110)
openmeter/notification/deliverystatus.go (6)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStateSuccess (14-14)
openmeter/notification/webhook/svix/application.go (1)
openmeter/notification/webhook/svix/annotations.go (2)
  • AnnotationApplicationName (11-11)
  • AnnotationApplicationUID (10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Migration Checks
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Code Generators
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
openmeter/notification/webhook/svix/message.go (6)

50-59: Nice tracing additions!

The span attributes and events add good observability to the message sending flow. The attributes are well-chosen and will help with debugging.


87-87: Payload assignment looks correct.

You're returning &params.Payload (the input payload), not &out.Payload (the Svix response). Since WithContent: false is used for optimization, the Svix response won't include payload content, but that's fine—you're echoing back what was sent. This correctly preserves the caller's payload in the response.


208-210: Smart handling of transient failed state!

Good catch that Svix returns FAIL even when it's still attempting delivery (if NextAttempt is set). Mapping that to SENDING makes the state more accurate for callers.


270-270: Payload expansion logic looks good!

The conditional WithContent based on params.Expand.Payload (line 270) and the corresponding payload assignment (line 293) are properly aligned. Nice job keeping the optimization consistent with what's actually requested.

Also applies to: 293-293


278-285: Clean conditional delivery status fetching.

The expansion logic for delivery statuses is clear and well-structured. The error handling is appropriate too.


307-320: Clean status mapping.

The deliveryStateFromSvixMessageStatus helper is straightforward and handles all the expected Svix message statuses. Returning an empty string for unknown statuses allows callers to detect and handle unexpected values, which is good.

Comment on lines +141 to +142
return nil, fmt.Errorf("failed to map delivery attempt state [svix.message.status=%d svix.message.status_test=%s]: %w",
attempt.Status, attempt.StatusText, err)
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix error reference in error messages.

Lines 141-142 and 213-214 reference %w, err in the error format string, but err is nil at those points (it was already checked and wrapped on the previous lines 133 and 199). Remove the %w, err portion or these will panic or format incorrectly.

Apply this diff:

 				if state == "" {
-					return nil, fmt.Errorf("failed to map delivery attempt state [svix.message.status=%d svix.message.status_test=%s]: %w",
-						attempt.Status, attempt.StatusText, err)
+					return nil, fmt.Errorf("failed to map delivery attempt state [svix.message.status=%d svix.message.status_text=%s]",
+						attempt.Status, attempt.StatusText)
 				}

And similarly:

 				if state == "" {
-					return nil, fmt.Errorf("failed to map delivery status [svix.message.status=%d svix.message.status_test=%s]: %w",
-						dest.Status, dest.StatusText, err)
+					return nil, fmt.Errorf("failed to map delivery status [svix.message.status=%d svix.message.status_text=%s]",
+						dest.Status, dest.StatusText)
 				}

Note: Also fixed the typo "status_test" → "status_text" in both error messages.

Also applies to: 213-214

🤖 Prompt for AI Agents
In openmeter/notification/webhook/svix/message.go around lines 141-142 and
213-214, the fmt.Errorf calls include a "%w" and "err" even though err is nil at
those points (it was already handled/wrapped earlier), which will panic or
misformat; remove the "%w, err" portion from both error format calls and update
the field name from "status_test" to "status_text" in the message format so the
error strings correctly include only attempt.Status and attempt.StatusText
without attempting to wrap a non-existent error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix it in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chrisgacsal, sounds good! Would you like me to create a GitHub issue to track this fix for the follow-up PR? I can include the details about the error formatting and the typo correction.

@chrisgacsal chrisgacsal merged commit fa9b806 into main Nov 7, 2025
27 checks passed
@chrisgacsal chrisgacsal deleted the refactor/reconcile branch November 7, 2025 13:02
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
openmeter/notification/webhook/svix/message.go (3)

141-142: Fix error formatting - err is nil at this point.

On line 141-142, you're trying to wrap err with %w, but err is already nil here (it was checked and wrapped on line 133-134). This will either panic or produce confusing output. Also noticed a small typo: "status_test" should be "status_text".

Apply this diff:

 				if state == "" {
-					return nil, fmt.Errorf("failed to map delivery attempt state [svix.message.status=%d svix.message.status_test=%s]: %w",
-						attempt.Status, attempt.StatusText, err)
+					return nil, fmt.Errorf("failed to map delivery attempt state [svix.message.status=%d svix.message.status_text=%s]",
+						attempt.Status, attempt.StatusText)
 				}

213-214: Same error formatting issue here.

Similar to the issue on lines 141-142, you're using %w, err when err is nil (already handled on line 199). Also has the "status_test" typo.

Apply this diff:

 				if state == "" {
-					return nil, fmt.Errorf("failed to map delivery status [svix.message.status=%d svix.message.status_test=%s]: %w",
-						dest.Status, dest.StatusText, err)
+					return nil, fmt.Errorf("failed to map delivery status [svix.message.status=%d svix.message.status_text=%s]",
+						dest.Status, dest.StatusText)
 				}

260-263: Span attributes should reflect the actual message ID used.

On lines 261-262, you're setting span attributes to params.ID and params.EventID separately, but line 250 coalesces them into msgID which is what's actually used in the API call (line 270). The trace would be more accurate if it showed the ID that was actually queried.

Consider this adjustment:

 	fn := func(ctx context.Context) (*webhook.Message, error) {
 		msgID := lo.CoalesceOrEmpty(params.ID, params.EventID)
 
 		if msgID == "" {
 			return nil, webhook.NewValidationError(
 				errors.New("either svix message id or event id must be provided"),
 			)
 		}
 
 		span := trace.SpanFromContext(ctx)
 
 		spanAttrs := []attribute.KeyValue{
-			attribute.String(AnnotationMessageID, params.ID),
-			attribute.String(AnnotationMessageEventID, params.EventID),
+			attribute.String(AnnotationMessageID, msgID),
 			attribute.String(AnnotationApplicationUID, params.Namespace),
 		}

Alternatively, if you want to keep both for debugging, add the coalesced value as a separate attribute.

🧹 Nitpick comments (2)
openmeter/notification/webhook/svix/message.go (2)

173-173: Consider extracting the rate-limit sleep duration to a constant.

The hardcoded 100 * time.Millisecond sleep appears both here and on line 234. Extracting it to a named constant (e.g., rateLimitSleepDuration) would make it easier to tune and maintain.

For example, near your other constants:

const (
	ListAttemptLimit       uint64        = 25
	rateLimitSleepDuration time.Duration = 100 * time.Millisecond
)

Then use it on lines 173 and 234:

-				time.Sleep(100 * time.Millisecond)
+				time.Sleep(rateLimitSleepDuration)

294-294: Consider conditional payload assignment based on expansion.

When params.Expand.Payload is false (line 271), the Svix response won't populate msgOut.Payload with meaningful content, but you're still assigning &msgOut.Payload on line 294. While this isn't breaking, it might be clearer to conditionally set it to nil when not expanded, matching the semantic intent of the webhook.Message.Payload field documentation ("stores the message payload if it was requested").

Something like:

+		var payload *webhook.Payload
+		if params.Expand.Payload {
+			payload = &msgOut.Payload
+		}
+
 		return &webhook.Message{
 			Namespace:        params.Namespace,
 			ID:               msgOut.Id,
 			EventID:          lo.FromPtr(msgOut.EventId),
 			EventType:        msgOut.EventType,
 			Channels:         msgOut.Channels,
-			Payload:          &msgOut.Payload,
+			Payload:          payload,
 			DeliveryStatuses: lo.ToPtr(statuses),
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f342f6 and b83bb6f.

📒 Files selected for processing (1)
  • openmeter/notification/webhook/svix/message.go (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/notification/webhook/svix/message.go
🧠 Learnings (1)
📚 Learning: 2025-11-07T12:29:09.025Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3581
File: openmeter/notification/eventhandler/webhook.go:391-396
Timestamp: 2025-11-07T12:29:09.025Z
Learning: In the OpenMeter notification system (specifically in openmeter/notification/eventhandler/webhook.go), webhook channels are equivalent to notification rules. When sending webhook messages, the rule ID (event.Rule.ID) should be used as the channel identifier in the Channels field of SendMessageInput.

Applied to files:

  • openmeter/notification/webhook/svix/message.go
🧬 Code graph analysis (1)
openmeter/notification/webhook/svix/message.go (6)
openmeter/notification/webhook/svix/annotations.go (5)
  • AnnotationMessageEventID (5-5)
  • AnnotationEventType (7-7)
  • AnnotationApplicationUID (10-10)
  • AnnotationMessageID (4-4)
  • AnnotationMessageAttemptsCount (6-6)
openmeter/notification/webhook/handler.go (5)
  • EventType (261-268)
  • Message (187-204)
  • Payload (178-178)
  • MessageDeliveryStatus (180-185)
  • GetMessageInput (242-249)
openmeter/notification/webhook/svix/internal/error.go (1)
  • WrapSvixError (68-110)
openmeter/notification/webhook/svix/svix.go (2)
  • Error (28-28)
  • New (86-121)
pkg/framework/tracex/tracex.go (1)
  • Start (32-39)
openmeter/notification/deliverystatus.go (6)
  • EventDeliveryAttempt (56-60)
  • EventDeliveryAttemptResponse (49-54)
  • EventDeliveryStatusStateFailed (15-15)
  • EventDeliveryStatusStateSending (16-16)
  • EventDeliveryStatusState (25-25)
  • EventDeliveryStatusStateSuccess (14-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (2)
openmeter/notification/webhook/svix/message.go (2)

24-99: SendMessage implementation looks solid!

The optimization to set WithContent: false (line 63) makes sense since you're returning the input payload you already have (&params.Payload on line 87), avoiding unnecessary data transfer from Svix. The new annotations structure (lines 89-94) correctly tracks the message metadata using the defined annotation keys.


308-321: Helper function looks good!

The status mapping logic is clean and straightforward. Returning an empty string for unknown statuses is a reasonable sentinel value, and the callers (lines 140 and 212) correctly check for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants