-
-
Notifications
You must be signed in to change notification settings - Fork 292
feat: add webhooks #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add webhooks #334
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds end-to-end webhook support: new Prisma models and migration for Webhook and WebhookCall (enums, indexes, FKs); server-side WebhookService and queue/worker (enqueue, delivery, retry, signing, auto-disable); TRPC webhook router and API surface; emission points in contact, domain, and SES hook flows; plan limit integration and a webhook limit checker; cleanup job and queue constants; dashboard UI for listing, creating, editing, testing, and inspecting webhook calls; SDK webhook verification and event construction; new packages/lib and packages/sdk additions and related build/config updates. Possibly related PRs
Suggested labels
Pre-merge checks❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8 issues found across 55 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx">
<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx:220">
P2: Missing error state handling: The component doesn't check `webhookQuery.isError`. If the API call fails, users will see "Webhook not found" instead of an appropriate error message, which could be misleading.</violation>
</file>
<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx">
<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx:118">
P2: Using `call.responseTimeMs &&` will hide the duration when it's 0ms, which is a valid response time. Use a null check instead.</violation>
</file>
<file name="packages/sdk/package.json">
<violation number="1" location="packages/sdk/package.json:28">
P0: Adding a private workspace package as a runtime dependency to a published SDK will break npm installations. `@usesend/lib` is marked `private: true` and won't be published to npm, causing dependency resolution failures for SDK users.
Consider either:
1. Moving `@usesend/lib` to `devDependencies` and bundling its code at build time (add `--noExternal @usesend/lib` to the tsup command)
2. Making `@usesend/lib` a public package that's also published to npm</violation>
</file>
<file name="apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx">
<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx:90">
P2: Incorrect `colSpan` value. The table has 5 columns but `colSpan={6}` is used. This will cause layout inconsistency.</violation>
</file>
<file name="packages/sdk/README.md">
<violation number="1" location="packages/sdk/README.md:131">
P2: The Next.js example is missing error handling for `constructEvent()`. Unlike the Express example below which shows proper try/catch, this code will throw an unhandled error when signature verification fails, resulting in a 500 instead of a 400 response. Consider wrapping in try/catch for consistency:
```ts
export async function POST(request: Request) {
try {
const rawBody = await request.text();
const event = constructEvent({
secret: process.env.USESEND_WEBHOOK_SECRET!,
headers: request.headers,
rawBody,
});
// handle event...
return new Response("ok");
} catch (error) {
return new Response((error as Error).message, { status: 400 });
}
}
```</violation>
</file>
<file name="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx">
<violation number="1" location="apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx:43">
P2: `navigator.clipboard.writeText` returns a Promise that can reject. Add error handling to avoid showing success toast when copy fails.</violation>
</file>
<file name="apps/web/src/components/code-display.tsx">
<violation number="1" location="apps/web/src/components/code-display.tsx:63">
P2: The `setTimeout` in `handleCopy` is not cleaned up on unmount, which can cause a React state update on an unmounted component. Consider using a ref to track the timeout and clear it in a cleanup function, or use a custom hook for this pattern.</violation>
</file>
<file name="apps/web/src/server/service/webhook-service.ts">
<violation number="1" location="apps/web/src/server/service/webhook-service.ts:783">
P2: Stream reader not canceled when truncating. When breaking early due to exceeding the response text limit, call `reader.cancel()` to properly release the underlying connection resources.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Nitpick comments (20)
packages/lib/package.json (1)
10-12: Consider adding a build script if distributable artifacts are needed.The package has no build script despite the
tsconfig.jsonbeing configured to output to"dist". If this package is meant to be consumed as built artifacts rather than source files, you should add a build script.Proposed build script addition
"scripts": { + "build": "tsc", "lint": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix" },packages/sdk/README.md (1)
129-142: Suggest adding error handling to the Next.js example.The Next.js App Route example currently lacks error handling, while the Express example (lines 169-183) includes a try-catch block. For webhook verification, explicit error handling is recommended because:
- Verification failures should return
400(Bad Request) rather than500(Internal Server Error)- Webhook senders use HTTP status codes to determine retry behavior
- Consistency between framework examples aids developer understanding
Consider adding a try-catch wrapper similar to the Express example to demonstrate proper error handling and return appropriate HTTP status codes for verification failures.
Suggested enhancement
// In a Next.js App Route export async function POST(request: Request) { - const rawBody = await request.text(); // important: raw body, not parsed JSON - const event = constructEvent({ - secret: process.env.USESEND_WEBHOOK_SECRET!, - headers: request.headers, - rawBody, - }); - - if (event.type === "email.delivered") { - // event.data is strongly typed here + try { + const rawBody = await request.text(); + const event = constructEvent({ + secret: process.env.USESEND_WEBHOOK_SECRET!, + headers: request.headers, + rawBody, + }); + + if (event.type === "email.delivered") { + // event.data is strongly typed here + } + + return new Response("ok"); + } catch (error) { + return new Response((error as Error).message, { status: 400 }); } - - return new Response("ok"); }apps/web/src/components/code-display.tsx (1)
44-49: Consider displaying raw code as fallback on highlighting failure.When
codeToHtmlfails,htmlremains empty andisLoadingbecomesfalse, resulting in an empty container. Showing the raw code as a fallback would provide a better user experience.🔎 Proposed fix
} catch (error) { console.error("Failed to highlight code:", error); if (isMounted) { + setHtml(""); // Ensure html is empty to trigger fallback setIsLoading(false); } }Then in the render section (around line 94):
+ if (!html) { + // Fallback: show raw code without highlighting + return ( + <div className="relative rounded-lg overflow-hidden border bg-muted/50"> + <Button + size="icon" + variant="ghost" + onClick={handleCopy} + className="absolute top-2 right-2 h-8 w-8 z-10" + > + {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} + </Button> + <pre + className={`text-xs font-mono p-4 overflow-auto ${className}`} + style={{ maxHeight }} + > + <code className="p-2">{code}</code> + </pre> + </div> + ); + } + return (apps/web/src/app/(dashboard)/webhooks/page.tsx (1)
4-5: Consider using the~/alias for consistency.Per coding guidelines, imports in
apps/webshould use the~/alias instead of relative paths.🔎 Suggested refactor
-import { AddWebhook } from "./add-webhook"; -import { WebhookList } from "./webhook-list"; +import { AddWebhook } from "~/app/(dashboard)/webhooks/add-webhook"; +import { WebhookList } from "~/app/(dashboard)/webhooks/webhook-list";Based on coding guidelines: "Use alias
~/for src imports in apps/web"apps/web/src/server/jobs/webhook-cleanup-job.ts (1)
14-34: Consider batching large deletions for better database performance.The
deleteManyoperation at line 18 could potentially delete millions of records in a single transaction if webhook calls have accumulated over time. This might cause:
- Long-running transactions that lock the
webhookCalltable- Memory pressure from processing large result sets
- Potential timeouts or connection issues
🔎 Suggested batched deletion approach
const worker = new Worker( WEBHOOK_CLEANUP_QUEUE, async () => { const cutoff = subDays(new Date(), WEBHOOK_RETENTION_DAYS); - const result = await db.webhookCall.deleteMany({ - where: { - createdAt: { - lt: cutoff, - }, - }, - }); + + const BATCH_SIZE = 1000; + let totalDeleted = 0; + let hasMore = true; + + while (hasMore) { + const result = await db.webhookCall.deleteMany({ + where: { + id: { + in: ( + await db.webhookCall.findMany({ + where: { createdAt: { lt: cutoff } }, + select: { id: true }, + take: BATCH_SIZE, + }) + ).map((r) => r.id), + }, + }, + }); + + totalDeleted += result.count; + hasMore = result.count === BATCH_SIZE; + } logger.info( - { deleted: result.count, cutoff: cutoff.toISOString() }, + { deleted: totalDeleted, cutoff: cutoff.toISOString() }, "[WebhookCleanupJob]: Deleted old webhook calls", ); },apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (2)
25-40: Optimize the filtering and counting logic.The current implementation:
- Filters calls to the last 7 days (line 26-28)
- Iterates three separate times to count different statuses (lines 30-40)
This can be optimized by combining the filtering and counting into a single pass, and by creating the
sevenDaysAgodate object once instead of creating a new Date object for each call during filtering.🔎 Proposed optimization
const calls = callsQuery.data?.items ?? []; - const last7DaysCalls = calls.filter( - (call) => new Date(call.createdAt) >= sevenDaysAgo, - ); - - const deliveredCount = last7DaysCalls.filter( - (c) => c.status === WebhookCallStatus.DELIVERED, - ).length; - const failedCount = last7DaysCalls.filter( - (c) => c.status === WebhookCallStatus.FAILED, - ).length; - const pendingCount = last7DaysCalls.filter( - (c) => - c.status === WebhookCallStatus.PENDING || - c.status === WebhookCallStatus.IN_PROGRESS, - ).length; + + const sevenDaysAgoTime = sevenDaysAgo.getTime(); + let deliveredCount = 0; + let failedCount = 0; + let pendingCount = 0; + let last7DaysCalls = []; + + for (const call of calls) { + if (new Date(call.createdAt).getTime() >= sevenDaysAgoTime) { + last7DaysCalls.push(call); + if (call.status === WebhookCallStatus.DELIVERED) { + deliveredCount++; + } else if (call.status === WebhookCallStatus.FAILED) { + failedCount++; + } else if ( + call.status === WebhookCallStatus.PENDING || + call.status === WebhookCallStatus.IN_PROGRESS + ) { + pendingCount++; + } + } + }
20-23: Add server-side date filtering to thelistCallsTRPC endpoint.The component fetches 50 webhook calls and filters them client-side to the last 7 days. The
api.webhook.listCallsendpoint does not currently support date range parameters. AddingcreatedAfterandcreatedBeforeparameters to the TRPC input schema andWebhookService.listWebhookCallsmethod would allow filtering on the server side, reducing unnecessary data transfer and improving performance.apps/web/src/server/service/ses-hook-parser.ts (1)
397-428: Make the default case more explicit.The
defaultcase returns"email.queued"for any unhandled status, includingSCHEDULED. WhileSCHEDULEDemails likely don't reach this code path via SES hooks, the implicit fallback could mask future issues when new statuses are added.🔎 Proposed fix to handle SCHEDULED explicitly
function emailStatusToEvent(status: EmailStatus): EmailWebhookEventType { switch (status) { case EmailStatus.QUEUED: return "email.queued"; case EmailStatus.SENT: return "email.sent"; case EmailStatus.DELIVERY_DELAYED: return "email.delivery_delayed"; case EmailStatus.DELIVERED: return "email.delivered"; case EmailStatus.BOUNCED: return "email.bounced"; case EmailStatus.REJECTED: return "email.rejected"; case EmailStatus.RENDERING_FAILURE: return "email.rendering_failure"; case EmailStatus.COMPLAINED: return "email.complained"; case EmailStatus.FAILED: return "email.failed"; case EmailStatus.CANCELLED: return "email.cancelled"; case EmailStatus.SUPPRESSED: return "email.suppressed"; case EmailStatus.OPENED: return "email.opened"; case EmailStatus.CLICKED: return "email.clicked"; + case EmailStatus.SCHEDULED: + // SCHEDULED emails should not trigger webhooks via SES + logger.warn({ status }, "Unexpected SCHEDULED status in SES hook"); + return "email.queued"; default: - return "email.queued"; + logger.error({ status }, "Unknown email status in emailStatusToEvent"); + return "email.queued"; } }apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
144-152: Consider rendering EditWebhookDialog unconditionally.The current pattern conditionally mounts/unmounts the
EditWebhookDialogcomponent for each webhook. This causes the component to reinitialize on every open, which is less efficient. Since the dialog is already controlled via theopenprop, you could render it once outside the map loop.🔎 Proposed refactor to improve efficiency
Move the dialog outside the map loop and track the webhook being edited:
export function WebhookList() { const webhooksQuery = api.webhook.list.useQuery(); const setStatusMutation = api.webhook.setStatus.useMutation(); const utils = api.useUtils(); const router = useRouter(); - const [editingId, setEditingId] = useState<string | null>(null); + const [editingWebhook, setEditingWebhook] = useState<Webhook | null>(null); const webhooks = webhooksQuery.data ?? []; // ... handlers ... return ( <div className="mt-10"> <div className="rounded-xl border shadow"> <Table> {/* ... table header ... */} <TableBody> {/* ... loading/empty states ... */} {webhooks.map((webhook) => ( <TableRow key={webhook.id} className="cursor-pointer hover:bg-muted/50" onClick={() => router.push(`/webhooks/${webhook.id}`)} > {/* ... table cells ... */} <TableCell className="text-right"> <div className="flex items-center justify-end" onClick={(e) => e.stopPropagation()} > <WebhookActions webhook={webhook} - onEdit={() => setEditingId(webhook.id)} + onEdit={() => setEditingWebhook(webhook)} onToggleStatus={() => handleToggleStatus(webhook.id, webhook.status) } isToggling={setStatusMutation.isPending} /> </div> - {editingId === webhook.id ? ( - <EditWebhookDialog - webhook={webhook} - open={editingId === webhook.id} - onOpenChange={(open) => - setEditingId(open ? webhook.id : null) - } - /> - ) : null} </TableCell> </TableRow> ))} </TableBody> </Table> </div> + {editingWebhook && ( + <EditWebhookDialog + webhook={editingWebhook} + open={!!editingWebhook} + onOpenChange={(open) => setEditingWebhook(open ? editingWebhook : null)} + /> + )} </div> ); }apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (2)
145-160: UnusedformStatevariable in URL field render.The
formStateis destructured but not used in this field's render function. Consider removing it to clean up the code.Proposed fix
<FormField control={form.control} name="url" - render={({ field, formState }) => ( + render={({ field }) => ( <FormItem> <FormLabel>Endpoint URL</FormLabel>
181-240: Consider extracting shared event selection logic.The
handleToggleAll,handleToggleGroup,handleToggleEvent, andisGroupFullySelectedfunctions are nearly identical to those inadd-webhook.tsx. Consider extracting these into a shared custom hook (e.g.,useWebhookEventSelection) to reduce duplication.apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)
151-166: UnusedformStatevariable in URL field render.Similar to the edit dialog,
formStateis destructured but not used in this field's render function.Proposed fix
<FormField control={form.control} name="url" - render={({ field, formState }) => ( + render={({ field }) => ( <FormItem>apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)
22-23: Remove unused import.
CircleEllipsisis imported but not used in the component.Proposed fix
Edit3, Key, MoreVertical, Pause, Play, TestTube, - CircleEllipsis, } from "lucide-react";
179-195: UseWebhookStatusenum for type safety.The status comparison and mutation use string literals (
"ACTIVE","PAUSED") instead of theWebhookStatusenum. This could cause runtime issues if enum values change.Proposed fix
+import { WebhookStatus } from "@prisma/client"; const handleToggleStatus = (currentStatus: string) => { - const newStatus = currentStatus === "ACTIVE" ? "PAUSED" : "ACTIVE"; + const newStatus = currentStatus === WebhookStatus.ACTIVE + ? WebhookStatus.PAUSED + : WebhookStatus.ACTIVE; setStatusMutation.mutate( { id: webhookId, status: newStatus },Also update the function signature to use the enum type:
- const handleToggleStatus = (currentStatus: string) => { + const handleToggleStatus = (currentStatus: WebhookStatus) => {packages/sdk/src/webhooks.ts (2)
2-7: Consider using a proper package subpath export instead of importing fromsrc/.The import path
@usesend/lib/src/webhook/webhook-eventsreaches into the package's internalsrc/directory. This pattern can be fragile if the package structure changes and doesn't follow conventional package subpath exports.Consider configuring a proper subpath export in
@usesend/lib'spackage.json(e.g.,@usesend/lib/webhook) to make this import more stable and explicit as part of the public API.
180-187: Document the negative tolerance behavior.The condition
toleranceMs >= 0means passing a negativetolerancevalue disables the timestamp check entirely. While this may be intentional for testing scenarios, it's not documented in the JSDoc comments and could be misused.Consider either documenting this behavior in the options type or removing the ability to disable tolerance checks via negative values.
apps/web/src/server/service/webhook-service.ts (3)
43-83: Consider lazy initialization for Queue and Worker.The static initialization creates Redis connections immediately when the module is imported. This could cause issues in serverless/edge environments or during build time if the Redis connection isn't available.
Consider using a lazy initialization pattern (e.g., singleton getter) to defer connection until the first actual use.
235-240: Consider providing a user-friendly error message.The error message uses
reason ?? "Webhook limit reached"wherereasonis aLimitReasonenum value. Depending on howLimitReasonis defined, the message might display an internal enum name (e.g., "WEBHOOK") rather than a user-friendly message.Consider mapping the
LimitReasonto a descriptive message string.
766-768: Consider extracting the skip threshold to a named constant.The
WEBHOOK_RESPONSE_TEXT_LIMIT * 2is a magic number for determining when to skip reading the response entirely. Consider extracting this to a named constant for clarity (e.g.,WEBHOOK_RESPONSE_SKIP_THRESHOLD).packages/lib/src/webhook/webhook-events.ts (1)
151-165: Consider adding extended payload foremail.complained.Looking at the payload map,
email.complainedusesEmailBasePayloadwithout complaint-specific details. If the complaint data (e.g., feedback type, complaint reason) is available from the email provider, consider adding anEmailComplainedPayloadtype similar toEmailBouncedPayload.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (54)
apps/web/package.jsonapps/web/prisma/migrations/20251122195838_add_webhook/migration.sqlapps/web/prisma/schema.prismaapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/components/AppSideBar.tsxapps/web/src/components/code-display.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/api/root.tsapps/web/src/server/api/routers/contacts.tsapps/web/src/server/api/routers/email.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/api/routers/webhook.tsapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/server/public-api/api/contacts/add-contact.tsapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/public-api/api/contacts/update-contact.tsapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/contact-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/service/limit-service.tsapps/web/src/server/service/ses-hook-parser.tsapps/web/src/server/service/webhook-service.tsapps/web/tailwind.config.tsdocker/dev/compose.ymlpackages/lib/.eslintrc.cjspackages/lib/index.tspackages/lib/package.jsonpackages/lib/src/constants/ses-errors.tspackages/lib/src/index.tspackages/lib/src/webhook/webhook-events.tspackages/lib/tsconfig.jsonpackages/lib/tsconfig.lint.jsonpackages/sdk/README.mdpackages/sdk/index.tspackages/sdk/package.jsonpackages/sdk/src/usesend.tspackages/sdk/src/webhooks.tspackages/ui/src/dropdown-menu.tsxpackages/ui/styles/globals.css
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{tsx,ts,jsx,js}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports and ensure proper naming of key components in React/NextJS code
Files:
apps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/contacts.tsapps/web/src/components/AppSideBar.tsxapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/service/limit-service.tsapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/public-api/api/contacts/add-contact.tspackages/sdk/src/usesend.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/server/public-api/api/contacts/update-contact.tspackages/sdk/index.tsapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/components/code-display.tsxapps/web/src/server/api/routers/webhook.tspackages/lib/src/index.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/api/root.tsapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/server/api/routers/email.tsapps/web/tailwind.config.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/service/ses-hook-parser.tspackages/sdk/src/webhooks.tspackages/lib/src/webhook/webhook-events.tsapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/server/service/webhook-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/server/service/contact-service.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs
Files:
apps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/contacts.tsapps/web/src/components/AppSideBar.tsxapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/service/limit-service.tsapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/public-api/api/contacts/add-contact.tspackages/sdk/src/usesend.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/server/public-api/api/contacts/update-contact.tspackages/sdk/index.tsapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/components/code-display.tsxapps/web/src/server/api/routers/webhook.tspackages/lib/src/index.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/api/root.tsapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/server/api/routers/email.tsapps/web/tailwind.config.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/service/ses-hook-parser.tspackages/sdk/src/webhooks.tspackages/lib/src/webhook/webhook-events.tsapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/server/service/webhook-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/server/service/contact-service.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
React components must use PascalCase naming convention (e.g., AppSideBar.tsx)
Files:
apps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/components/AppSideBar.tsxapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/components/code-display.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxpackages/ui/src/dropdown-menu.tsx
apps/web/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use alias
~/for src imports in apps/web (e.g.,import { x } from "~/utils/x")
Files:
apps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/contacts.tsapps/web/src/components/AppSideBar.tsxapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/service/limit-service.tsapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/public-api/api/contacts/add-contact.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/server/public-api/api/contacts/update-contact.tsapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/components/code-display.tsxapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/api/root.tsapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/server/api/routers/email.tsapps/web/tailwind.config.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/server/service/webhook-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxapps/web/src/server/service/contact-service.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web
Files:
apps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/contacts.tsapps/web/src/components/AppSideBar.tsxapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/service/limit-service.tsapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/public-api/api/contacts/add-contact.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/server/public-api/api/contacts/update-contact.tsapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/components/code-display.tsxapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/api/root.tsapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/server/api/routers/email.tsapps/web/tailwind.config.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/server/service/webhook-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxapps/web/src/server/service/contact-service.ts
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files
Files:
apps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/contacts.tsapps/web/src/components/AppSideBar.tsxapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/service/limit-service.tsapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/public-api/api/contacts/add-contact.tspackages/sdk/src/usesend.tspackages/sdk/README.mdapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/server/public-api/api/contacts/update-contact.tspackages/sdk/index.tsapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/components/code-display.tsxapps/web/src/server/api/routers/webhook.tspackages/lib/src/index.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/api/root.tsapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/server/api/routers/email.tsapps/web/tailwind.config.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/service/ses-hook-parser.tspackages/sdk/src/webhooks.tspackages/lib/src/webhook/webhook-events.tsapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/server/service/webhook-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/server/service/contact-service.ts
🧠 Learnings (7)
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run ESLint via usesend/eslint-config and ensure no warnings remain before submitting PRs
Applied to files:
packages/lib/package.jsonpackages/lib/.eslintrc.cjspackages/lib/tsconfig.jsonapps/web/tailwind.config.tspackages/lib/tsconfig.lint.json
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Applied to files:
packages/lib/package.jsonpackages/lib/.eslintrc.cjspackages/lib/tsconfig.jsonapps/web/tailwind.config.tspackages/lib/tsconfig.lint.json
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code
Applied to files:
apps/web/src/components/code-display.tsxapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Applied to files:
apps/web/src/server/api/routers/webhook.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx,js,jsx} : Use alias `~/` for src imports in apps/web (e.g., `import { x } from "~/utils/x"`)
Applied to files:
apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,md} : Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files
Applied to files:
apps/web/tailwind.config.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Use Prisma for database access in apps/web
Applied to files:
apps/web/prisma/schema.prisma
🧬 Code graph analysis (21)
apps/web/src/app/(dashboard)/webhooks/page.tsx (2)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)
AddWebhook(66-333)apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
WebhookList(29-162)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (4)
packages/ui/src/toaster.tsx (1)
toast(31-31)packages/ui/src/badge.tsx (1)
Badge(36-36)apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
WebhookStatusBadge(1-23)packages/ui/src/button.tsx (1)
Button(80-80)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
apps/web/src/components/DeleteResource.tsx (1)
DeleteResource(63-199)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (5)
packages/ui/src/select.tsx (5)
Select(150-150)SelectTrigger(153-153)SelectValue(152-152)SelectContent(154-154)SelectItem(156-156)packages/ui/src/table.tsx (5)
Table(109-109)TableRow(114-114)TableHead(113-113)TableBody(111-111)TableCell(115-115)packages/ui/src/spinner.tsx (1)
Spinner(4-51)apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
WebhookCallStatusBadge(3-41)packages/ui/src/button.tsx (1)
Button(80-80)
apps/web/src/server/api/routers/contacts.ts (1)
apps/web/src/server/api/trpc.ts (1)
contactBookProcedure(204-222)
apps/web/src/server/public-api/api/contacts/delete-contact.ts (1)
apps/web/src/server/service/contact-service.ts (1)
deleteContact(97-107)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (4)
packages/lib/src/webhook/webhook-events.ts (5)
WebhookEvents(36-40)WebhookEventType(42-42)ContactEvents(1-5)DomainEvents(9-14)EmailEvents(18-32)packages/sdk/index.ts (1)
WebhookEventType(16-16)packages/sdk/src/webhooks.ts (1)
WebhookEventType(278-278)apps/web/src/store/upgradeModalStore.ts (1)
useUpgradeModalStore(13-20)
packages/sdk/src/usesend.ts (2)
packages/sdk/index.ts (1)
Webhooks(5-5)packages/sdk/src/webhooks.ts (1)
Webhooks(45-199)
apps/web/src/server/public-api/api/contacts/update-contact.ts (1)
apps/web/src/server/service/contact-service.ts (1)
updateContact(80-95)
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (3)
apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
WebhookStatusBadge(1-23)apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)
EditWebhookDialog(66-323)apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
DeleteWebhook(11-61)
apps/web/src/server/jobs/webhook-cleanup-job.ts (4)
apps/web/src/server/queue/queue-constants.ts (2)
WEBHOOK_CLEANUP_QUEUE(7-7)DEFAULT_QUEUE_OPTIONS(9-14)apps/web/src/server/redis.ts (1)
getRedis(6-13)apps/web/src/server/db.ts (1)
db(20-20)apps/web/src/server/logger/log.ts (1)
logger(31-63)
apps/web/src/server/api/routers/webhook.ts (2)
apps/web/src/server/api/trpc.ts (2)
createTRPCRouter(82-82)teamProcedure(127-152)apps/web/src/server/service/webhook-service.ts (1)
WebhookService(85-385)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/webhook.ts (1)
webhookRouter(9-135)
apps/web/src/server/service/contact-queue-service.ts (1)
apps/web/src/server/service/contact-service.ts (1)
addOrUpdateContact(19-78)
apps/web/src/server/service/domain-service.ts (2)
packages/lib/src/webhook/webhook-events.ts (2)
DomainPayload(86-100)DomainWebhookEventType(16-16)apps/web/src/server/service/webhook-service.ts (1)
WebhookService(85-385)
apps/web/src/server/api/routers/limits.ts (1)
apps/web/src/server/service/limit-service.ts (1)
LimitService(19-280)
apps/web/src/server/service/ses-hook-parser.ts (3)
apps/web/src/server/service/webhook-service.ts (1)
WebhookService(85-385)packages/lib/src/webhook/webhook-events.ts (3)
EmailEventPayloadMap(151-165)EmailStatus(44-58)EmailBasePayload(60-72)apps/web/src/types/aws-types.ts (4)
SesEvent(143-156)SesEventDataKey(132-141)SesBounce(30-50)SesClick(91-97)
packages/sdk/src/webhooks.ts (2)
packages/sdk/index.ts (7)
WebhookVerificationError(6-6)WEBHOOK_SIGNATURE_HEADER(9-9)WEBHOOK_TIMESTAMP_HEADER(10-10)WEBHOOK_EVENT_HEADER(7-7)WEBHOOK_CALL_HEADER(8-8)Webhooks(5-5)WebhookEventData(14-14)packages/lib/src/webhook/webhook-events.ts (1)
WebhookEventData(194-196)
apps/web/src/server/service/webhook-service.ts (6)
apps/web/src/server/queue/bullmq-context.ts (2)
TeamJob(5-5)createWorkerHandler(10-24)packages/lib/src/webhook/webhook-events.ts (2)
WebhookEventType(42-42)WebhookPayloadData(184-185)apps/web/src/server/queue/queue-constants.ts (2)
WEBHOOK_DISPATCH_QUEUE(6-6)DEFAULT_QUEUE_OPTIONS(9-14)apps/web/src/server/redis.ts (1)
getRedis(6-13)apps/web/src/server/db.ts (1)
db(20-20)apps/web/src/server/public-api/api-error.ts (1)
UnsendApiError(62-75)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx (1)
packages/ui/src/code-block.tsx (1)
CodeBlock(13-36)
apps/web/src/server/service/contact-service.ts (4)
apps/web/src/server/db.ts (1)
db(20-20)packages/lib/src/webhook/webhook-events.ts (2)
ContactWebhookEventType(7-7)ContactPayload(74-84)apps/web/src/server/logger/log.ts (1)
logger(31-63)apps/web/src/server/service/webhook-service.ts (1)
WebhookService(85-385)
🪛 ast-grep (0.40.3)
apps/web/src/components/code-display.tsx
[warning] 106-106: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
apps/web/src/components/code-display.tsx
[error] 107-107: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
⏰ 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). (2)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: Cloudflare Pages
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
Show resolved
Hide resolved
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
Show resolved
Hide resolved
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
Outdated
Show resolved
Hide resolved
Deploying usesend with
|
| Latest commit: |
0116bb8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8e47a651.usesend.pages.dev |
| Branch Preview URL: | https://km-2025-11-17-webhook.usesend.pages.dev |
stuff webhook call changes more stuff stuff stuff more webhook stuff add pnpm more ui stuff stuff sdk stuff stuff webhook ui stuff stuff stuff fix: address code review issues in webhooks and SDK - Fix colSpan values in webhook tables to match actual column counts - Remove unused Card component imports from webhook-info.tsx - Use WebhookStatus enum instead of string literals in webhook-status-badge - Fix JSDoc example in usesend.ts to use correct constructEvent signature - Cancel stream reader when truncating response in webhook-service - Add error handling to Next.js webhook example in SDK README - Move @usesend/lib to devDependencies and bundle at build time - Use null check for responseTimeMs to handle 0ms response times other changes update lock
044e408 to
0116bb8
Compare
There was a problem hiding this 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 (2)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)
113-113: LGTM! ThecolSpanfix has been applied.The previous issue with
colSpan={5}has been corrected tocolSpan={3}, which now matches the actual number of columns in the table.Also applies to: 122-122
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
31-31: Remove unused mutation import.The
testWebhookmutation is imported but never used in the component. This appears to be leftover from when thehandleTestfunction was removed.🔎 Proposed fix
export function WebhookList() { const webhooksQuery = api.webhook.list.useQuery(); - const testWebhook = api.webhook.test.useMutation(); const setStatusMutation = api.webhook.setStatus.useMutation();
🧹 Nitpick comments (3)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (2)
99-157: Consider using a single table for better column alignment and accessibility.The current implementation splits the table into two separate
<Table>components—one for the header and one for the body—to achieve a sticky header with scrollable content. While this works functionally, it can lead to:
- Column width misalignment between header and body (not guaranteed to stay in sync)
- Semantic HTML issues (header and body should be in the same table element)
- Accessibility concerns for screen readers expecting a unified table structure
🔎 Recommended approach using a single table
Consider using a single
<Table>with CSS to make the header sticky:- <div className="flex-1 overflow-hidden rounded-xl border shadow flex flex-col"> - <Table> - <TableHeader className="sticky top-0 z-10"> - <TableRow className="bg-muted dark:bg-muted/70"> - <TableHead className="h-9 rounded-tl-xl">Status</TableHead> - <TableHead className="h-9">Event Type</TableHead> - <TableHead className="h-9 rounded-tr-xl">Time</TableHead> - </TableRow> - </TableHeader> - </Table> - <div className="flex-1 overflow-auto no-scrollbar"> - <Table> - <TableBody> + <div className="flex-1 overflow-auto no-scrollbar rounded-xl border shadow"> + <Table> + <TableHeader className="sticky top-0 z-10 bg-muted dark:bg-muted/70"> + <TableRow> + <TableHead className="h-9">Status</TableHead> + <TableHead className="h-9">Event Type</TableHead> + <TableHead className="h-9">Time</TableHead> + </TableRow> + </TableHeader> + <TableBody> {/* ... table body content ... */} - </TableBody> - </Table> - </div> + </TableBody> + </Table> </div>This approach maintains column alignment and semantic correctness. If the rounded corners on the header are important, they can be achieved with additional CSS targeting the first/last
TableHeadelements.
111-119: Consider adding ARIA labels to the loading spinner for screen reader users.While the spinner is visually clear, screen reader users would benefit from an explicit loading announcement.
🔎 Suggested enhancement
<TableCell colSpan={3} className="py-4 text-center"> + <div role="status" aria-live="polite" aria-label="Loading webhook calls"> <Spinner className="mx-auto h-6 w-6" innerSvgClass="stroke-primary" /> + </div> </TableCell>Alternatively, add
aria-busy="true"to the table wrapper during loading and include visually hidden text.references/webhook-architecture.md (1)
7-129: Consider adding language identifiers to fenced code blocks.The architecture diagram and state machine use fenced code blocks without language identifiers. While markdownlint flags this, these ASCII art diagrams don't have appropriate language tags. You could use
textorasciito satisfy the linter while maintaining readability.🔎 Example fix for the architecture diagram
-``` +```text ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ EVENT SOURCES │
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
apps/web/prisma/migrations/20251122195838_add_webhook/migration.sqlapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/server/service/webhook-service.tspackages/sdk/README.mdpackages/sdk/package.jsonpackages/sdk/src/usesend.tsreferences/webhook-architecture.md
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
- packages/sdk/src/usesend.ts
- packages/sdk/package.json
- apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx
- apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files
Files:
packages/sdk/README.mdapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/service/webhook-service.tsreferences/webhook-architecture.md
**/*.{tsx,ts,jsx,js}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports and ensure proper naming of key components in React/NextJS code
Files:
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/service/webhook-service.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs
Files:
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/service/webhook-service.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
React components must use PascalCase naming convention (e.g., AppSideBar.tsx)
Files:
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
apps/web/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use alias
~/for src imports in apps/web (e.g.,import { x } from "~/utils/x")
Files:
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/service/webhook-service.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web
Files:
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/server/service/webhook-service.ts
🧬 Code graph analysis (1)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (2)
packages/ui/src/spinner.tsx (1)
Spinner(4-51)apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
WebhookCallStatusBadge(3-41)
🪛 markdownlint-cli2 (0.18.1)
references/webhook-architecture.md
7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
133-133: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
173-173: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
320-320: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
364-364: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ 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: Cloudflare Pages
🔇 Additional comments (9)
packages/sdk/README.md (1)
118-193: LGTM! Webhook documentation is comprehensive and accurate.The webhook verification examples now include proper error handling in both Next.js and Express, headers are well-documented, and the typed event usage is clearly demonstrated.
apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
39-212: LGTM! Component logic is well-structured.The status toggle handler, table rendering with correct
colSpan={5}, row navigation, and actions menu are all implemented correctly. Good use of event propagation control to prevent row clicks when interacting with the actions popover.apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql (1)
1-66: LGTM! Database migration is well-structured.The migration correctly:
- Defines comprehensive status enums for webhooks and calls
- Creates properly indexed tables with appropriate foreign key constraints
- Uses CASCADE for cleanup and SET NULL for optional references
- Includes composite indexes optimized for common query patterns (teamId + webhookId + status)
apps/web/src/server/service/webhook-service.ts (6)
42-82: LGTM! Queue service initialization is clean.The static initialization pattern for BullMQ queue and worker is appropriate, with proper error handling and observability via logging. Configuration constants are well-defined at the module level.
84-133: LGTM! Event emission logic is robust.The
emitmethod correctly:
- Filters for ACTIVE webhooks with matching event subscriptions (including "all events" via empty array)
- Persists WebhookCall records before enqueueing for reliability
- Handles payload serialization safely
135-390: LGTM! Service methods follow solid patterns.The retry, test, and CRUD methods demonstrate:
- Consistent error handling with appropriate UnsendApiError codes
- Proper team-based scoping for multi-tenancy security
- Limit enforcement on webhook creation
- Safe status transitions (resetting failure counters on activation)
409-574: LGTM! Webhook delivery worker is well-implemented.The
processWebhookCallfunction handles the complete delivery lifecycle correctly:
- Discards calls for inactive webhooks
- Acquires per-webhook locks to ensure ordered delivery
- Handles both success and failure paths with proper status updates
- Implements exponential backoff with attempt limits
- Auto-disables webhooks after consecutive failure threshold
- Captures response metadata for observability
393-407: LGTM! Helper functions are correctly implemented.The utility functions demonstrate best practices:
stringifyPayloadwith safe error handling and fallbackacquireLockusing Redis SET NX PX patternreleaseLockwith Lua script for atomic check-and-deletecomputeBackoffwith 30% jitter to prevent thundering herdbuildPayloadwith safe JSON parsingAlso applies to: 576-648
650-821: LGTM! HTTP delivery and signing implementation is robust.The webhook HTTP POST logic is well-implemented:
- AbortController properly handles request timeouts
- HMAC-SHA256 signature follows standard format (
v1=<hex>)- Response capture handles both direct text and streaming bodies
- Stream reader is properly canceled when truncating (line 800) ✓
- WebhookHttpError captures response metadata for diagnostics
| <TableRow | ||
| key={call.id} | ||
| className={`cursor-pointer transition-colors ${ | ||
| selectedCallId === call.id | ||
| ? "bg-accent/50 text-accent-foreground" | ||
| : "hover:bg-muted/50" | ||
| }`} | ||
| onClick={() => onSelectCall(call.id)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add keyboard accessibility to clickable table rows.
The table rows are clickable but lack keyboard accessibility. Users navigating with a keyboard cannot interact with these rows because they're missing tabIndex, onKeyDown, and appropriate ARIA attributes.
🔎 Proposed fix to add keyboard support
calls.map((call) => (
<TableRow
key={call.id}
+ role="button"
+ tabIndex={0}
className={`cursor-pointer transition-colors ${
selectedCallId === call.id
? "bg-accent/50 text-accent-foreground"
: "hover:bg-muted/50"
}`}
onClick={() => onSelectCall(call.id)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onSelectCall(call.id);
+ }
+ }}
>This enables keyboard users to:
- Tab to each row
- Press Enter or Space to select a webhook call
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <TableRow | |
| key={call.id} | |
| className={`cursor-pointer transition-colors ${ | |
| selectedCallId === call.id | |
| ? "bg-accent/50 text-accent-foreground" | |
| : "hover:bg-muted/50" | |
| }`} | |
| onClick={() => onSelectCall(call.id)} | |
| <TableRow | |
| key={call.id} | |
| role="button" | |
| tabIndex={0} | |
| className={`cursor-pointer transition-colors ${ | |
| selectedCallId === call.id | |
| ? "bg-accent/50 text-accent-foreground" | |
| : "hover:bg-muted/50" | |
| }`} | |
| onClick={() => onSelectCall(call.id)} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| onSelectCall(call.id); | |
| } | |
| }} |
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx
around lines 130-137, the clickable TableRow lacks keyboard accessibility; add
tabIndex={0} so rows are focusable, add an onKeyDown handler that invokes
onSelectCall(call.id) when Enter or Space is pressed, and include appropriate
ARIA attributes (e.g., role="button" and aria-selected={selectedCallId ===
call.id}) so assistive tech knows the row is interactive and its selection
state; keep the existing onClick behavior intact.
| // TODO: perform signed HTTP POST with backoff tracking and update status/metrics. | ||
| // This stub ensures call rows and queue wiring exist before adding delivery logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove outdated TODO comment.
This TODO suggests the code is a stub, but the full webhook delivery implementation is present (signed HTTP POST, backoff tracking, status updates, auto-disable logic). The comment is misleading and should be removed.
🔎 Proposed fix
- // TODO: perform signed HTTP POST with backoff tracking and update status/metrics.
- // This stub ensures call rows and queue wiring exist before adding delivery logic.
const lockKey = `webhook:lock:${call.webhookId}`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // TODO: perform signed HTTP POST with backoff tracking and update status/metrics. | |
| // This stub ensures call rows and queue wiring exist before adding delivery logic. | |
| const lockKey = `webhook:lock:${call.webhookId}`; |
🤖 Prompt for AI Agents
In apps/web/src/server/service/webhook-service.ts around lines 449 to 450,
remove the outdated TODO comment that claims webhook delivery is a stub; delete
that comment line(s) so the codebase no longer contains misleading TODOs and
leave any actual implementation or explanatory comments that accurately reflect
the present signed HTTP POST, backoff tracking, status updates, and auto-disable
behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (4)
packages/lib/tsconfig.lint.json (1)
6-6: Remove the non-existent "turbo" directory from the include array.As flagged in a previous review, the "turbo" directory does not exist in
packages/liband should be removed from the include array.🔎 Proposed fix
- "include": ["src", "turbo", "**/*.ts", "**/*.tsx"], + "include": ["src", "**/*.ts", "**/*.tsx"],apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (1)
41-44: Add error handling for clipboard operation.
navigator.clipboard.writeTextreturns a Promise that can reject (e.g., due to permissions). The success toast will show even if the copy fails.🔎 Proposed fix
- const handleCopySecret = () => { - navigator.clipboard.writeText(webhook.secret); - toast.success("Secret copied to clipboard"); + const handleCopySecret = async () => { + try { + await navigator.clipboard.writeText(webhook.secret); + toast.success("Secret copied to clipboard"); + } catch { + toast.error("Failed to copy secret"); + } };apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)
130-152: Add keyboard accessibility to clickable table rows.The table rows are interactive via click but cannot be accessed via keyboard navigation. This is an accessibility concern for users who navigate with keyboards.
🔎 Proposed fix
<TableRow key={call.id} + role="button" + tabIndex={0} className={`cursor-pointer transition-colors ${ selectedCallId === call.id ? "bg-accent/50 text-accent-foreground" : "hover:bg-muted/50" }`} onClick={() => onSelectCall(call.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelectCall(call.id); + } + }} >apps/web/src/server/service/webhook-service.ts (1)
449-450: Remove outdated TODO comment.This TODO claims the code is a stub, but the full webhook delivery implementation is present (signed HTTP POST, backoff tracking, status updates, auto-disable logic). The comment is misleading.
🔎 Proposed fix
- // TODO: perform signed HTTP POST with backoff tracking and update status/metrics. - // This stub ensures call rows and queue wiring exist before adding delivery logic. const lockKey = `webhook:lock:${call.webhookId}`;
🧹 Nitpick comments (12)
references/webhook-architecture.md (1)
7-7: Consider adding language specifiers to fenced code blocks.Several fenced code blocks lack language specifiers, which can affect rendering and tooling support. Consider adding appropriate tags:
- Lines 7, 133: Use
```textfor ASCII diagrams- Line 173: Use
```textfor schema definitions- Line 320: Use
```httpor```textfor HTTP headers- Line 364: Use
```textfor the status flowThis improves syntax highlighting and markdown linting compliance.
Example fix for the first diagram
-``` +```text ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ EVENT SOURCES │Also applies to: 133-133, 173-173, 320-320, 364-364
apps/web/src/server/service/ses-hook-parser.ts (2)
372-395: Consider deriving valid subtypes from the type definition.The function correctly normalizes bounce subtypes, but the
validSubTypesarray is hardcoded and could drift from theEmailBounceSubTypedefinition over time.💡 Alternative approach using type-level validation
If the webhook events library exports the valid subtypes as a constant array, you could import and reuse it here to maintain a single source of truth. However, if the type is only available at compile-time, the current approach is acceptable with proper documentation.
430-489: Add explicit return type annotation for type safety.The function currently has an implicit
anyreturn type. Consider adding an explicit return type annotation for better type safety and documentation.🔎 Suggested improvement
function buildEmailMetadata( status: EmailStatus, mailData: SesEvent | SesEvent[SesEventDataKey], -) { +): Record<string, unknown> | undefined {This makes the return type explicit and prevents accidental
anytype propagation.apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (2)
154-154: Remove unusedformStateparameter.The
formStateparameter is destructured but never used in this render function.🔎 Proposed fix
- render={({ field, formState }) => ( + render={({ field }) => (
128-133: SimplifyonOpenChangehandler.The check
nextOpen !== openis unnecessary sinceonOpenChangeis only called when the dialog state actually changes.🔎 Proposed fix
<Dialog open={open} - onOpenChange={(nextOpen) => - nextOpen !== open ? onOpenChange(nextOpen) : null - } + onOpenChange={onOpenChange} >apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (2)
16-16: Remove unused importFormDescription.
FormDescriptionis imported but never used in this file.🔎 Proposed fix
import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@usesend/ui/src/form";
148-148: Remove unusedformStateparameter.The
formStateparameter is destructured but never used in this render function.🔎 Proposed fix
- render={({ field, formState }) => ( + render={({ field }) => (apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (2)
78-78: UseWebhookCallStatusenum instead of string literal.For consistency and type safety, import and use the
WebhookCallStatusenum rather than the string literal"FAILED".🔎 Proposed fix
+import { WebhookCallStatus } from "@prisma/client"; + // ... - {call.status === "FAILED" && ( + {call.status === WebhookCallStatus.FAILED && (
163-170: Remove unnecessary fragment wrapper.The fragment
<>...</>wraps only a single<div>child, making it redundant.🔎 Proposed fix
{call.responseText && ( - <> - <div className="flex flex-col gap-3"> - <h4 className="font-medium text-sm">Response Body</h4> - <CodeDisplay code={call.responseText} /> - </div> - </> + <div className="flex flex-col gap-3"> + <h4 className="font-medium text-sm">Response Body</h4> + <CodeDisplay code={call.responseText} /> + </div> )}apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)
22-22: Remove unused importCircleEllipsis.The
CircleEllipsisicon is imported but never used in the component.🔎 Proposed fix
import { Edit3, Key, MoreVertical, Pause, Play, TestTube, - CircleEllipsis, } from "lucide-react";
57-58: Consider usingWebhookStatusenum for status comparisons.Lines 57-58 and line 180 use string literals for status comparisons. Using the Prisma-generated enum would provide better type safety.
🔎 Proposed fix
+import { WebhookStatus } from "@prisma/client"; // In WebhookDetailActions: - const isPaused = webhook.status === "PAUSED"; - const isAutoDisabled = webhook.status === "AUTO_DISABLED"; + const isPaused = webhook.status === WebhookStatus.PAUSED; + const isAutoDisabled = webhook.status === WebhookStatus.AUTO_DISABLED; // In handleToggleStatus: - const newStatus = currentStatus === "ACTIVE" ? "PAUSED" : "ACTIVE"; + const newStatus = currentStatus === WebhookStatus.ACTIVE ? WebhookStatus.PAUSED : WebhookStatus.ACTIVE;packages/sdk/src/webhooks.ts (1)
180-187: Consider documenting the negative tolerance escape hatch.Setting
toleranceto a negative value disables timestamp validation. While this can be useful for testing, it might be worth documenting this behavior in the JSDoc or adding a named constant likeSKIP_TIMESTAMP_VALIDATION = -1.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (55)
apps/web/package.jsonapps/web/prisma/migrations/20251122195838_add_webhook/migration.sqlapps/web/prisma/schema.prismaapps/web/src/app/(dashboard)/dev-settings/layout.tsxapps/web/src/app/(dashboard)/emails/email-details.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsxapps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/page.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/app/(dashboard)/webhooks/webhook-list.tsxapps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsxapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/components/AppSideBar.tsxapps/web/src/components/code-display.tsxapps/web/src/lib/constants/plans.tsapps/web/src/server/api/root.tsapps/web/src/server/api/routers/contacts.tsapps/web/src/server/api/routers/email.tsapps/web/src/server/api/routers/limits.tsapps/web/src/server/api/routers/webhook.tsapps/web/src/server/jobs/webhook-cleanup-job.tsapps/web/src/server/public-api/api/contacts/add-contact.tsapps/web/src/server/public-api/api/contacts/delete-contact.tsapps/web/src/server/public-api/api/contacts/update-contact.tsapps/web/src/server/public-api/api/contacts/upsert-contact.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/contact-service.tsapps/web/src/server/service/domain-service.tsapps/web/src/server/service/limit-service.tsapps/web/src/server/service/ses-hook-parser.tsapps/web/src/server/service/webhook-service.tsapps/web/tailwind.config.tsdocker/dev/compose.ymlpackages/lib/.eslintrc.cjspackages/lib/index.tspackages/lib/package.jsonpackages/lib/src/constants/ses-errors.tspackages/lib/src/index.tspackages/lib/src/webhook/webhook-events.tspackages/lib/tsconfig.jsonpackages/lib/tsconfig.lint.jsonpackages/sdk/README.mdpackages/sdk/index.tspackages/sdk/package.jsonpackages/sdk/src/usesend.tspackages/sdk/src/webhooks.tspackages/ui/src/dropdown-menu.tsxpackages/ui/styles/globals.cssreferences/webhook-architecture.md
🚧 Files skipped from review as they are similar to previous changes (27)
- packages/sdk/package.json
- apps/web/src/app/(dashboard)/webhooks/page.tsx
- apps/web/src/server/api/routers/email.ts
- packages/sdk/README.md
- apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx
- apps/web/src/server/public-api/api/contacts/delete-contact.ts
- docker/dev/compose.yml
- apps/web/tailwind.config.ts
- packages/lib/tsconfig.json
- packages/lib/src/index.ts
- packages/sdk/index.ts
- apps/web/src/server/public-api/api/contacts/update-contact.ts
- apps/web/src/app/(dashboard)/emails/email-details.tsx
- apps/web/src/server/public-api/api/contacts/upsert-contact.ts
- apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-payload-display.tsx
- packages/lib/package.json
- apps/web/src/app/(dashboard)/dev-settings/layout.tsx
- apps/web/src/server/service/contact-service.ts
- apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx
- apps/web/src/server/jobs/webhook-cleanup-job.ts
- apps/web/src/server/public-api/api/contacts/add-contact.ts
- apps/web/src/server/api/routers/contacts.ts
- apps/web/prisma/migrations/20251122195838_add_webhook/migration.sql
- packages/ui/styles/globals.css
- apps/web/src/server/service/limit-service.ts
- apps/web/src/components/AppSideBar.tsx
- packages/lib/src/webhook/webhook-events.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{tsx,ts,jsx,js}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports and ensure proper naming of key components in React/NextJS code
Files:
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/server/api/root.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/lib/constants/plans.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/server/service/domain-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxpackages/sdk/src/usesend.tsapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/components/code-display.tsxapps/web/src/server/service/contact-queue-service.tspackages/sdk/src/webhooks.tsapps/web/src/server/service/webhook-service.tsapps/web/src/server/api/routers/limits.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs
Files:
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/server/api/root.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/lib/constants/plans.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/server/service/domain-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxpackages/sdk/src/usesend.tsapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/components/code-display.tsxapps/web/src/server/service/contact-queue-service.tspackages/sdk/src/webhooks.tsapps/web/src/server/service/webhook-service.tsapps/web/src/server/api/routers/limits.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
React components must use PascalCase naming convention (e.g., AppSideBar.tsx)
Files:
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/components/code-display.tsx
apps/web/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use alias
~/for src imports in apps/web (e.g.,import { x } from "~/utils/x")
Files:
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/server/api/root.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/lib/constants/plans.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/service/domain-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/components/code-display.tsxapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/webhook-service.tsapps/web/src/server/api/routers/limits.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}: Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Use Prisma for database access in apps/web
Files:
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/server/api/root.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/lib/constants/plans.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxapps/web/src/server/service/domain-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/components/code-display.tsxapps/web/src/server/service/contact-queue-service.tsapps/web/src/server/service/webhook-service.tsapps/web/src/server/api/routers/limits.ts
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files
Files:
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsxapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsxapps/web/src/app/(dashboard)/webhooks/delete-webhook.tsxapps/web/src/server/api/root.tsapps/web/src/server/queue/queue-constants.tsapps/web/src/lib/constants/plans.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsxpackages/ui/src/dropdown-menu.tsxapps/web/src/server/service/domain-service.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsxpackages/sdk/src/usesend.tsapps/web/src/server/api/routers/webhook.tsapps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsxapps/web/src/server/service/ses-hook-parser.tsapps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsxapps/web/src/components/code-display.tsxapps/web/src/server/service/contact-queue-service.tspackages/sdk/src/webhooks.tsapps/web/src/server/service/webhook-service.tsreferences/webhook-architecture.mdapps/web/src/server/api/routers/limits.ts
🧠 Learnings (5)
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code
Applied to files:
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsxapps/web/src/components/code-display.tsx
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Prefer to use TRPC for client-server communication unless explicitly asked otherwise in apps/web
Applied to files:
apps/web/src/server/api/routers/webhook.ts
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Applied to files:
packages/lib/tsconfig.lint.jsonpackages/lib/.eslintrc.cjs
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run ESLint via usesend/eslint-config and ensure no warnings remain before submitting PRs
Applied to files:
packages/lib/tsconfig.lint.jsonpackages/lib/.eslintrc.cjs
📚 Learning: 2025-11-28T21:14:07.734Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T21:14:07.734Z
Learning: Applies to apps/web/**/*.{ts,tsx} : Use Prisma for database access in apps/web
Applied to files:
apps/web/prisma/schema.prisma
🧬 Code graph analysis (15)
apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (2)
packages/lib/src/webhook/webhook-events.ts (5)
WebhookEvents(36-40)WebhookEventType(42-42)ContactEvents(1-5)DomainEvents(9-14)EmailEvents(18-32)apps/web/src/store/upgradeModalStore.ts (1)
useUpgradeModalStore(13-20)
apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (3)
packages/ui/src/toaster.tsx (1)
toast(31-31)apps/web/src/components/DeleteResource.tsx (1)
DeleteResource(63-199)packages/ui/src/button.tsx (1)
Button(80-80)
apps/web/src/server/api/root.ts (1)
apps/web/src/server/api/routers/webhook.ts (1)
webhookRouter(9-135)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (4)
packages/ui/src/toaster.tsx (1)
toast(31-31)packages/ui/src/button.tsx (1)
Button(80-80)apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
WebhookCallStatusBadge(3-41)apps/web/src/components/code-display.tsx (1)
CodeDisplay(15-111)
apps/web/src/server/service/domain-service.ts (3)
packages/lib/src/webhook/webhook-events.ts (2)
DomainPayload(86-100)DomainWebhookEventType(16-16)apps/web/src/server/db.ts (1)
db(20-20)apps/web/src/server/service/webhook-service.ts (1)
WebhookService(84-391)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (4)
packages/ui/src/toaster.tsx (1)
toast(31-31)packages/ui/src/badge.tsx (1)
Badge(36-36)apps/web/src/app/(dashboard)/webhooks/webhook-status-badge.tsx (1)
WebhookStatusBadge(3-25)packages/ui/src/button.tsx (1)
Button(80-80)
packages/sdk/src/usesend.ts (2)
packages/sdk/index.ts (1)
Webhooks(5-5)packages/sdk/src/webhooks.ts (1)
Webhooks(45-199)
apps/web/src/server/api/routers/webhook.ts (3)
packages/lib/src/webhook/webhook-events.ts (1)
WebhookEvents(36-40)apps/web/src/server/api/trpc.ts (2)
createTRPCRouter(82-82)teamProcedure(127-152)apps/web/src/server/service/webhook-service.ts (1)
WebhookService(84-391)
apps/web/src/server/service/ses-hook-parser.ts (3)
apps/web/src/server/service/webhook-service.ts (1)
WebhookService(84-391)packages/lib/src/webhook/webhook-events.ts (4)
EmailEventPayloadMap(151-165)EmailStatus(44-58)EmailWebhookEventType(34-34)EmailBasePayload(60-72)apps/web/src/types/aws-types.ts (3)
SesEvent(143-156)SesBounce(30-50)SesClick(91-97)
apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (10)
packages/ui/src/popover.tsx (3)
Popover(36-36)PopoverTrigger(36-36)PopoverContent(36-36)packages/ui/src/button.tsx (1)
Button(80-80)apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx (1)
DeleteWebhook(11-61)apps/web/src/server/service/webhook-service.ts (2)
testWebhook(162-194)updateWebhook(256-293)packages/ui/src/toaster.tsx (1)
toast(31-31)packages/ui/src/breadcrumb.tsx (5)
Breadcrumb(108-108)BreadcrumbList(109-109)BreadcrumbItem(110-110)BreadcrumbLink(111-111)BreadcrumbPage(112-112)apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx (1)
WebhookInfo(13-115)apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)
WebhookCallsTable(28-173)apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)
WebhookCallDetails(14-174)apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx (1)
EditWebhookDialog(66-323)
apps/web/src/components/code-display.tsx (1)
packages/email-editor/src/renderer.tsx (1)
code(740-756)
apps/web/src/server/service/contact-queue-service.ts (1)
apps/web/src/server/service/contact-service.ts (1)
addOrUpdateContact(19-78)
packages/sdk/src/webhooks.ts (2)
packages/sdk/index.ts (7)
WebhookVerificationError(6-6)WEBHOOK_SIGNATURE_HEADER(9-9)WEBHOOK_TIMESTAMP_HEADER(10-10)WEBHOOK_EVENT_HEADER(7-7)WEBHOOK_CALL_HEADER(8-8)Webhooks(5-5)WebhookEventData(14-14)packages/lib/src/webhook/webhook-events.ts (1)
WebhookEventData(194-196)
apps/web/src/server/service/webhook-service.ts (7)
apps/web/src/server/queue/bullmq-context.ts (2)
TeamJob(5-5)createWorkerHandler(10-24)packages/lib/src/webhook/webhook-events.ts (2)
WebhookEventType(42-42)WebhookPayloadData(184-185)apps/web/src/server/queue/queue-constants.ts (2)
WEBHOOK_DISPATCH_QUEUE(6-6)DEFAULT_QUEUE_OPTIONS(9-14)apps/web/src/server/redis.ts (1)
getRedis(6-13)apps/web/src/server/db.ts (1)
db(20-20)apps/web/src/server/public-api/api-error.ts (1)
UnsendApiError(62-75)apps/web/src/server/service/limit-service.ts (1)
LimitService(19-280)
apps/web/src/server/api/routers/limits.ts (1)
apps/web/src/server/service/limit-service.ts (1)
LimitService(19-280)
🪛 ast-grep (0.40.3)
apps/web/src/components/code-display.tsx
[warning] 106-106: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
apps/web/src/components/code-display.tsx
[error] 107-107: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🪛 markdownlint-cli2 (0.18.1)
references/webhook-architecture.md
7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
133-133: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
173-173: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
320-320: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
364-364: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ 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: Cloudflare Pages
🔇 Additional comments (32)
apps/web/src/server/service/contact-queue-service.ts (1)
100-108: LGTM! Team-scoped webhook support correctly implemented.The changes properly extract and forward
teamIdto enable team-scoped webhook event emission. The implementation is consistent with the existing job data structure and aligns with the updatedaddOrUpdateContactsignature.packages/lib/.eslintrc.cjs (1)
1-10: LGTM!The ESLint configuration is correctly structured and follows the monorepo's conventions. It properly extends the shared library config, uses the TypeScript parser, and correctly references the lint-specific tsconfig file.
apps/web/package.json (1)
42-42: LGTM! Dependency addition is correct.The addition of
@usesend/libas a workspace dependency aligns with the PR's webhook functionality requirements and follows the monorepo pattern used for other internal packages.apps/web/src/lib/constants/plans.ts (2)
7-7: LGTM! Webhook limit reason added consistently.The
WEBHOOKenum value follows the existing naming convention and integrates properly with the limit checking system.
21-21: LGTM! Webhook limits configured appropriately.The webhook limits are properly integrated into the plan structure with sensible values: 1 webhook for FREE tier and unlimited (-1) for BASIC tier. This matches the pattern used for other resource limits.
Also applies to: 30-30, 38-38
packages/ui/src/dropdown-menu.tsx (1)
102-113: LGTM! Checkbox item layout improved.The refactoring moves the checkbox indicator from the left to the right side using
ml-auto, which improves the visual hierarchy. The structural changes maintain the same public API while updating the internal rendering order.apps/web/src/components/code-display.tsx (2)
107-107: Safe usage ofdangerouslySetInnerHTMLwith Shiki.The
dangerouslySetInnerHTMLusage here is safe because Shiki'scodeToHtmlfunction properly escapes all code content and only generates safe HTML for syntax highlighting. This is an appropriate use case for this API.
15-111: Component implementation looks solid.The
CodeDisplaycomponent properly handles async highlighting with mount tracking, provides a loading state with unstyled code to preserve layout, and includes copy-to-clipboard functionality. The isMounted pattern in the useEffect correctly prevents state updates after unmount.references/webhook-architecture.md (1)
1-422: Excellent documentation of the webhook architecture.This documentation provides comprehensive coverage of the webhook system, including clear diagrams, status flows, payload structures, retry logic, and configuration details. The architecture is well-designed with proper reliability features (locking, retries, auto-disable) and the documentation accurately reflects the implementation.
apps/web/src/server/service/ses-hook-parser.ts (3)
1-11: LGTM!The webhook-related imports are properly structured and necessary for the new webhook emission functionality.
Also applies to: 33-33
279-302: LGTM! Proper error isolation for webhook emission.The webhook emission is correctly wrapped in a try-catch block to prevent failures from disrupting the main SES hook processing flow. The error logging includes appropriate context.
310-370: LGTM! Well-structured payload builder with type safety.The function appropriately constructs type-safe webhook payloads for different email statuses with proper fallbacks and optional chaining.
apps/web/src/server/api/routers/limits.ts (1)
21-22: LGTM!The webhook limit case follows the established pattern and properly delegates to the limit service.
packages/sdk/src/usesend.ts (2)
6-6: LGTM!The Webhooks import is correctly added to support the new webhook functionality.
175-195: LGTM! The JSDoc example has been corrected.The webhooks factory method follows the Stripe pattern and is well-documented. The example now correctly shows passing an options object with headers property, addressing the previous review feedback.
apps/web/src/server/api/root.ts (1)
17-17: LGTM!The webhook router is properly imported and registered, following the established pattern for other routers in the application.
Also applies to: 40-40
apps/web/src/server/queue/queue-constants.ts (1)
6-7: LGTM!The new queue constants follow the established naming convention and clearly indicate their purpose for webhook dispatch and cleanup operations.
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (1)
1-41: LGTM!The component correctly maps all
WebhookCallStatusenum values to appropriate badge colors and labels. The switch statement provides exhaustive coverage with a sensible default fallback.Minor note: the
capitalizeclass on line 36 is redundant since all labels are already capitalized in the switch cases, but it doesn't cause any issues.apps/web/src/server/service/domain-service.ts (3)
90-106: LGTM!The
buildDomainPayloadhelper correctly maps all fields from theDomainmodel to theDomainPayloadtype, including proper ISO string conversion for dates and handling of nullable fields.
387-396: LGTM!The
emitDomainEventhelper properly wraps webhook emission with try/catch and structured logging, ensuring that webhook failures don't break domain operations. This is a good defensive pattern.
306-312: LGTM!The webhook emission logic correctly detects status changes and emits the appropriate event type (
domain.verifiedfor successful verification,domain.updatedotherwise). ComparingpreviousStatusagainstdomainWithDns.statusensures events are only emitted when there's an actual change.apps/web/src/server/api/routers/webhook.ts (1)
1-135: LGTM!The webhook router is well-structured and follows TRPC best practices:
- All endpoints are properly scoped by
ctx.team.idfor multi-tenant isolation- Input validation via zod schemas is comprehensive
- Business logic is cleanly delegated to
WebhookService- The
teamProcedurewrapper ensures proper access controlBased on learnings, using TRPC for client-server communication aligns with the project's conventions.
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)
1-72: LGTM on core component structure and data handling.The component correctly handles loading states, retry functionality with proper cache invalidation, and payload reconstruction. The
responseTimeMs != nullcheck at line 134 properly handles the 0ms edge case.apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (1)
137-162: LGTM on page setup and auto-selection logic.The use of React 19's
use()hook for async params is correct for Next.js 15. TheuseEffectfor auto-selecting the first call appropriately guards against overwriting user selections.apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-calls-table.tsx (1)
28-68: LGTM on pagination and filtering implementation.The cursor-based pagination with proper cursor stack management and filter reset logic is well implemented. The status filter correctly maps to the API query parameters.
apps/web/prisma/schema.prisma (2)
464-506: LGTM on webhook schema design.The Webhook and WebhookCall models are well-designed with:
- Appropriate cascade delete behavior for team/webhook relationships
- SetNull for user deletion preserving webhook history
- Composite index
(teamId, webhookId, status)supporting common query patterns- Descending createdAt index for efficient recent-first ordering
450-462: LGTM on webhook status enums.The
WebhookStatusandWebhookCallStatusenums provide clear state management for the webhook lifecycle with appropriate values for active/paused/auto-disabled webhooks and pending/in-progress/delivered/failed/discarded calls.packages/sdk/src/webhooks.ts (2)
263-272: Timing-safe comparison implementation is correct.The
safeEqualfunction properly usestimingSafeEqualfrom crypto module with length check guard. The early return on length mismatch is acceptable since length differences are already observable through timing of other operations.
45-137: LGTM on webhook verification implementation.The
Webhooksclass provides a clean API following established patterns (similar to Stripe's SDK). Key security aspects are handled correctly:
- HMAC-SHA256 signature verification
- Timestamp validation with configurable tolerance
- Timing-safe comparison to prevent timing attacks
- Comprehensive error codes for debugging
apps/web/src/server/service/webhook-service.ts (3)
576-602: LGTM on Redis-based distributed locking.The lock implementation correctly uses:
SET key value PX ttl NXfor atomic lock acquisition- Lua script for safe release ensuring only the lock owner can release
- Proper error handling in
releaseLock
752-821: LGTM on response capture with streaming support.The
captureResponseTextfunction properly:
- Checks content-type before attempting to read text
- Handles Content-Length header for early bailout on oversized responses
- Uses streaming reader with proper truncation and
reader.cancel()to release resources- Falls back to
response.text()when streaming is unavailable
84-133: LGTM on webhook event emission.The
emitmethod correctly:
- Queries for active webhooks matching the event type (or subscribed to all events via empty array)
- Creates webhook call records before enqueueing to ensure delivery tracking
- Handles the fan-out pattern for multiple webhooks
| const deliveredCount = last7DaysCalls.filter( | ||
| (c) => c.status === WebhookCallStatus.DELIVERED, | ||
| ).length; | ||
| const failedCount = last7DaysCalls.filter( | ||
| (c) => c.status === WebhookCallStatus.FAILED, | ||
| ).length; | ||
| const pendingCount = last7DaysCalls.filter( | ||
| (c) => | ||
| c.status === WebhookCallStatus.PENDING || | ||
| c.status === WebhookCallStatus.IN_PROGRESS, | ||
| ).length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove unused computed values.
deliveredCount, failedCount, and pendingCount are computed but never rendered. This is dead code that wastes cycles on every render.
🔎 Proposed fix
const calls = callsQuery.data?.items ?? [];
- const last7DaysCalls = calls.filter(
- (call) => new Date(call.createdAt) >= sevenDaysAgo,
- );
-
- const deliveredCount = last7DaysCalls.filter(
- (c) => c.status === WebhookCallStatus.DELIVERED,
- ).length;
- const failedCount = last7DaysCalls.filter(
- (c) => c.status === WebhookCallStatus.FAILED,
- ).length;
- const pendingCount = last7DaysCalls.filter(
- (c) =>
- c.status === WebhookCallStatus.PENDING ||
- c.status === WebhookCallStatus.IN_PROGRESS,
- ).length;If these stats are intended to be displayed, implement the UI; otherwise remove to avoid unnecessary computation.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const deliveredCount = last7DaysCalls.filter( | |
| (c) => c.status === WebhookCallStatus.DELIVERED, | |
| ).length; | |
| const failedCount = last7DaysCalls.filter( | |
| (c) => c.status === WebhookCallStatus.FAILED, | |
| ).length; | |
| const pendingCount = last7DaysCalls.filter( | |
| (c) => | |
| c.status === WebhookCallStatus.PENDING || | |
| c.status === WebhookCallStatus.IN_PROGRESS, | |
| ).length; | |
| const calls = callsQuery.data?.items ?? []; | |
| const last7DaysCalls = calls.filter( | |
| (call) => new Date(call.createdAt) >= sevenDaysAgo, | |
| ); |
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-info.tsx around
lines 29 to 39, the computed variables deliveredCount, failedCount, and
pendingCount are calculated but never used; remove these unused computations to
eliminate wasted work (delete the three const declarations and any now-unused
imports/types), or if the intent was to display these stats, hook them into the
component UI (add the corresponding JSX to render the counts where appropriate)
and ensure they are memoized if needed to avoid repeated computation.
| async function onConfirm(values: z.infer<typeof schema>) { | ||
| deleteWebhookMutation.mutate( | ||
| { id: webhook.id }, | ||
| { | ||
| onSuccess: async () => { | ||
| await utils.webhook.list.invalidate(); | ||
| toast.success("Webhook deleted"); | ||
| }, | ||
| onError: (error) => { | ||
| toast.error(error.message); | ||
| }, | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Async onConfirm should await the mutation.
The onConfirm function is marked async but uses mutate() which doesn't return a Promise. Since DeleteResource awaits onConfirm(), the function returns immediately while the mutation is still pending. Use mutateAsync() to properly chain the async behavior.
🔎 Proposed fix
async function onConfirm(values: z.infer<typeof schema>) {
- deleteWebhookMutation.mutate(
+ await deleteWebhookMutation.mutateAsync(
{ id: webhook.id },
- {
- onSuccess: async () => {
- await utils.webhook.list.invalidate();
- toast.success("Webhook deleted");
- },
- onError: (error) => {
- toast.error(error.message);
- },
- },
);
+ await utils.webhook.list.invalidate();
+ toast.success("Webhook deleted");
}Note: Error handling will be managed by the mutation's error state or you can wrap in try/catch if custom error handling is needed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function onConfirm(values: z.infer<typeof schema>) { | |
| deleteWebhookMutation.mutate( | |
| { id: webhook.id }, | |
| { | |
| onSuccess: async () => { | |
| await utils.webhook.list.invalidate(); | |
| toast.success("Webhook deleted"); | |
| }, | |
| onError: (error) => { | |
| toast.error(error.message); | |
| }, | |
| }, | |
| ); | |
| } | |
| async function onConfirm(values: z.infer<typeof schema>) { | |
| await deleteWebhookMutation.mutateAsync( | |
| { id: webhook.id }, | |
| ); | |
| await utils.webhook.list.invalidate(); | |
| toast.success("Webhook deleted"); | |
| } |
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/webhooks/delete-webhook.tsx around lines 26 to
39, onConfirm is async but calls deleteWebhookMutation.mutate which is not
promise-based, so the caller awaits prematurely; replace the mutate call with
await deleteWebhookMutation.mutateAsync({ id: webhook.id }) and either keep the
existing onSuccess/onError behavior on the mutation or handle success/error in a
try/catch around the await (e.g., await mutateAsync then await
utils.webhook.list.invalidate() and call toast.success, catch to call
toast.error).
| function emailStatusToEvent(status: EmailStatus): EmailWebhookEventType { | ||
| switch (status) { | ||
| case EmailStatus.QUEUED: | ||
| return "email.queued"; | ||
| case EmailStatus.SENT: | ||
| return "email.sent"; | ||
| case EmailStatus.DELIVERY_DELAYED: | ||
| return "email.delivery_delayed"; | ||
| case EmailStatus.DELIVERED: | ||
| return "email.delivered"; | ||
| case EmailStatus.BOUNCED: | ||
| return "email.bounced"; | ||
| case EmailStatus.REJECTED: | ||
| return "email.rejected"; | ||
| case EmailStatus.RENDERING_FAILURE: | ||
| return "email.rendering_failure"; | ||
| case EmailStatus.COMPLAINED: | ||
| return "email.complained"; | ||
| case EmailStatus.FAILED: | ||
| return "email.failed"; | ||
| case EmailStatus.CANCELLED: | ||
| return "email.cancelled"; | ||
| case EmailStatus.SUPPRESSED: | ||
| return "email.suppressed"; | ||
| case EmailStatus.OPENED: | ||
| return "email.opened"; | ||
| case EmailStatus.CLICKED: | ||
| return "email.clicked"; | ||
| default: | ||
| return "email.queued"; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the EmailStatus enum definition to see all possible values
rg -n "enum EmailStatus|type EmailStatus" --type ts -A 20 -B 2
# Search for emailStatusToEvent usage to understand the context
rg -n "emailStatusToEvent" --type ts -B 3 -A 3
# Check if SCHEDULED is referenced anywhere in the codebase
rg -n "SCHEDULED" --type ts -B 2 -A 2Repository: usesend/useSend
Length of output: 15706
🏁 Script executed:
# Check what EmailWebhookEventType values are available, especially for SCHEDULED
rg -n "EmailWebhookEventType|type EmailWebhookEventType" --type ts -A 20
# Check if there's any filtering that prevents SCHEDULED emails from reaching webhooks
rg -n "latestStatus.*SCHEDULED|SCHEDULED.*latestStatus" --type ts -B 3 -A 3Repository: usesend/useSend
Length of output: 14109
🏁 Script executed:
# Get the full webhook-events.ts file to see the EmailEvents definition
cat -n packages/lib/src/webhook/webhook-events.ts | head -40Repository: usesend/useSend
Length of output: 1189
Add explicit handling for EmailStatus.SCHEDULED in the switch statement.
The switch statement doesn't explicitly handle EmailStatus.SCHEDULED, which falls through to the default case returning "email.queued". Since there is no "email.scheduled" webhook event type available in the EmailEvents array, either filter out SCHEDULED emails before calling this function or throw an error to catch any unexpected status values at runtime.
🤖 Prompt for AI Agents
In apps/web/src/server/service/ses-hook-parser.ts around lines 397-428, the
switch lacks an explicit case for EmailStatus.SCHEDULED so it currently falls to
the default and returns "email.queued"; add an explicit case for
EmailStatus.SCHEDULED and handle it deterministically (either throw a clear
Error indicating SCHEDULED is unsupported here so callers must filter it out, or
map it to a defined event if you intentionally want to treat it as queued), then
update callers or document that SCHEDULED must be filtered before calling this
function.
| const updatedWebhook = await db.webhook.update({ | ||
| where: { id: call.webhookId }, | ||
| data: { | ||
| consecutiveFailures: { | ||
| increment: 1, | ||
| }, | ||
| lastFailureAt: new Date(), | ||
| status: | ||
| call.webhook.consecutiveFailures + 1 >= WEBHOOK_AUTO_DISABLE_THRESHOLD | ||
| ? WebhookStatus.AUTO_DISABLED | ||
| : call.webhook.status, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential race condition in auto-disable threshold check.
The threshold check at line 526 uses the pre-update value (call.webhook.consecutiveFailures + 1) while the atomic increment happens in the same update. If multiple webhook calls fail concurrently for the same webhook, each worker reads the same stale consecutiveFailures value, potentially delaying auto-disable.
This is low severity since the webhook will eventually be disabled, but for correctness you could use a transaction with SELECT ... FOR UPDATE or rely on the database-returned value.
🤖 Prompt for AI Agents
In apps/web/src/server/service/webhook-service.ts around lines 518-530, the code
computes the auto-disable status using the stale pre-update consecutiveFailures
value which can race under concurrent failures; wrap the read-and-update in a
transaction that locks the webhook row (SELECT ... FOR UPDATE), read the current
consecutiveFailures, compute newFailures = current + 1 and desired status based
on WEBHOOK_AUTO_DISABLE_THRESHOLD, then perform the update (or perform an atomic
update that returns the updated consecutiveFailures/status and use that returned
value) so the threshold decision is made against the locked/returned value.
Summary by cubic
Adds first-class webhooks across the app with signed delivery, retries, logging, and a full dashboard UI. Also adds SDK helpers for signature verification and typed events.
New Features
Migration
Written for commit 0116bb8. Summary will update on new commits.
Summary by CodeRabbit
New Features
Updates
✏️ Tip: You can customize this high-level summary in your review settings.