-
-
Notifications
You must be signed in to change notification settings - Fork 300
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
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 full webhook support: DB migration and Prisma models for Webhook and WebhookCall (enums, indexes, FKs); server-side WebhookService and WebhookQueueService with delivery, signing, retries, backoff, locking, and auto-disable; TRPC router endpoints for webhook CRUD, calls, test, and retry; emits webhook events from contacts, domains, and SES parsing; dashboard UI (list, detail, create/edit/delete, calls table/details); SDK and packages/lib webhook types/utilities; plan-limit checks, cleanup job, and queue constants. Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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
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.
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.
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
4ed626c to
3438882
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docker/dev/compose.yml (1)
27-36: Service name has typo that needs fixing.The compose.yml file is being created new with the correct
usesend/local-ses-sns:latestimage. However, the service name on line 27 islocal-sen-sns, which is missing an 's' in "ses" and doesn't match the container namelocal-ses-snson line 28. Fix the service name tolocal-ses-snsfor consistency.apps/web/src/server/service/contact-service.ts (1)
157-177: Emit webhook events forunsubscribeContactandsubscribeContact.Both functions update contact subscription status without emitting webhook events. Other contact mutation operations in this file (
createContact,updateContact,deleteContact) all emitcontact.updatedorcontact.createdevents. Addawait emitContactEvent(updatedContact, "contact.updated", teamId)before returning in both functions to maintain consistency and notify webhook consumers of subscription status changes.
🤖 Fix all issues with AI agents
In `@apps/web/src/server/service/webhook-service.ts`:
- Around line 222-254: The createWebhook flow currently accepts any
syntactically valid URL; before persisting (in createWebhook) or before sending
requests (e.g., postWebhook), add SSRF/egress filtering: parse the URL (ensure
protocol is http or https), resolve the hostname to IP(s) (handle both A and
AAAA records and literal IPs), and reject if any resolved address falls into
private/reserved ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
localhost/127.0.0.0/8, ::1, fc00::/7, link-local, and cloud metadata like
169.254.169.254), or if the hostname is a localhost alias; implement this check
as a reusable helper (e.g., WebhookService.validateEgressUrl or a new helper
used by createWebhook and postWebhook), throw a UnsendApiError with FORBIDDEN
when blocked, and add unit tests covering hostnames, literal IPs, and DNS
resolving to private addresses.
In `@packages/sdk/README.md`:
- Around line 147-157: The snippet calling verifyWebhookSignature references
rawBody without showing how to obtain it; update the README example to
explicitly show how to derive rawBody (e.g., reading the request body as text or
buffer) before calling verifyWebhookSignature, mirroring the Next.js example’s
approach, and ensure the snippet uses the same header names (signatureHeader /
timestampHeader) from request.headers so readers can copy-paste without
ambiguity.
♻️ Duplicate comments (6)
apps/web/src/components/code-display.tsx (1)
59-67: Clean upsetTimeouton unmount to prevent state updates on unmounted component.This issue was previously identified. The timeout should be tracked and cleared on unmount.
♻️ Recommended fix using useRef
+"use client"; + +import { useEffect, useState, useRef } from "react"; import { BundledLanguage, codeToHtml } from "shiki"; // ... rest of imports export function CodeDisplay({ // ... props }: CodeDisplayProps) { const [html, setHtml] = useState<string>(""); const [isLoading, setIsLoading] = useState(true); const [copied, setCopied] = useState(false); + const copyTimeoutRef = useRef<NodeJS.Timeout | null>(null); + + useEffect(() => { + return () => { + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + } + }; + }, []); // ... useEffect for highlighting const handleCopy = async () => { try { await navigator.clipboard.writeText(code); setCopied(true); - setTimeout(() => setCopied(false), 2000); + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + } + copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000); } catch (error) { console.error("Failed to copy to clipboard:", error); } };apps/web/src/server/service/ses-hook-parser.ts (1)
422-452: HandleEmailStatus.SCHEDULEDexplicitly (currently falls through).
EmailStatus.SCHEDULEDstill falls to the default and maps to"email.queued". This should be handled explicitly (either map intentionally or throw so callers filter it).apps/web/src/server/service/webhook-service.ts (1)
516-526: Auto-disable threshold uses stale failure count under concurrency.The status decision uses
call.webhook.consecutiveFailures + 1, which can be stale if multiple calls fail concurrently for the same webhook. Prefer a transaction/locked read or use the updated value returned by the DB update.apps/web/src/app/(dashboard)/webhooks/webhook-list.tsx (1)
31-31: Remove unusedtestWebhookmutation.The
testWebhookmutation is declared but never used in this component. This appears to be leftover code that should be removed.🔎 Proposed fix
export function WebhookList() { const webhooksQuery = api.webhook.list.useQuery(); - const testWebhook = api.webhook.test.useMutation(); const setStatusMutation = api.webhook.setStatus.useMutation();apps/web/src/app/(dashboard)/webhooks/[webhookId]/page.tsx (2)
212-226: Add error state handling forwebhookQuery.The component checks
isLoadingand!webhookbut doesn't handlewebhookQuery.isError. If the API call fails due to a network or server error, users will see "Webhook not found" instead of an appropriate error message.🔎 Proposed fix
if (webhookQuery.isLoading) { return ( <div className="flex items-center justify-center h-screen"> <p className="text-muted-foreground">Loading webhook...</p> </div> ); } + if (webhookQuery.isError) { + return ( + <div className="flex items-center justify-center h-screen"> + <p className="text-destructive"> + Failed to load webhook: {webhookQuery.error.message} + </p> + </div> + ); + } + if (!webhook) {
22-22: Remove unusedCircleEllipsisimport.The
CircleEllipsisicon is imported but never used in this component.🔎 Proposed fix
import { Edit3, Key, MoreVertical, Pause, Play, TestTube, - CircleEllipsis, } from "lucide-react";
🧹 Nitpick comments (6)
apps/web/src/app/(dashboard)/webhooks/webhook-call-status-badge.tsx (2)
11-32: Consider adding exhaustive type checking for future-proofing.The switch statement handles all current
WebhookCallStatusvalues, but adding adefaultcase with an exhaustiveness check would catch future enum additions at compile time.♻️ Optional: Add exhaustiveness check
case WebhookCallStatus.DISCARDED: badgeColor = "bg-gray-700/10 text-gray-400 border border-gray-400/10"; label = "Discarded"; break; + default: { + const _exhaustiveCheck: never = status; + break; + } }
34-40: Minor:capitalizeclass is redundant.The labels are already capitalized strings (e.g., "Delivered", "In Progress"), so the
capitalizeTailwind class has no effect.♻️ Remove redundant class
- className={`text-center w-[110px] rounded capitalize py-1 text-xs ${badgeColor}`} + className={`text-center w-[110px] rounded py-1 text-xs ${badgeColor}`}apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx (1)
154-166: TheformStateparameter is unused in this render function.The
formStateis destructured but not used in the URL field's render function, unlike the eventTypes field below which uses it for error display.♻️ Suggested fix
<FormField control={form.control} name="url" - render={({ field, formState }) => ( + render={({ field }) => ( <FormItem>references/webhook-architecture.md (1)
7-129: Consider adding a language identifier to the ASCII art code blocks.The static analysis tool flags code blocks without language specifiers. For ASCII diagrams, you can use
textorplaintextas the language identifier to satisfy linters while preserving the content.♻️ Suggested fix for diagram blocks
-``` +```text ┌─────────────────────────────────────────────────────────────────────────────────────┐Apply similar changes to the other ASCII diagram blocks at lines 133, 173, 320, and 364.
apps/web/src/app/(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx (1)
163-170: Unnecessary React fragment wrapping a single element.The fragment
<>...</>around the Response Body section contains only a single<div>child, making the fragment redundant.♻️ Suggested 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/webhook-update-dialog.tsx (1)
57-64: Consider extracting shared event selection logic.The
eventGroupsdefinition and the event selection handlers (handleToggleAll,handleToggleGroup,handleToggleEvent) are duplicated between this file andadd-webhook.tsx. Consider extracting these into a shared component or custom hook to reduce duplication and ensure consistent behavior.♻️ Example extraction
Create a shared hook or component:
// webhook-event-selector.tsx export function useWebhookEventSelection(initialEvents: WebhookEventType[]) { const [allEventsSelected, setAllEventsSelected] = useState( initialEvents.length === 0 ); // ... shared toggle logic return { allEventsSelected, handleToggleAll, handleToggleGroup, handleToggleEvent }; }
| public static async createWebhook(params: { | ||
| teamId: number; | ||
| userId: number; | ||
| url: string; | ||
| description?: string; | ||
| eventTypes: string[]; | ||
| secret?: string; | ||
| }) { | ||
| const { isLimitReached, reason } = await LimitService.checkWebhookLimit( | ||
| params.teamId, | ||
| ); | ||
|
|
||
| if (isLimitReached) { | ||
| throw new UnsendApiError({ | ||
| code: "FORBIDDEN", | ||
| message: reason ?? "Webhook limit reached", | ||
| }); | ||
| } | ||
|
|
||
| const secret = params.secret ?? WebhookService.generateSecret(); | ||
|
|
||
| return db.webhook.create({ | ||
| data: { | ||
| teamId: params.teamId, | ||
| url: params.url, | ||
| description: params.description, | ||
| secret, | ||
| eventTypes: params.eventTypes, | ||
| status: WebhookStatus.ACTIVE, | ||
| createdByUserId: params.userId, | ||
| }, | ||
| }); | ||
| } |
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
# Search for webhook URL validation or SSRF protections
rg -n "webhook.*url|validate.*url|ssrf|private ip|blocklist|allowlist" -t ts --max-count=50Repository: usesend/useSend
Length of output: 810
🏁 Script executed:
#!/bin/bash
# Find the webhook-service.ts file and check its size
find . -name "webhook-service.ts" -type f
wc -l $(find . -name "webhook-service.ts" -type f)Repository: usesend/useSend
Length of output: 160
🏁 Script executed:
#!/bin/bash
# Look at the postWebhook method around lines 666-742
sed -n '666,742p' $(find . -name "webhook-service.ts" -type f)Repository: usesend/useSend
Length of output: 1980
🏁 Script executed:
#!/bin/bash
# Search for any URL validation utilities in the codebase
rg -n "validateUrl|validateWebhookUrl|isValidUrl" -t tsRepository: usesend/useSend
Length of output: 41
🏁 Script executed:
#!/bin/bash
# Search for fetch calls related to webhooks
rg -n "fetch.*webhook|webhook.*fetch" -t ts -A 3Repository: usesend/useSend
Length of output: 400
🏁 Script executed:
#!/bin/bash
# Search for webhook-related tRPC procedures or API endpoints
rg -n "createWebhook|webhook.*procedure|webhook.*router" -t ts -B 2 -A 5 | head -100Repository: usesend/useSend
Length of output: 4164
🏁 Script executed:
#!/bin/bash
# Look for input validation schemas for webhook endpoints
rg -n "webhook.*schema|createWebhookInput|zod.*webhook" -t ts -A 5Repository: usesend/useSend
Length of output: 513
🏁 Script executed:
#!/bin/bash
# Find where webhookSchema is defined
rg -n "webhookSchema|webhook.*z\.object" -t ts -A 10 | head -60Repository: usesend/useSend
Length of output: 2634
🏁 Script executed:
#!/bin/bash
# Check the webhook router file for input validation
cat -n apps/web/src/server/api/routers/webhook.ts | head -50Repository: usesend/useSend
Length of output: 1951
Add SSRF protection to webhook URLs.
The URL validation (z.string().url()) only checks format validity, not content. A tenant can create webhooks pointing to private IPs, localhost, or cloud metadata endpoints (e.g., http://127.0.0.1:8080, http://169.254.169.254/), which would then be fetched by the server, exposing internal services. Implement egress filtering before postWebhook executes the request: validate that URLs use only http/https, block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and reject localhost/reserved addresses.
🤖 Prompt for AI Agents
In `@apps/web/src/server/service/webhook-service.ts` around lines 222 - 254, The
createWebhook flow currently accepts any syntactically valid URL; before
persisting (in createWebhook) or before sending requests (e.g., postWebhook),
add SSRF/egress filtering: parse the URL (ensure protocol is http or https),
resolve the hostname to IP(s) (handle both A and AAAA records and literal IPs),
and reject if any resolved address falls into private/reserved ranges
(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, localhost/127.0.0.0/8, ::1,
fc00::/7, link-local, and cloud metadata like 169.254.169.254), or if the
hostname is a localhost alias; implement this check as a reusable helper (e.g.,
WebhookService.validateEgressUrl or a new helper used by createWebhook and
postWebhook), throw a UnsendApiError with FORBIDDEN when blocked, and add unit
tests covering hostnames, literal IPs, and DNS resolving to private addresses.
- Remove unused Trash2 import from webhook-list.tsx - Centralize WEBHOOK_EVENT_VERSION in @usesend/lib - Add webhook.test event type with proper payload typing
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
🤖 Fix all issues with AI agents
In `@apps/web/src/app/`(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx:
- Around line 3-4: The file imports and uses formatDate from date-fns which
doesn't exist; replace the import of formatDate with format and update all
usages of formatDate (e.g., any calls like formatDate(...)) to call format(...)
instead; ensure the symbol names referenced are updated (import: format, usages:
replace formatDate with format) so date formatting works with date-fns v4.1.0.
In `@apps/web/src/server/service/webhook-service.ts`:
- Around line 530-543: In the db.webhookCall.update call that updates
status/attempt/nextAttemptAt/lastError (the data block around
db.webhookCall.update), change the response fields so they are set to null on no
response rather than using the "?? undefined" pattern; specifically set
responseStatus, responseTimeMs, and responseText to null when there is no new
value so previous attempt data is cleared (preserve existing WebhookCallStatus
logic and attempt/nextAttemptAt handling).
♻️ Duplicate comments (1)
apps/web/src/server/service/webhook-service.ts (1)
222-254: Add SSRF/egress validation for webhook URLs.
createWebhookaccepts any valid URL string; without egress filtering, tenants could target localhost/private IPs or metadata endpoints. Add URL validation (http/https only) plus DNS/IP allow/deny checks (localhost, RFC1918, link-local, metadata IPs) before persisting and beforepostWebhooksends.
| import { formatDate } from "date-fns"; | ||
| import { RefreshCw } from "lucide-react"; |
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
🌐 Web query:
date-fns v4.1.0 formatDate export API documentation
💡 Result:
Short answer: date-fns v4.1.0 does not export a function named formatDate — the formatter API function is named format (and related helpers like formatISO, formatRelative, etc.). v4.1.0 added missing time‑zone support to format/formatISO/formatISO9075/formatRelative/formatRFC3339. [1][2]
References
- v4.1.0 release notes (time‑zone support added). [1]
- API docs for format (signature and examples). [2]
- npm listing for date-fns v4.1.0. [3]
Sources
[1] GitHub Releases — date-fns v4.1.0.
[2] date-fns (v4.1.0) API docs (format).
[3] npm: date-fns v4.1.0.
🏁 Script executed:
# Examine the file to see imports and usages
cat -n apps/web/src/app/\(dashboard\)/webhooks/\[webhookId\]/webhook-call-details.tsx | head -20Repository: usesend/useSend
Length of output: 915
🏁 Script executed:
# Check for all occurrences of formatDate in the file
rg "formatDate" apps/web/src/app/\(dashboard\)/webhooks/\[webhookId\]/webhook-call-details.tsx -nRepository: usesend/useSend
Length of output: 171
Use format instead of formatDate from date-fns.
formatDate is not exported from date-fns v4.1.0. The correct function is format.
🛠️ Proposed fix
-import { formatDate } from "date-fns";
+import { format } from "date-fns";Also applies to: 113
🤖 Prompt for AI Agents
In `@apps/web/src/app/`(dashboard)/webhooks/[webhookId]/webhook-call-details.tsx
around lines 3 - 4, The file imports and uses formatDate from date-fns which
doesn't exist; replace the import of formatDate with format and update all
usages of formatDate (e.g., any calls like formatDate(...)) to call format(...)
instead; ensure the symbol names referenced are updated (import: format, usages:
replace formatDate with format) so date formatting works with date-fns v4.1.0.
| await db.webhookCall.update({ | ||
| where: { id: call.id }, | ||
| data: { | ||
| status: | ||
| attempt >= WEBHOOK_MAX_ATTEMPTS | ||
| ? WebhookCallStatus.FAILED | ||
| : WebhookCallStatus.PENDING, | ||
| attempt, | ||
| nextAttemptAt, | ||
| lastError: errorMessage, | ||
| responseStatus: responseStatus ?? undefined, | ||
| responseTimeMs: responseTimeMs ?? undefined, | ||
| responseText: responseText ?? undefined, | ||
| }, |
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.
Clear response fields on error to avoid stale data.
Using ?? undefined keeps prior attempt values when a later attempt has no response data (e.g., timeout). Set null so the call reflects the latest attempt.
🛠️ Proposed fix
- responseStatus: responseStatus ?? undefined,
- responseTimeMs: responseTimeMs ?? undefined,
- responseText: responseText ?? undefined,
+ responseStatus: responseStatus ?? null,
+ responseTimeMs: responseTimeMs ?? null,
+ responseText: responseText ?? null,🤖 Prompt for AI Agents
In `@apps/web/src/server/service/webhook-service.ts` around lines 530 - 543, In
the db.webhookCall.update call that updates
status/attempt/nextAttemptAt/lastError (the data block around
db.webhookCall.update), change the response fields so they are set to null on no
response rather than using the "?? undefined" pattern; specifically set
responseStatus, responseTimeMs, and responseText to null when there is no new
value so previous attempt data is cleared (preserve existing WebhookCallStatus
logic and attempt/nextAttemptAt handling).
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.
2 issues found across 4 files (changes from recent commits).
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/docs/guides/webhooks.mdx">
<violation number="1" location="apps/docs/guides/webhooks.mdx:260">
P2: Guard against missing signature/timestamp headers before calling `verifyWebhook`; otherwise the example can throw at runtime or validate an invalid signature when a header is absent.</violation>
</file>
<file name="references/webhook-architecture.md">
<violation number="1" location="references/webhook-architecture.md:233">
P2: The TypeScript union example is now invalid: `("webhook.test");` is a standalone expression, not a union member. This makes the docs misleading and the snippet won’t compile if copied. Add it as a union member instead.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const signature = request.headers.get("X-UseSend-Signature"); | ||
| const timestamp = request.headers.get("X-UseSend-Timestamp"); | ||
|
|
||
| const isValid = verifyWebhook(secret, rawBody, signature, timestamp); |
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.
P2: Guard against missing signature/timestamp headers before calling verifyWebhook; otherwise the example can throw at runtime or validate an invalid signature when a header is absent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/docs/guides/webhooks.mdx, line 260:
<comment>Guard against missing signature/timestamp headers before calling `verifyWebhook`; otherwise the example can throw at runtime or validate an invalid signature when a header is absent.</comment>
<file context>
@@ -0,0 +1,486 @@
+const signature = request.headers.get("X-UseSend-Signature");
+const timestamp = request.headers.get("X-UseSend-Timestamp");
+
+const isValid = verifyWebhook(secret, rawBody, signature, timestamp);
+```
+
</file context>
| "email.opened" | | ||
| "email.clicked"; | ||
|
|
||
| // Test events |
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.
P2: The TypeScript union example is now invalid: ("webhook.test"); is a standalone expression, not a union member. This makes the docs misleading and the snippet won’t compile if copied. Add it as a union member instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At references/webhook-architecture.md, line 233:
<comment>The TypeScript union example is now invalid: `("webhook.test");` is a standalone expression, not a union member. This makes the docs misleading and the snippet won’t compile if copied. Add it as a union member instead.</comment>
<file context>
@@ -229,6 +229,9 @@ Defined in `packages/lib/src/webhook/webhook-events.ts`:
"email.opened" |
"email.clicked";
+
+// Test events
+("webhook.test");
</file context>
</details>
<a href="https://www.cubic.dev/action/fix/violation/879f1301-0689-4e12-a8b9-0127eda1ee96" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cubic.dev/buttons/fix-with-cubic-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://cubic.dev/buttons/fix-with-cubic-light.svg">
<img alt="Fix with Cubic" src="https://cubic.dev/buttons/fix-with-cubic-dark.svg">
</picture>
</a>
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
🤖 Fix all issues with AI agents
In `@apps/docs/guides/webhooks.mdx`:
- Around line 256-261: The example uses request.headers.get(...) which may
return null but verifyWebhook(secret, rawBody, signature, timestamp) requires
strings; update the example around the variables signature and timestamp to
handle nulls before calling verifyWebhook — e.g., retrieve them via
request.headers.get("X-UseSend-Signature") and
request.headers.get("X-UseSend-Timestamp"), validate or coerce to string (or
throw/return an error if missing) so that the values passed to verifyWebhook are
non-null strings; ensure you reference the same variable names (signature,
timestamp, secret, rawBody, verifyWebhook) in the example.
In `@references/webhook-architecture.md`:
- Line 336: Fix the typo in the signature pseudocode: replace the incorrect
"HMAC - SHA256" with the correct algorithm name "HMAC-SHA256" in the line that
computes const signature; ensure the call reads HMAC-SHA256(secret,
`${timestamp}.${JSON.stringify(body)}`) so the symbols signature, secret,
timestamp, and body are used exactly as intended.
🧹 Nitpick comments (2)
references/webhook-architecture.md (1)
7-7: Add language specifiers to fenced code blocks.Markdownlint flags these code blocks as missing a language specifier. For ASCII diagrams and pseudocode, use
textorplaintextto satisfy the linter while preserving formatting.Suggested fix
-``` +```text ┌─────────────────────────────────────────────────────────────────────────────────────┐Apply similarly to lines 133, 173, 323, and 367.
Also applies to: 133-133, 173-173, 323-323, 367-367
apps/docs/guides/webhooks.mdx (1)
329-345: Accordion list items may not render correctly.The troubleshooting accordion content uses inline dashes for list items without line breaks, which may render as a single paragraph rather than a proper list in MDX.
Suggested fix for proper list rendering
<Accordion title="Webhook not receiving events"> - - Verify your endpoint URL is correct and publicly accessible - Check that - your endpoint returns a 2xx status code - Ensure the webhook is set to - ACTIVE status in the dashboard - Check if the webhook was auto-disabled due - to consecutive failures + - Verify your endpoint URL is correct and publicly accessible + - Check that your endpoint returns a 2xx status code + - Ensure the webhook is set to ACTIVE status in the dashboard + - Check if the webhook was auto-disabled due to consecutive failures </Accordion> <Accordion title="Signature verification failing"> - - Use the raw request body, not parsed JSON - Ensure you're using the - correct webhook secret - Check that the timestamp hasn't expired (5 minute - window) - Verify you're computing the HMAC correctly: `HMAC-SHA256(secret, - "${timestamp}.${rawBody}")` + - Use the raw request body, not parsed JSON + - Ensure you're using the correct webhook secret + - Check that the timestamp hasn't expired (5 minute window) + - Verify you're computing the HMAC correctly: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")` </Accordion>Apply similar formatting to all accordion items.
| // Usage | ||
| const signature = request.headers.get("X-UseSend-Signature"); | ||
| const timestamp = request.headers.get("X-UseSend-Timestamp"); | ||
|
|
||
| const isValid = verifyWebhook(secret, rawBody, signature, timestamp); | ||
| ``` |
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.
Handle potential null values from headers.get().
The headers.get() method can return null, but verifyWebhook expects string arguments. Users copying this example verbatim may encounter TypeScript errors or runtime issues.
Suggested fix
// Usage
const signature = request.headers.get("X-UseSend-Signature");
const timestamp = request.headers.get("X-UseSend-Timestamp");
+if (!signature || !timestamp) {
+ return new Response("Missing signature headers", { status: 400 });
+}
+
const isValid = verifyWebhook(secret, rawBody, signature, timestamp);🤖 Prompt for AI Agents
In `@apps/docs/guides/webhooks.mdx` around lines 256 - 261, The example uses
request.headers.get(...) which may return null but verifyWebhook(secret,
rawBody, signature, timestamp) requires strings; update the example around the
variables signature and timestamp to handle nulls before calling verifyWebhook —
e.g., retrieve them via request.headers.get("X-UseSend-Signature") and
request.headers.get("X-UseSend-Timestamp"), validate or coerce to string (or
throw/return an error if missing) so that the values passed to verifyWebhook are
non-null strings; ensure you reference the same variable names (signature,
timestamp, secret, rawBody, verifyWebhook) in the example.
| Signature computation: | ||
|
|
||
| ```typescript | ||
| const signature = HMAC - SHA256(secret, `${timestamp}.${JSON.stringify(body)}`); |
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.
Typo in signature computation pseudocode.
There are extraneous spaces in HMAC - SHA256 which should be HMAC-SHA256.
Suggested fix
-const signature = HMAC - SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);
+const signature = HMAC-SHA256(secret, `${timestamp}.${JSON.stringify(body)}`);📝 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 signature = HMAC - SHA256(secret, `${timestamp}.${JSON.stringify(body)}`); | |
| const signature = HMAC-SHA256(secret, `${timestamp}.${JSON.stringify(body)}`); |
🤖 Prompt for AI Agents
In `@references/webhook-architecture.md` at line 336, Fix the typo in the
signature pseudocode: replace the incorrect "HMAC - SHA256" with the correct
algorithm name "HMAC-SHA256" in the line that computes const signature; ensure
the call reads HMAC-SHA256(secret, `${timestamp}.${JSON.stringify(body)}`) so
the symbols signature, secret, timestamp, and body are used exactly as intended.
Summary by cubic
Adds first-class webhooks across the app with signed delivery, retries, logging, and a full dashboard UI. Also adds SDK support for signature verification with typed events.
New Features
Migration
Written for commit 20a74de. Summary will update on new commits.
Summary by CodeRabbit
New Features
Updates
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.