Skip to content

Conversation

steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Sep 3, 2025

Summary by CodeRabbit

  • New Features

    • Slack settings now offer partner.enrolled, commission.created, bounty.created, and bounty.updated subscription options.
  • Improvements

    • New partner-enrolled notifications with Partner ID and a direct “View on Dub” link.
    • Commission notifications show currency-formatted amounts, partner and optional customer details, and a quick view link.
    • Rich bounty notifications include truncated name/description, reward, type, duration, and a direct “View” link.
    • More consistent, event-specific Slack messaging.

Copy link
Contributor

coderabbitai bot commented Sep 3, 2025

Walkthrough

Refactors Slack message templates (renames and adds bounty support), strengthens commission/partner templates, extends Slack settings with new events, and updates the Singular webhook route to use Axiom, change GET/HEAD signatures, and add token/event/workspace validation and dispatching.

Changes

Cohort / File(s) Summary of Changes
Slack template refactor & bounty support
apps/web/lib/integrations/slack/transform.ts
Renames multiple internal templates (e.g., createLinkTemplatelinkTemplates, createLeadTemplateleadCreatedTemplate), adds bountyTemplates (renders truncated bounty name/description, reward, type, duration, view link), strengthens commission/partner templates (destructure currency/partner/customer, format amounts, add View links), uses truncate and updates template mapping and formatEventForSlack to detect/pass bounty events.
Slack settings event list update
apps/web/lib/integrations/slack/ui/settings.tsx
Appends new supported webhook events: partner.enrolled, commission.created, bounty.created, bounty.updated.
Singular webhook route & Axiom integration
apps/web/app/(ee)/api/singular/webhook/route.ts
Adds AxiomRequest and withAxiom; wraps GET with withAxiom (signature becomes GET = withAxiom(async (req: AxiomRequest) => { ... })), converts HEAD to synchronous, and adds token validation, env checks, event-name validation, workspace lookup, event mapping (lead/sale), and dispatch to tracking functions; unsupported events return OK; errors handled by existing error handler.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Source as Event Source
  participant Formatter as formatEventForSlack
  participant Templates as Template Selector
  participant Slack as Slack Webhook

  Source->>Formatter: Emit event (e.g., bounty.created / commission.created)
  Formatter->>Templates: Determine template (isBountyEvent? bountyTemplates : mapping)
  Templates-->>Formatter: Render Slack blocks (truncate, amounts, view links)
  Formatter->>Slack: POST message payload (blocks + view links)
  Slack-->>Source: Delivery response
Loading
sequenceDiagram
  autonumber
  actor HTTP as Incoming Request
  participant Axiom as withAxiom wrapper
  participant Route as GET handler (AxiomRequest)
  participant Auth as Token & env validation
  participant Workspace as Workspace lookup
  participant Dispatcher as Event dispatcher (tracking functions)

  HTTP->>Axiom: GET /singular/webhook?event=...
  Axiom->>Route: Invoke handler with AxiomRequest
  Route->>Auth: Validate token & event name
  Auth-->>Route: OK / Reject
  Route->>Workspace: Resolve workspace by token
  Workspace-->>Route: workspace
  Route->>Dispatcher: Map event -> tracking function (lead/sale) or return OK for unsupported
  Dispatcher-->>Route: result
  Route-->>HTTP: Response (200 OK / error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • TWilson023

Poem

I nibble keys beneath the moonlit dub,
I trim long names and tuck a bounty's grub.
Commissions shine with links to view,
Webhooks hum their routing through.
A happy hop — the rabbit's done the job. 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch slack-webhooks

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

vercel bot commented Sep 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 3, 2025 10:36pm

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/web/lib/integrations/slack/transform.ts (1)

15-21: Escape user-supplied text to avoid Slack mrkdwn breakage and accidental mentions

Several fields (names, emails, countries, referer, descriptions, IDs) are inserted directly into mrkdwn. Unescaped <, >, &, or | can break formatting; <!here>/<!channel> could also ping channels. Add a small escape helper and use it across templates.

Add once near the top of this file (outside the shown ranges):

const escapeMrkdwn = (s?: string | null) =>
  (s ?? "Unknown")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");

Also applies to: 54-60, 106-118, 155-160, 205-212, 247-252, 318-326

🧹 Nitpick comments (7)
apps/web/lib/integrations/slack/ui/settings.tsx (1)

21-24: Add type-safety and confirm backend trigger availability

Looks good functionally. To catch typos at compile time and ensure end-to-end wiring, type supportedEvents as WebhookTrigger[] (if ConfigureWebhook allows) and confirm these four events are present in WEBHOOK_TRIGGERS and processed server-side.

apps/web/lib/integrations/slack/transform.ts (6)

54-60: Harden link.clicked template: URL-encode params and escape fields

  • Encode domain/key in the URL.
  • Escape country and referer with the helper.
-  const linkToClicks = `${APP_DOMAIN}/events?event=clicks&domain=${link.domain}&key=${link.key}`;
+  const linkToClicks = `${APP_DOMAIN}/events?event=clicks&domain=${encodeURIComponent(link.domain)}&key=${encodeURIComponent(link.key)}`;
@@
-            text: `*Country*\n${click.country}`,
+            text: `*Country*\n${escapeMrkdwn(click.country)}`,
@@
-            text: `*Referrer*\n${click.referer}`,
+            text: `*Referrer*\n${escapeMrkdwn(click.referer)}`,

Also applies to: 70-77, 83-91, 97-101


110-118: Section block may not support both text and fields together; also escape/encode

Slack Section blocks commonly use either text or fields. To be safe, move the customer/email fields into the next section and escape text. Also URL-encode query params.

-  const linkToLeads = `${APP_DOMAIN}/events?event=leads&domain=${link.domain}&key=${link.key}`;
+  const linkToLeads = `${APP_DOMAIN}/events?event=leads&domain=${encodeURIComponent(link.domain)}&key=${encodeURIComponent(link.key)}`;
@@
-      {
-        type: "section",
-        text: {
-          type: "mrkdwn",
-          text: "*New lead created* :tada:",
-        },
-        fields: [
-          {
-            type: "mrkdwn",
-            text: `*Customer*\n${customer.name}`,
-          },
-          {
-            type: "mrkdwn",
-            text: `*Email*\n<mailto:${customer.email}|${customer.email}>`,
-          },
-        ],
-      },
+      {
+        type: "section",
+        text: { type: "mrkdwn", text: "*New lead created* :tada:" },
+      },
@@
-          {
-            type: "mrkdwn",
-            text: `*Country*\n${click.country}`,
-          },
+          { type: "mrkdwn", text: `*Customer*\n${escapeMrkdwn(customer.name)}` },
+          { type: "mrkdwn", text: `*Email*\n<mailto:${customer.email}|${escapeMrkdwn(customer.email)}>` },
+          { type: "mrkdwn", text: `*Country*\n${escapeMrkdwn(click.country)}` },
           {
             type: "mrkdwn",
             text: `*Short link*\n<${link.shortLink}|${link.shortLink}>`,
           },

Also applies to: 118-127, 130-141, 146-149


155-160: Use currencyFormatter for sales and escape text

  • Prefer currencyFormatter for locale-aware currency with proper symbols.
  • Encode query params and escape customer/country text.
-  const amountInDollars = (sale.amount / 100).toFixed(2);
-  const linkToSales = `${APP_DOMAIN}/events?event=sales&domain=${link.domain}&key=${link.key}`;
+  const formattedAmount = currencyFormatter(sale.amount / 100, { currency: sale.currency.toUpperCase() });
+  const linkToSales = `${APP_DOMAIN}/events?event=sales&domain=${encodeURIComponent(link.domain)}&key=${encodeURIComponent(link.key)}`;
@@
-            text: `*Customer*\n${customer.name}`,
+            text: `*Customer*\n${escapeMrkdwn(customer.name)}`,
@@
-            text: `*Email*\n<mailto:${customer.email}|${customer.email}>`,
+            text: `*Email*\n<mailto:${customer.email}|${escapeMrkdwn(customer.email)}>`,
@@
-            text: `*Country*\n${click.country}`,
+            text: `*Country*\n${escapeMrkdwn(click.country)}`,
@@
-            text: `*Amount*\n${amountInDollars} ${sale.currency.toUpperCase()}`,
+            text: `*Amount*\n${formattedAmount}`,

Also applies to: 169-176, 183-190, 196-199


226-235: Escape partner fields

Protect mrkdwn formatting for name/email/country.

-            text: `*Name*\n${name}`,
+            text: `*Name*\n${escapeMrkdwn(name)}`,
@@
-            text: `*Email*\n<mailto:${email}|${email}>`,
+            text: `*Email*\n<mailto:${email}|${escapeMrkdwn(email)}>`,
@@
-            text: `*Country*\n${country}`,
+            text: `*Country*\n${escapeMrkdwn(country)}`,

267-274: Escape commission fields; keep currency logic as-is

Escape partner/customer text. Currency handling looks solid.

-            text: `*Partner*\n${partner.name}`,
+            text: `*Partner*\n${escapeMrkdwn(partner.name)}`,
@@
-            text: `*Email*\n<mailto:${partner.email}|${partner.email}>`,
+            text: `*Email*\n<mailto:${partner.email}|${escapeMrkdwn(partner.email)}>`,
@@
-                  text: `*Customer*\n${customer.name}`,
+                  text: `*Customer*\n${escapeMrkdwn(customer.name)}`,
@@
-                  text: `*Customer Email*\n<mailto:${customer.email}|${customer.email}>`,
+                  text: `*Customer Email*\n<mailto:${customer.email}|${escapeMrkdwn(customer.email)}>`,

Also applies to: 276-287, 288-305, 306-312


395-405: Type the template map for stronger compile-time guarantees

Current Record<WebhookTrigger, any> loses arg/result types. Consider a discriminated union for args or a shared TemplateArgs type to ensure each handler receives the correct shape.

Example (outside this hunk):

type BaseArgs<T> = { data: T; event?: WebhookTrigger };
type TemplateFn<T> = (args: BaseArgs<T>) => { blocks: any[] };

const slackTemplates: {
  "link.created": TemplateFn<LinkWebhookEvent["data"]>;
  "link.updated": TemplateFn<LinkWebhookEvent["data"]>;
  "link.deleted": TemplateFn<LinkWebhookEvent["data"]>;
  "link.clicked": TemplateFn<ClickEventWebhookPayload>;
  "lead.created": TemplateFn<LeadEventWebhookPayload>;
  "sale.created": TemplateFn<SaleEventWebhookPayload>;
  "partner.enrolled": TemplateFn<PartnerEventWebhookPayload>;
  "commission.created": TemplateFn<CommissionEventWebhookPayload>;
  "bounty.created": TemplateFn<BountyEventWebhookPayload>;
  "bounty.updated": TemplateFn<BountyEventWebhookPayload>;
} = { /* ... */ };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 79116bf and 46226f7.

📒 Files selected for processing (2)
  • apps/web/lib/integrations/slack/transform.ts (8 hunks)
  • apps/web/lib/integrations/slack/ui/settings.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/integrations/slack/transform.ts (3)
apps/web/lib/webhook/types.ts (5)
  • ClickEventWebhookPayload (15-15)
  • LeadEventWebhookPayload (17-17)
  • SaleEventWebhookPayload (19-19)
  • BountyEventWebhookPayload (27-27)
  • WebhookTrigger (13-13)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (5-16)
packages/utils/src/constants/main.ts (1)
  • APP_DOMAIN (13-18)
⏰ 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: Vade Review
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/lib/integrations/slack/transform.ts (1)

423-426: LGTM: event-aware dispatch preserves existing behavior

Passing event only for templates that need it keeps signatures minimal and avoids churn.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
apps/web/app/(ee)/api/singular/webhook/route.ts (1)

62-69: Add event_name validation schema for better error handling.

While the code checks for the presence of event_name, it would be more consistent to add it to the schema validation along with the auth parameters.

Consider adding event_name to the validation schema:

 const authSchema = z.object({
   dub_token: z
     .string()
     .min(1, "dub_token is required")
     .describe("Global token to identify Singular events."),
   dub_workspace_id: z
     .string()
     .min(1, "dub_workspace_id is required")
     .describe(
       "The Singular advertiser's workspace ID on Dub (see https://d.to/id).",
     )
     .transform((v) => normalizeWorkspaceId(v)),
+  event_name: z
+    .string()
+    .min(1, "event_name is required")
+    .describe("The Singular event name to process."),
 });

Then update the parsing logic:

-    const { dub_token: token, dub_workspace_id: workspaceId } =
+    const { dub_token: token, dub_workspace_id: workspaceId, event_name: eventName } =
       authSchema.parse(queryParams);
 
     if (token !== singularWebhookToken) {
       throw new DubApiError({
         code: "unauthorized",
         message: "Invalid Singular webhook token. Skipping event processing.",
       });
     }
 
-    const { event_name: eventName } = queryParams;
-
-    if (!eventName) {
-      throw new DubApiError({
-        code: "bad_request",
-        message: "event_name is required.",
-      });
-    }
apps/web/lib/integrations/slack/transform.ts (1)

205-240: Consider escaping user-controlled text in partner template.

Similar to the bounty template, the partner name should be escaped to prevent Slack markdown issues.

           {
             type: "mrkdwn",
-            text: `*Name*\n${name}`,
+            text: `*Name*\n${escapeMrkdwn(name)}`,
           },
♻️ Duplicate comments (2)
apps/web/lib/integrations/slack/transform.ts (2)

318-392: Escape user-controlled text and improve date formatting for bounty templates.

The bounty template doesn't escape user-controlled text (name, description) which could break Slack's markdown formatting if they contain special characters. Also, date formatting should be consistent and explicit.

Add an escape function and apply proper formatting:

+// Add this helper function at the top of the file
+const escapeMrkdwn = (text: string): string => {
+  return text
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;");
+};

 const bountyTemplates = ({
   data,
   event,
 }: {
   data: BountyEventWebhookPayload;
   event: WebhookTrigger;
 }) => {
   const { id, name, description, rewardAmount, type, startsAt, endsAt } = data;

   const eventMessages = {
     "bounty.created": "*New bounty created* :money_with_wings:",
     "bounty.updated": "*Bounty updated* :memo:",
   };

   const formattedReward = currencyFormatter(rewardAmount / 100);
   const linkToBounty = `${APP_DOMAIN}/bounties/${id}`;
+  const escapedName = escapeMrkdwn(truncate(name, 140) || "Untitled Bounty");
+  const truncatedDescription = description ? truncate(description, 2800) : null;
+  const escapedDescription = truncatedDescription ? escapeMrkdwn(truncatedDescription) : null;

   return {
     blocks: [
       {
         type: "section",
         text: {
           type: "mrkdwn",
           text: eventMessages[event],
         },
       },
       {
         type: "section",
         fields: [
           {
             type: "mrkdwn",
-            text: `*Bounty Name*\n${truncate(name, 140) || "Untitled Bounty"}`,
+            text: `*Bounty Name*\n${escapedName}`,
           },
           {
             type: "mrkdwn",
             text: `*Reward Amount*\n${formattedReward}`,
           },
         ],
       },
       {
         type: "section",
         fields: [
           {
             type: "mrkdwn",
             text: `*Type*\n${type.charAt(0).toUpperCase() + type.slice(1)}`,
           },
           {
             type: "mrkdwn",
-            text: `*Duration*\n${new Date(startsAt).toLocaleDateString()}${endsAt ? ` - ${new Date(endsAt).toLocaleDateString()}` : " (No end date)"}`,
+            text: `*Duration*\n${new Date(startsAt).toLocaleDateString("en-US", { dateStyle: "medium" })}${endsAt ? ` - ${new Date(endsAt).toLocaleDateString("en-US", { dateStyle: "medium" })}` : " (No end date)"}`,
           },
         ],
       },
-      ...(description
+      ...(escapedDescription
         ? [
             {
               type: "section",
               text: {
                 type: "mrkdwn",
-                text: `*Description*\n${truncate(description, 140)}`,
+                text: `*Description*\n${escapedDescription}`,
               },
             },
           ]
         : []),
       {
         type: "context",
         elements: [
           {
             type: "mrkdwn",
             text: `<${linkToBounty}|View bounty on Dub>`,
           },
         ],
       },
     ],
   };
 };

362-362: Add null check for type field to prevent runtime errors.

The code assumes type is always defined, but should handle the case where it might be null or undefined.

-            text: `*Type*\n${type.charAt(0).toUpperCase() + type.slice(1)}`,
+            text: `*Type*\n${type ? type.charAt(0).toUpperCase() + type.slice(1) : "N/A"}`,
🧹 Nitpick comments (2)
apps/web/app/(ee)/api/singular/webhook/route.ts (1)

71-77: Consider returning 200 OK instead of logging errors for unsupported events.

The current approach logs an error to console but returns a 200 OK response. For webhook handlers, it's generally better to silently acknowledge unsupported events without logging errors, as they're expected behavior.

     if (!supportedEvents.includes(eventName)) {
-      console.error(
-        `Event ${eventName} is not supported by Singular <> Dub integration.`,
-      );
+      // Silently ignore unsupported events - this is expected behavior
       return NextResponse.json("OK");
     }
apps/web/lib/integrations/slack/transform.ts (1)

349-349: Consider using a larger truncation limit for better UX.

The current truncation limit of 140 characters for bounty names and descriptions is quite restrictive. Slack can handle much longer text in message blocks.

-            text: `*Bounty Name*\n${truncate(name, 140) || "Untitled Bounty"}`,
+            text: `*Bounty Name*\n${truncate(name, 500) || "Untitled Bounty"}`,
-                text: `*Description*\n${truncate(description, 140)}`,
+                text: `*Description*\n${truncate(description, 2800)}`,

Also applies to: 376-376

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 46226f7 and 31077eb.

📒 Files selected for processing (2)
  • apps/web/app/(ee)/api/singular/webhook/route.ts (3 hunks)
  • apps/web/lib/integrations/slack/transform.ts (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T06:41:45.620Z
Learnt from: devkiran
PR: dubinc/dub#2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.

Applied to files:

  • apps/web/app/(ee)/api/singular/webhook/route.ts
🧬 Code graph analysis (2)
apps/web/lib/integrations/slack/transform.ts (3)
apps/web/lib/webhook/types.ts (2)
  • BountyEventWebhookPayload (27-27)
  • WebhookTrigger (13-13)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (5-16)
packages/utils/src/constants/main.ts (1)
  • APP_DOMAIN (13-18)
apps/web/app/(ee)/api/singular/webhook/route.ts (1)
apps/web/app/(ee)/api/events/export/route.ts (1)
  • GET (45-136)
🔇 Additional comments (2)
apps/web/app/(ee)/api/singular/webhook/route.ts (1)

7-7: LGTM! Proper integration of Next.js Axiom for observability.

The addition of Axiom for request logging and observability is correctly implemented with the withAxiom wrapper.

Also applies to: 40-40, 118-118

apps/web/lib/integrations/slack/transform.ts (1)

247-251: LGTM! Proper currency handling in commission template.

The commission template correctly uses the currency from the data payload and passes it to the currency formatter.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
apps/web/lib/integrations/slack/transform.ts (2)

155-204: Use currencyFormatter with currency for sales amounts.

toFixed(2) breaks for zero-decimal currencies (e.g., JPY) and locale formatting. Use the shared formatter.

-  const amountInDollars = (sale.amount / 100).toFixed(2);
+  const formattedAmount = currencyFormatter(sale.amount / 100, {
+    currency: sale.currency?.toUpperCase?.() || "USD",
+  });
@@
-          {
-            type: "mrkdwn",
-            text: `*Amount*\n${amountInDollars} ${sale.currency.toUpperCase()}`,
-          },
+          {
+            type: "mrkdwn",
+            text: `*Amount*\n${formattedAmount}`,
+          },

257-326: Currency normalization and text escaping in commissions.

  • Normalize currency to uppercase to avoid Intl errors.
  • Escape partner/customer names.
-  const { id, amount, earnings, currency, partner, customer } = data;
+  const { id, amount, earnings, currency, partner, customer } = data;
+  const cur = (currency || "USD").toUpperCase();
@@
-  const formattedAmount = currencyFormatter(amount / 100, { currency });
-  const formattedEarnings = currencyFormatter(earnings / 100, { currency });
+  const formattedAmount = currencyFormatter(amount / 100, { currency: cur });
+  const formattedEarnings = currencyFormatter(earnings / 100, { currency: cur });
@@
-            text: `*Partner*\n${partner.name}`,
+            text: `*Partner*\n${escapeMrkdwn(partner.name)}`,
@@
-      ...(customer
+      ...(customer
         ? [
             {
               type: "section",
               fields: [
                 {
                   type: "mrkdwn",
-                  text: `*Customer*\n${customer.name}`,
+                  text: `*Customer*\n${escapeMrkdwn(customer.name)}`,
                 },
♻️ Duplicate comments (2)
apps/web/lib/integrations/slack/transform.ts (2)

1-1: Escape Slack text and standardize date formatting (repeat of prior ask).

User-controlled strings (names, descriptions, types) should be mrkdwn-escaped; dates should use a consistent locale. Add an escape helper and use it in templates; prefer toLocaleDateString("en-US", { dateStyle: "medium" }).

Add once near the top (outside the diff range):

// Minimal Slack mrkdwn escape for &, <, >
const escapeMrkdwn = (s: string) =>
  (s ?? "").replace(/[&<>]/g, (ch) =>
    ch === "&" ? "&amp;" : ch === "<" ? "&lt;" : "&gt;",
  );

328-402: Bounty template: guard nulls, escape text, and standardize dates.

type, startsAt, endsAt, and description may be null/undefined; current code can throw (type.charAt) or output “Invalid Date”.

 const bountyTemplates = ({
   data,
   event,
 }: {
   data: BountyEventWebhookPayload;
   event: WebhookTrigger;
 }) => {
   const { id, name, description, rewardAmount, type, startsAt, endsAt } = data;
 
   const eventMessages = {
     "bounty.created": "*New bounty created* :money_with_wings:",
     "bounty.updated": "*Bounty updated* :memo:",
   };
 
-  const formattedReward = currencyFormatter(rewardAmount / 100);
+  const formattedReward = currencyFormatter(rewardAmount / 100);
   const linkToBounty = `${APP_DOMAIN}/program/bounties/${id}`;
+  const safeName = escapeMrkdwn(truncate(name || "Untitled Bounty", 140));
+  const safeType = type
+    ? `${type.slice(0, 1).toUpperCase()}${type.slice(1)}`
+    : "N/A";
+  const formattedStart = startsAt
+    ? new Date(startsAt).toLocaleDateString("en-US", { dateStyle: "medium" })
+    : "N/A";
+  const formattedEnd = endsAt
+    ? new Date(endsAt).toLocaleDateString("en-US", { dateStyle: "medium" })
+    : undefined;
+  const safeDescription =
+    description && description.length > 0
+      ? escapeMrkdwn(truncate(description, 2800))
+      : undefined;
@@
           {
             type: "mrkdwn",
-            text: `*Bounty Name*\n${truncate(name, 140) || "Untitled Bounty"}`,
+            text: `*Bounty Name*\n${safeName}`,
           },
@@
           {
             type: "mrkdwn",
-            text: `*Type*\n${type.charAt(0).toUpperCase() + type.slice(1)}`,
+            text: `*Type*\n${safeType}`,
           },
           {
             type: "mrkdwn",
-            text: `*Duration*\n${new Date(startsAt).toLocaleDateString()}${endsAt ? ` - ${new Date(endsAt).toLocaleDateString()}` : " (No end date)"}`,
+            text: `*Duration*\n${
+              formattedEnd ? `${formattedStart} - ${formattedEnd}` : `${formattedStart} (No end date)`
+            }`,
           },
         ],
       },
-      ...(description
+      ...(safeDescription
         ? [
             {
               type: "section",
               text: {
                 type: "mrkdwn",
-                text: `*Description*\n${truncate(description, 140)}`,
+                text: `*Description*\n${safeDescription}`,
               },
             },
           ]
         : []),

If bounties support non-USD currency, pass it to currencyFormatter; otherwise defaults to USD.

#!/bin/bash
# Does the bounty payload include a currency field?
rg -nP --type=ts -C2 'Bounty(Schema|EventWebhookPayload).*currency' apps/web
🧹 Nitpick comments (2)
apps/web/lib/integrations/slack/transform.ts (2)

205-250: Partner fields: avoid “undefined” output and escape names.

Use safe fallbacks for optional properties and escape text.

   const { name, email, country, partnerId } = data;
   const linkToPartner = `${APP_DOMAIN}/program/partners?partnerId=${partnerId}`;
+  const safeName = escapeMrkdwn(name || "Unknown");
+  const safeEmail = email ? `<mailto:${email}|${email}>` : "—";
+  const safeCountry = country || "—";
@@
           {
             type: "mrkdwn",
-            text: `*Name*\n${name}`,
+            text: `*Name*\n${safeName}`,
           },
           {
             type: "mrkdwn",
-            text: `*Email*\n<mailto:${email}|${email}>`,
+            text: `*Email*\n${safeEmail}`,
           },
           {
             type: "mrkdwn",
-            text: `*Country*\n${country}`,
+            text: `*Country*\n${safeCountry}`,
           },

54-104: Null-safe country and referer fields

Default to "—" when these values are nullish, and use the referer property (single r) as defined in the schema:

           {
             type: "mrkdwn",
-            text: `*Country*\n${click.country}`,
+            text: `*Country*\n${click.country ?? "—"}`,
           },
           {
             type: "mrkdwn",
-            text: `*Referrer*\n${click.referer}`,
+            text: `*Referrer*\n${click.referer ?? "—"}`,
           },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 31077eb and 2289067.

📒 Files selected for processing (1)
  • apps/web/lib/integrations/slack/transform.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/integrations/slack/transform.ts (3)
apps/web/lib/webhook/types.ts (7)
  • ClickEventWebhookPayload (15-15)
  • LeadEventWebhookPayload (17-17)
  • SaleEventWebhookPayload (19-19)
  • PartnerEventWebhookPayload (21-21)
  • CommissionEventWebhookPayload (23-25)
  • BountyEventWebhookPayload (27-27)
  • WebhookTrigger (13-13)
packages/utils/src/constants/main.ts (1)
  • APP_DOMAIN (13-18)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (5-16)
⏰ 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: Vade Review
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/lib/integrations/slack/transform.ts (4)

404-415: Template mapping looks good.

New events are wired to the corresponding templates.


417-436: Dispatcher logic is clear.

Passing event only where needed (link/bounty) keeps payloads lean.


106-153: No changes needed: Section block supports both text and fields
Confirmed via Slack Block Kit docs that a section block may include both a text object and a fields array simultaneously.


15-52: Remove unnecessary conditional for data.url
The Zod linkEventSchema defines url as a non-nullable string (even on deletes) and the sample link-deleted.json always includes a url field. data.url won’t be undefined, so the suggested conditional spread isn’t needed.

Likely an incorrect or invalid review comment.

@steven-tey steven-tey merged commit fc84027 into main Sep 3, 2025
9 checks passed
@steven-tey steven-tey deleted the slack-webhooks branch September 3, 2025 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant