+
Skip to content

Conversation

steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Aug 9, 2025

Summary by CodeRabbit

  • New Features

    • Client-side tracking endpoints for lead and sale conversions (plan-restricted) and CORS preflight support.
  • API

    • Publishable-key authentication with rate limiting and plan enforcement; centralized CORS headers for client APIs.
  • UI

    • Publishable Key management UI (generate/regenerate/revoke) added to analytics settings; improved allowed-hostnames form with confirm modal.
  • Database

    • Added unique publishableKey per project; removed SSO flag.
  • Schema/Types

    • Workspace schemas and types include publishableKey.
  • Bug Fixes

    • Clarified analytics log messages.

Copy link
Contributor

vercel bot commented Aug 9, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Aug 21, 2025 4:50am

Copy link
Contributor

coderabbitai bot commented Aug 9, 2025

Walkthrough

Adds client-side tracking endpoints (lead, sale) protected by a new publishable-key middleware, introduces publishableKey on Project plus UI to manage it, updates workspace PATCH and Zod/types, and centralizes COMMON_CORS_HEADERS across multiple API routes.

Changes

Cohort / File(s) Change Summary
Client Lead Tracking API
apps/web/app/(ee)/api/track/lead/client/route.ts
New POST route (wrapped with withPublishableKey) that validates origin against workspace.allowedHostnames, parses trackLeadRequestSchema, calls trackLead, and returns JSON; adds OPTIONS responding with COMMON_CORS_HEADERS.
Client Sale Tracking API
apps/web/app/(ee)/api/track/sale/client/route.ts
New POST route (wrapped with withPublishableKey) that validates origin, parses trackSaleRequestSchema (requires customerExternalId), calls trackSale, and returns JSON; adds OPTIONS responding with COMMON_CORS_HEADERS.
Publishable Key Middleware
apps/web/lib/auth/publishable-key.ts
New withPublishableKey HOF: extracts Bearer publishable key, enforces rate limit (~600/min), resolves Project/workspace by key, enforces allowed plans, passes workspace and search params to handler; errors handled centrally and wrapped with logging.
DB Schema: Project
packages/prisma/schema/workspace.prisma
Added optional unique publishableKey String? @unique; removed ssoEnabled field.
Workspace PATCH API & Zod schemas
apps/web/app/api/workspaces/[idOrSlug]/route.ts,
apps/web/lib/zod/schemas/workspaces.ts
PATCH route now accepts optional publishableKey and conditionally updates it; updateWorkspaceSchema and WorkspaceSchemaExtended extended to include validated/nullable publishableKey.
Workspace types
apps/web/lib/types.ts
Added `publishableKey: string
Publishable Key UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx
New client component to display/generate/regenerate/revoke publishable keys using PATCH calls, with confirmation modals, toasts, and mutate.
Analytics settings page & hostname UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/page.tsx,
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx
Replaced AllowedHostnamesAllowedHostnamesForm, added PublishableKeyForm to settings page, adjusted imports, and refactored hostname deletion to use a confirm modal.
Centralized CORS
apps/web/lib/api/cors.ts, plus many routes under apps/web/app/(ee)/api/... and apps/web/app/api/oauth/token/route.ts
Added exported COMMON_CORS_HEADERS and replaced local CORS header objects across multiple tracking and OAuth token routes.
Other track endpoints (small behavior/log/text changes)
apps/web/app/(ee)/api/track/{click,open,visit,lead,route.ts} and apps/web/lib/analytics/verify-analytics-allowed-hostnames.ts
Replaced local CORS headers with COMMON_CORS_HEADERS. lead route removed default mode = "async". Log messages in hostname verification updated wording ("Event not recorded …").

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Route as /api/track/{lead|sale}/client
    participant Middleware as withPublishableKey
    participant DB as Project DB
    participant Tracker as trackLead/trackSale

    Client->>Route: POST (body) + Authorization: Bearer <pub_key>
    Route->>Middleware: invoke wrapped handler
    Middleware->>DB: find Project by publishableKey
    DB-->>Middleware: Project (plan, allowedHostnames, publishableKey)
    Middleware->>Middleware: rate-limit check (600/min)
    alt allowed host & plan OK
        Middleware-->>Route: call handler with workspace
        Route->>Route: parse body, verify hostname (verifyAnalyticsAllowedHostnames)
        Route->>Tracker: call trackLead/trackSale(workspace, rawBody, fields)
        Tracker-->>Route: result
        Route-->>Client: 200 JSON + COMMON_CORS_HEADERS
    else forbidden/invalid/ratelimit
        Middleware-->>Client: 401/403/429 or error JSON + COMMON_CORS_HEADERS
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Singular integration #2637 — Overlaps client tracking surface and publishable-key handling; likely touches similar tracking wiring and middleware usage.
  • Improve track click #2562 — Related analytics hostname verification and client-side tracking changes; may overlap on helpers like getHostnameFromRequest and verifyAnalyticsAllowedHostnames.
  • Add click not record logs #2545 — Adjusts log wording in verify-analytics-allowed-hostnames; touches the same file and messaging changes.

Suggested reviewers

  • devkiran

Poem

I’m a rabbit with a tiny key,
I hop through routes so merrily,
I guard the plans and count each click,
I copy keys and do it quick.
Hop, track, and vault — a publishable glee! 🐇🔑

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

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

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 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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
packages/prisma/schema/workspace.prisma (1)

42-42: Clarify nullability, rotation, and backfill strategy for publishableKey

publishableKey is optional with a unique constraint. Confirm intent:

  • If all projects using client APIs must have a key, consider making it non-nullable and backfilling existing rows.
  • If optional by design, document/handle “no key” behavior in middleware.

Also consider:

  • Enforcing a bounded length (e.g., @db.VarChar(64/128)) to keep index sizes predictable.
  • Establishing a rotation/revocation story (single key vs. multiple keys table).

I can help generate a safe migration/backfill plan if needed.

Would you like me to draft a migration that generates keys for existing projects and adds a uniqueness-preserving index with length bounds?

apps/web/lib/auth/publishable-key.ts (1)

98-101: Consider adding WWW-Authenticate for 401 responses

For better client UX, include WWW-Authenticate: Bearer on 401 Unauthorized responses. If handleAndReturnErrorResponse supports custom headers, set it there for code: "unauthorized" errors.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b2dce5 and 16e09d0.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/track/lead/client/route.ts (1 hunks)
  • apps/web/lib/auth/publishable-key.ts (1 hunks)
  • packages/prisma/schema/workspace.prisma (1 hunks)
⏰ 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: build
🔇 Additional comments (2)
apps/web/app/(ee)/api/track/lead/client/route.ts (2)

7-9: Add OPTIONS for CORS preflight (client-side endpoint)

Browsers will preflight POST application/json with OPTIONS. Provide an OPTIONS handler to return 204 and appropriate CORS headers. Consider reflecting and validating Origin against workspace.allowedHostnames.

Example:

export const OPTIONS = () =>
  new Response(null, {
    status: 204,
    headers: {
      "Access-Control-Allow-Origin": "*", // or reflect validated origin
      "Access-Control-Allow-Methods": "POST, OPTIONS",
      "Access-Control-Allow-Headers": "Authorization, Content-Type",
      "Access-Control-Max-Age": "600",
    },
  });

Would you like me to wire this with allowed hostnames validation?


10-11: No changes needed for rawBody handling

Upon inspection, parseRequestBody(req) returns the parsed JSON and trackLead’s rawBody parameter is never used for signature or checksum verification—it's simply carried through to the conversion logic. Passing the parsed object into rawBody matches the current implementation and does not break any behavior.

Comment on lines +35 to +43
requiredPlan: [
"business",
"business plus",
"business extra",
"business max",
"advanced",
"enterprise",
],
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid plan list duplication; centralize allowed plans

This list is duplicated here and in middleware defaults. Centralize in a single constants module (e.g., @/lib/plans) to prevent drift.

If plan matching should be case-insensitive, normalize here instead of repeating that logic across callers.

🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/track/lead/client/route.ts around lines 35 to 43, the
requiredPlan array is duplicated in middleware defaults. To fix this, move the
plan list to a shared constants module like '@/lib/plans' and import it here.
Also, if plan matching needs to be case-insensitive, normalize the plan names
once in that module rather than repeating normalization logic in multiple
places.

Comment on lines +56 to +66
const { success, limit, reset, remaining } = await ratelimit(
600,
"1 m",
).limit(publishableKey);

headers = {
"Retry-After": reset.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return rate limit headers on both success and limit-exceeded responses

You compute rate-limit metadata but only return headers on 429. Surfacing them on success helps clients throttle themselves.

-          const { success, limit, reset, remaining } = await ratelimit(
+          const { success, limit, reset, remaining } = await ratelimit(
             600,
             "1 m",
           ).limit(publishableKey);

-          headers = {
+          rateLimitHeaders = {
             "Retry-After": reset.toString(),
             "X-RateLimit-Limit": limit.toString(),
             "X-RateLimit-Remaining": remaining.toString(),
             "X-RateLimit-Reset": reset.toString(),
           };

           if (!success) {
             return new Response("Too many requests.", {
               status: 429,
-              headers,
+              headers: rateLimitHeaders,
             });
           }

And after invoking the handler (see separate comment), set these headers on the success response as well.

Also applies to: 68-74

🤖 Prompt for AI Agents
In apps/web/lib/auth/publishable-key.ts around lines 56 to 66 and 68 to 74, the
rate limit headers are only set on 429 responses but should also be included on
successful responses. Modify the code to set the "Retry-After",
"X-RateLimit-Limit", "X-RateLimit-Remaining", and "X-RateLimit-Reset" headers on
the success response after invoking the handler, ensuring clients receive rate
limit metadata regardless of success or failure.

@steven-tey steven-tey marked this pull request as ready for review August 21, 2025 01:03
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: 11

Caution

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

⚠️ Outside diff range comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (2)

55-61: Wildcard validation accepts invalid inputs like "*."

isValidHostname returns true for ".","..com", etc. Tighten validation by checking the suffix after "*." with validDomainRegex.

Apply this diff:

-  const isValidHostname = (hostname: string) => {
-    return (
-      validDomainRegex.test(hostname) ||
-      hostname === "localhost" ||
-      hostname.startsWith("*.")
-    );
-  };
+  const isValidHostname = (hostname: string) => {
+    const value = hostname.trim().toLowerCase();
+    if (value === "localhost") return true;
+    if (value.startsWith("*.")) {
+      // validate the remainder after "*." as a domain
+      const rest = value.slice(2);
+      return rest.length > 0 && validDomainRegex.test(rest);
+    }
+    return validDomainRegex.test(value);
+  };

63-97: Normalize input and de-duplicate case-insensitively; guard on permissions; add error handling

  • Normalize to lowercase/trim before validation and storage to avoid duplicates like "Example.com" vs "example.com".
  • Short-circuit when permissionsError is present.
  • Wrap network calls in try/catch to handle non-JSON errors.

Apply this diff:

   const addHostname = async () => {
-    if (allowedHostnames?.includes(hostname)) {
+    if (permissionsError) {
+      toast.error(permissionsError);
+      return;
+    }
+
+    const normalized = hostname.trim().toLowerCase();
+
+    if (allowedHostnames?.some((h) => h.toLowerCase() === normalized)) {
       toast.error("Hostname already exists.");
       return;
     }
 
-    if (!isValidHostname(hostname)) {
+    if (!isValidHostname(normalized)) {
       toast.error("Enter a valid domain.");
       return;
     }
 
     setProcessing(true);
 
-    const response = await fetch(`/api/workspaces/${id}`, {
-      method: "PATCH",
-      headers: {
-        "Content-Type": "application/json",
-      },
-      body: JSON.stringify({
-        allowedHostnames: [...(allowedHostnames || []), hostname],
-      }),
-    });
-
-    if (response.ok) {
-      toast.success("Hostname added.");
-    } else {
-      const { error } = await response.json();
-      toast.error(error.message);
-    }
+    try {
+      const response = await fetch(`/api/workspaces/${id}`, {
+        method: "PATCH",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          allowedHostnames: [...(allowedHostnames || []), normalized],
+        }),
+      });
+      if (response.ok) {
+        toast.success("Hostname added.");
+      } else {
+        const data = await response.json().catch(() => ({}));
+        toast.error(data?.error?.message ?? "Failed to add hostname.");
+      }
+    } catch {
+      toast.error("Network error while adding hostname.");
+    }
 
     mutate();
     setProcessing(false);
-    setHostname("");
+    setHostname("");
   };
apps/web/app/api/workspaces/[idOrSlug]/route.ts (1)

116-125: Potentially updates defaultWorkspace to undefined when slug is not provided

If slug is undefined, the condition slug !== workspace.slug is true and the code sets defaultWorkspace to undefined for many users. Guard on slug being truthy before updating.

Apply this diff:

-      if (slug !== workspace.slug) {
+      if (slug && slug !== workspace.slug) {
         await prisma.user.updateMany({
           where: {
             defaultWorkspace: workspace.slug,
           },
           data: {
             defaultWorkspace: slug,
           },
         });
       }
🧹 Nitpick comments (5)
apps/web/lib/types.ts (1)

179-189: Redundant redeclaration of publishableKey on ExtendedWorkspaceProps

WorkspaceProps extends Prisma’s Project, which already includes publishableKey (String? → string | null). Redeclaring it here is unnecessary and risks future type drift if the Prisma field changes.

Apply this diff to rely on the base type:

 export interface ExtendedWorkspaceProps extends WorkspaceProps {
   domains: (WorkspaceProps["domains"][number] & {
     linkRetentionDays: number | null;
   })[];
   defaultProgramId: string | null;
   allowedHostnames: string[];
   users: (WorkspaceProps["users"][number] & {
     workspacePreferences?: z.infer<typeof workspacePreferencesSchema>;
   })[];
-  publishableKey: string | null;
 }
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (2)

98-129: Avoid double-submission; wire Button as submit and disable on processing/permissions

Form onSubmit already calls addHostname(). If Button is type="submit", the onClick handler can cause duplicate calls. Also disable the button on processing and when permission is missing.

Apply this diff:

-      <Button
-        text="Add Hostname"
-        variant="primary"
-        onClick={addHostname}
-        disabled={!isValidHostname(hostname) || hostname.length === 0}
-        loading={processing}
-        className="w-fit"
-        disabledTooltip={permissionsError || undefined}
-      />
+      <Button
+        text="Add Hostname"
+        variant="primary"
+        type="submit"
+        disabled={
+          processing ||
+          !!permissionsError ||
+          !isValidHostname(hostname.trim().toLowerCase())
+        }
+        loading={processing}
+        className="w-fit"
+        disabledTooltip={permissionsError || undefined}
+      />

187-203: Disable Delete action during processing and when permission is missing

Prevent repeated PATCH requests and respect permission errors by disabling the button.

Apply this diff:

         <Button
           variant="outline"
           className={cn(
             "h-8 px-1.5 outline-none transition-all duration-200",
             "border-transparent data-[state=open]:border-neutral-500 sm:group-hover/card:data-[state=closed]:border-neutral-200",
           )}
           icon={
             processing ? (
               <LoadingSpinner className="size-4 shrink-0" />
             ) : (
               <Trash className="size-4" />
             )
           }
           onClick={deleteHostname}
+          disabled={processing || !!permissionsError}
           disabledTooltip={permissionsError || undefined}
         />
apps/web/app/(ee)/api/track/sale/client/route.ts (1)

24-29: Prefer schema-level validation over imperative check

customerExternalId emptiness should be enforced in trackSaleRequestSchema (e.g., .min(1)) rather than checked ad hoc here. This removes dead code paths and keeps validation centralized.

Apply this diff after updating the schema as suggested below:

-    if (!customerExternalId) {
-      throw new DubApiError({
-        code: "bad_request",
-        message: "customerExternalId is required",
-      });
-    }

Update the schema (apps/web/lib/zod/schemas/sales.ts) to ensure non-empty values:

// apps/web/lib/zod/schemas/sales.ts
customerExternalId: z
  .string()
  .trim()
  .min(1, "customerExternalId is required")
  .max(100)
  .describe("..."),
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx (1)

25-26: Consider centralizing publishable key generation (optional)

Current setup

  • Client generates a key with dub_pk_${nanoid(24)} in
    apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx:25
  • Server’s Zod schema (apps/web/lib/zod/schemas/workspaces.ts:150) simply allows any string for publishableKey
  • Auth middleware (apps/web/lib/auth/publishable-key.ts:54) only checks that the key starts with "dub_pk_"

Because format logic (client) and validation logic (server) live in separate places, there’s a risk of drift if you ever change the key contract. To keep everything in sync, you might:

  • Move key generation to the server-side endpoint
  • Extract a shared utility that defines and enforces the key format (both for generation and validation)
  • Update the Zod schema and auth middleware to use that shared util

No immediate fixes are required—your current client-generated keys will be accepted as-is. This refactor is purely optional to reduce future maintenance overhead.

📜 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 16e09d0 and 59c7c62.

📒 Files selected for processing (8)
  • apps/web/app/(ee)/api/track/sale/client/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[idOrSlug]/route.ts (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/page.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx (1 hunks)
  • apps/web/lib/auth/publishable-key.ts (1 hunks)
  • apps/web/lib/types.ts (1 hunks)
  • apps/web/lib/zod/schemas/workspaces.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/auth/publishable-key.ts
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/page.tsx (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (1)
  • AllowedHostnamesForm (11-42)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx (1)
  • PublishableKeyForm (12-163)
apps/web/app/(ee)/api/track/sale/client/route.ts (4)
apps/web/lib/auth/publishable-key.ts (1)
  • withPublishableKey (22-114)
apps/web/lib/zod/schemas/sales.ts (1)
  • trackSaleRequestSchema (7-66)
apps/web/lib/api/errors.ts (1)
  • DubApiError (75-92)
apps/web/lib/api/conversions/track-sale.ts (1)
  • trackSale (28-286)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx (2)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/ui/modals/confirm-modal.tsx (1)
  • useConfirmModal (71-84)
apps/web/app/api/workspaces/[idOrSlug]/route.ts (1)
apps/web/lib/zod/schemas/workspaces.ts (1)
  • updateWorkspaceSchema (148-151)
⏰ 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: build
🔇 Additional comments (7)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (1)

11-42: Rename to AllowedHostnamesForm improves clarity and aligns with usage

Component rename/export looks good and matches the updated imports in analytics/page.tsx.

apps/web/app/(ee)/api/track/sale/client/route.ts (1)

8-56: Overall route flow LGTM

withPublishableKey wrapper, plan gating, body parsing, schema validation, and handoff to trackSale are consistent with the lead route and error handling patterns.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/page.tsx (1)

8-10: Nice composition of analytics settings

Good ordering: hostnames, publishable key, then conversion toggle.

apps/web/app/api/workspaces/[idOrSlug]/route.ts (1)

77-101: LGTM on allowedHostnames handling and cache sync

Validation + cache updates remain intact; the new field inclusion is isolated and does not affect these paths.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx (2)

72-91: Modal usage looks good.

Clear copy, destructive confirmations in place, and actions wired to the handlers. No issues.


121-149: Nice UX touches.

Displaying the key with truncation + a CopyButton, and offering regenerate/revoke paths is straightforward and intuitive.

apps/web/lib/zod/schemas/workspaces.ts (1)

1-189: Overall schema updates align with the new feature.

Good separation between base and extended workspace schemas; allowing null/undefined on updates matches revoke and generate flows.

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 (5)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (5)

56-62: Wildcard validation currently accepts any string starting with "*." (including invalid ones).

isValidHostname returns true for values like "*." or "*.bad host". This can persist invalid entries in settings and break matching logic downstream.

Apply:

-  const isValidHostname = (hostname: string) => {
-    return (
-      validDomainRegex.test(hostname) ||
-      hostname === "localhost" ||
-      hostname.startsWith("*.")
-    );
-  };
+  const isValidHostname = (hostname: string) => {
+    const h = hostname.toLowerCase();
+    if (h === "localhost") return true;
+    if (h.startsWith("*.")) {
+      const rest = h.slice(2);
+      return rest.length > 0 && validDomainRegex.test(rest);
+    }
+    return validDomainRegex.test(h);
+  };

64-97: Normalize input, compare case-insensitively, and harden network error handling.

Prevents duplicates due to casing/whitespace, improves UX on network errors, and sets the field back only on success.

Apply:

-  const addHostname = async () => {
-    if (allowedHostnames?.includes(hostname)) {
+  const addHostname = async () => {
+    const input = hostname.trim().toLowerCase();
+    if (allowedHostnames?.some((h) => h.toLowerCase() === input)) {
       toast.error("Hostname already exists.");
       return;
     }
 
-    if (!isValidHostname(hostname)) {
+    if (!isValidHostname(input)) {
       toast.error("Enter a valid domain.");
       return;
     }
 
     setProcessing(true);
 
-    const response = await fetch(`/api/workspaces/${id}`, {
-      method: "PATCH",
-      headers: {
-        "Content-Type": "application/json",
-      },
-      body: JSON.stringify({
-        allowedHostnames: [...(allowedHostnames || []), hostname],
-      }),
-    });
-
-    if (response.ok) {
-      toast.success("Hostname added.");
-    } else {
-      const { error } = await response.json();
-      toast.error(error.message);
-    }
-
-    mutate();
-    setProcessing(false);
-    setHostname("");
+    try {
+      const response = await fetch(`/api/workspaces/${id}`, {
+        method: "PATCH",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          allowedHostnames: [...(allowedHostnames || []), input],
+        }),
+      });
+
+      if (response.ok) {
+        toast.success("Hostname added.");
+        setHostname("");
+      } else {
+        const { error } = await response.json().catch(() => ({} as any));
+        toast.error(error?.message ?? "Failed to add hostname.");
+      }
+    } catch {
+      toast.error("Network error. Please try again.");
+    } finally {
+      mutate();
+      setProcessing(false);
+    }
   };

121-129: Avoid double-submit and enforce permissions/processing on the Add button.

Rely on the form’s onSubmit; disable when processing or lacking permission. This prevents duplicate PATCH calls and aligns the disabled tooltip with actual disabled state.

Apply:

       <Button
         text="Add Hostname"
         variant="primary"
-        onClick={addHostname}
-        disabled={!isValidHostname(hostname) || hostname.length === 0}
+        type="submit"
+        disabled={
+          !isValidHostname(hostname) ||
+          hostname.length === 0 ||
+          !!permissionsError ||
+          processing
+        }
         loading={processing}
-        className="w-fit"
+        className="w-fit"
         disabledTooltip={permissionsError || undefined}
       />

144-166: Harden delete flow with try/catch/finally and avoid sending undefined array.

Ensures mutate() and state cleanup always run, and avoids allowedHostnames: undefined when SWR cache is momentarily empty.

Apply:

   const handleDeleteHostname = async () => {
     setProcessing(true);
 
-    const response = await fetch(`/api/workspaces/${id}`, {
-      method: "PATCH",
-      headers: {
-        "Content-Type": "application/json",
-      },
-      body: JSON.stringify({
-        allowedHostnames: allowedHostnames?.filter((h) => h !== hostname),
-      }),
-    });
-
-    if (response.ok) {
-      toast.success("Hostname deleted.");
-    } else {
-      const { error } = await response.json();
-      toast.error(error.message);
-    }
-
-    mutate();
-    setProcessing(false);
+    try {
+      const response = await fetch(`/api/workspaces/${id}`, {
+        method: "PATCH",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          allowedHostnames: (allowedHostnames || []).filter((h) => h !== hostname),
+        }),
+      });
+
+      if (response.ok) {
+        toast.success("Hostname deleted.");
+      } else {
+        const { error } = await response.json().catch(() => ({} as any));
+        toast.error(error?.message ?? "Failed to delete hostname.");
+      }
+    } catch {
+      toast.error("Network error. Please try again.");
+    } finally {
+      mutate();
+      setProcessing(false);
+    }
   };

191-207: Disable Delete button on insufficient permissions or while processing; add accessible label.

Prevents opening the modal when the action isn’t allowed and improves a11y for icon-only buttons.

Apply:

         <Button
           variant="outline"
           className={cn(
             "h-8 px-1.5 outline-none transition-all duration-200",
             "border-transparent data-[state=open]:border-neutral-500 sm:group-hover/card:data-[state=closed]:border-neutral-200",
           )}
           icon={
             processing ? (
               <LoadingSpinner className="size-4 shrink-0" />
             ) : (
               <Trash className="size-4" />
             )
           }
-          onClick={() => setShowConfirmModal(true)}
+          onClick={() => setShowConfirmModal(true)}
+          disabled={!!permissionsError || processing}
+          aria-label="Delete hostname"
           disabledTooltip={permissionsError || undefined}
         />

Also applies to: 205-205

🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (2)

168-174: Optional: render destructive styling for the confirm action (if supported).

If ConfirmModal supports a destructive variant for the confirm button, consider enabling it to better signal risk.

Example (adjust to actual prop name):

useConfirmModal({
  // ...
  confirmVariant: "destructive",
});

176-178: Redundant key prop inside card.

The list key lives on the parent mapping (<AllowedHostnameCard key={hostname} />). The inner key here has no effect and can be removed for clarity.

Apply:

-    <CardList.Card
-      key={hostname}
+    <CardList.Card
📜 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 59c7c62 and 289e4d2.

📒 Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (5 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/publishable-key-form.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (1)
apps/web/ui/modals/confirm-modal.tsx (1)
  • useConfirmModal (71-84)
⏰ 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: build
🔇 Additional comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/allowed-hostnames-form.tsx (4)

5-5: Confirm modal hook import looks good.

Importing useConfirmModal aligns with the new confirmation UX and simplifies local state handling.


168-174: Confirm modal integration reads well.

Good use of the shared useConfirmModal hook and clear copy for a destructive action.


209-210: Rendering the confirmModal near the card is fine.

Keeps modal logic local to the card and avoids prop drilling.


12-13: Component rename verified – all imports updated

I ran a project-wide search and confirmed there are no remaining imports or references to the old AllowedHostnames identifier. The only import of the new component is in:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/page.tsx:2
    import { AllowedHostnamesForm } from "./allowed-hostnames-form";

Everything’s up to date.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (9)
apps/web/lib/api/cors.ts (1)

1-5: Make headers immutable and add a dynamic preflight helper (+ Vary/Max-Age).

This prevents accidental mutation and makes OPTIONS robust by reflecting Access-Control-Request-Headers. Also add Vary to keep caches honest and Access-Control-Max-Age to cut preflight noise.

Apply this diff for immutability:

-export const COMMON_CORS_HEADERS = {
+export const COMMON_CORS_HEADERS: Readonly<Record<string, string>> = Object.freeze({
   "Access-Control-Allow-Origin": "*",
   "Access-Control-Allow-Methods": "POST, OPTIONS",
   "Access-Control-Allow-Headers":
     "Content-Type, Authorization, X-Dub-Publishable-Key, X-Requested-With",
-};
+});

Add this helper (outside the changed lines; place below the constant):

// Build preflight headers that reflect requested headers to avoid CORS rejections.
export function preflightHeaders(req: Request): Record<string, string> {
  const requested = req.headers.get("Access-Control-Request-Headers") ?? "";
  return {
    ...COMMON_CORS_HEADERS,
    ...(requested && { "Access-Control-Allow-Headers": requested }),
    "Access-Control-Max-Age": "600",
    Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
  };
}
apps/web/app/(ee)/api/track/open/route.ts (1)

1-1: Use dynamic preflight headers to mirror requested headers and add Vary/Max-Age.

Hardcoding Access-Control-Allow-Headers can break some clients. Mirror the Access-Control-Request-Headers and expose proper Vary/Max-Age using a helper.

Apply:

-import { COMMON_CORS_HEADERS } from "@/lib/api/cors";
+import { COMMON_CORS_HEADERS, preflightHeaders } from "@/lib/api/cors";
-export const OPTIONS = () => {
-  return new Response(null, {
-    status: 204,
-    headers: COMMON_CORS_HEADERS,
-  });
-};
+export const OPTIONS = (req: Request) =>
+  new Response(null, {
+    status: 204,
+    headers: preflightHeaders(req),
+  });

Also applies to: 138-143

apps/web/app/(ee)/api/shopify/pixel/route.ts (1)

1-1: Prefer dynamic preflight to accommodate Shopify/browser-added headers.

Some storefront setups include additional headers; mirroring requested headers prevents brittle CORS failures.

Apply:

-import { COMMON_CORS_HEADERS } from "@/lib/api/cors";
+import { COMMON_CORS_HEADERS, preflightHeaders } from "@/lib/api/cors";
-export const OPTIONS = () => {
-  return new Response(null, {
-    status: 204,
-    headers: COMMON_CORS_HEADERS,
-  });
-};
+export const OPTIONS = (req: Request) =>
+  new Response(null, {
+    status: 204,
+    headers: preflightHeaders(req),
+  });

Also applies to: 58-63

apps/web/app/(ee)/api/track/click/route.ts (1)

6-6: Align OPTIONS with dynamic preflight; optionally narrow Allow-Origin post-allowlist.

  • Dynamic preflight avoids header mismatches.
  • Optional: after verifyAnalyticsAllowedHostnames succeeds, consider echoing the request Origin instead of "*" to tighten CORS posture for allowed hosts (logic stays the same; just a response header tweak).

Apply dynamic preflight:

-import { COMMON_CORS_HEADERS } from "@/lib/api/cors";
+import { COMMON_CORS_HEADERS, preflightHeaders } from "@/lib/api/cors";
-export const OPTIONS = () => {
-  return new Response(null, {
-    status: 204,
-    headers: COMMON_CORS_HEADERS,
-  });
-};
+export const OPTIONS = (req: Request) =>
+  new Response(null, {
+    status: 204,
+    headers: preflightHeaders(req),
+  });

If you want to scope Allow-Origin after allowlist verification, I can draft a small helper to conditionally set it based on the Origin header.

Also applies to: 170-175

apps/web/app/api/oauth/token/route.ts (2)

1-1: Use dynamic preflight for OPTIONS to reflect requested headers.

Same rationale as other routes; safer for diverse clients and future header additions.

Apply:

-import { COMMON_CORS_HEADERS } from "@/lib/api/cors";
+import { COMMON_CORS_HEADERS, preflightHeaders } from "@/lib/api/cors";
-export const OPTIONS = () => {
-  return new Response(null, {
-    status: 204,
-    headers: COMMON_CORS_HEADERS,
-  });
-};
+export const OPTIONS = (req: Request) =>
+  new Response(null, {
+    status: 204,
+    headers: preflightHeaders(req),
+  });

Also applies to: 32-37


19-21: Double-check CORS exposure for the token endpoint fits your threat model.

Allow-Origin: "*" on a token endpoint is a product decision. If tokens are intended to be retrievable by browsers from arbitrary origins (e.g., SPA + PKCE), this is fine. If not, consider restricting origins or disabling CORS here.

I can draft an Origin-allowlist solution keyed off environment config if you’d like.

Also applies to: 24-26

apps/web/app/(ee)/api/track/visit/route.ts (3)

2-2: CORS centralization is good; double-check allowed headers for publishable-key flows

Importing COMMON_CORS_HEADERS reduces drift—nice. One caution: If client endpoints authenticated via a “publishable key” use a custom header (e.g., X-Dub-Publishable-Key), ensure COMMON_CORS_HEADERS["Access-Control-Allow-Headers"] includes it; otherwise, browsers will block cross-origin requests at preflight.

Proposed update in apps/web/lib/api/cors.ts:

 export const COMMON_CORS_HEADERS = {
   "Access-Control-Allow-Origin": "*",
   "Access-Control-Allow-Methods": "POST, OPTIONS",
-  "Access-Control-Allow-Headers": "Content-Type, Authorization",
+  "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Dub-Publishable-Key",
+  // Optional: reduce preflight chatter across busy tracking endpoints
+  "Access-Control-Max-Age": "86400",
 };

If the publishable key is carried via Authorization: Bearer ..., this change isn’t strictly required—please confirm what withPublishableKey expects.


107-108: Final JSON response uses common CORS — LGTM

Consistent header application on the success path. Consider adding a Cache-Control header if you ever want clients/CDNs to cache this short-lived response, but not required.


118-119: OPTIONS 204 with common CORS — LGTM; consider max-age

Preflight handling is correct. If you adopt Access-Control-Max-Age in COMMON_CORS_HEADERS, this OPTIONS response will benefit automatically.

📜 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 23d0f97 and 1077dcd.

📒 Files selected for processing (9)
  • apps/web/app/(ee)/api/shopify/pixel/route.ts (2 hunks)
  • apps/web/app/(ee)/api/track/click/route.ts (2 hunks)
  • apps/web/app/(ee)/api/track/lead/client/route.ts (1 hunks)
  • apps/web/app/(ee)/api/track/lead/route.ts (1 hunks)
  • apps/web/app/(ee)/api/track/open/route.ts (4 hunks)
  • apps/web/app/(ee)/api/track/sale/client/route.ts (1 hunks)
  • apps/web/app/(ee)/api/track/visit/route.ts (3 hunks)
  • apps/web/app/api/oauth/token/route.ts (2 hunks)
  • apps/web/lib/api/cors.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/(ee)/api/track/sale/client/route.ts
  • apps/web/app/(ee)/api/track/lead/client/route.ts
🧰 Additional context used
🧬 Code Graph Analysis (5)
apps/web/app/(ee)/api/track/click/route.ts (2)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
apps/web/app/(ee)/api/track/visit/route.ts (2)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
apps/web/app/(ee)/api/track/open/route.ts (2)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
apps/web/app/api/oauth/token/route.ts (2)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
apps/web/app/(ee)/api/shopify/pixel/route.ts (2)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
⏰ 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: build
🔇 Additional comments (8)
apps/web/lib/api/cors.ts (1)

1-5: Good centralization of CORS defaults.

A single source of truth reduces drift across routes. Naming and placement look good.

apps/web/app/(ee)/api/track/open/route.ts (1)

49-51: LGTM: migrated to COMMON_CORS_HEADERS consistently.

Responses and error-paths now use the shared headers; OPTIONS returns expected headers. Behavior parity retained.

Also applies to: 61-62, 132-135, 141-142

apps/web/app/(ee)/api/shopify/pixel/route.ts (1)

51-52: LGTM: swapped to COMMON_CORS_HEADERS across success, error, and OPTIONS.

Change is scoped and consistent; no behavioral regressions expected.

Also applies to: 55-55, 61-62

apps/web/app/(ee)/api/track/click/route.ts (1)

6-6: LGTM: consolidated CORS usage without touching business logic.

Adoption is uniform; error responses still include CORS headers. Good.

Also applies to: 164-165, 166-167, 173-174

apps/web/app/api/oauth/token/route.ts (1)

1-1: LGTM on centralization; token flow unchanged.

Swapping to COMMON_CORS_HEADERS is consistent across both grant paths, errors, and OPTIONS.

Also applies to: 19-21, 25-26, 28-29, 35-36

apps/web/app/(ee)/api/track/lead/route.ts (1)

24-24: Ensure mode optionality and default behavior downstream

  • The trackLeadRequestSchema in apps/web/lib/zod/schemas/leads.ts does not declare a mode field, so requests may arrive with mode = undefined. If you intend for mode to be optional or have a default, update the schema (e.g., .optional() or .default(...)) to match expectations.
  • The trackLead call in apps/web/app/(ee)/api/track/lead/route.ts resolves to an external implementation (no local definition). Please confirm that this function:
    • Accepts an undefined mode without throwing
    • Applies the correct default or branches safely when mode is omitted
  • Add or update tests to cover both scenarios:
    • An explicit mode value
    • Omitted mode (i.e. relying on the default)

Please verify these points in your schema and downstream integration to ensure deterministic behavior.

apps/web/app/(ee)/api/track/visit/route.ts (2)

45-46: Returning cached clickId with CORS headers — LGTM

Using COMMON_CORS_HEADERS here keeps cached and non-cached responses consistent for the browser. No further action.


111-112: Error path includes CORS headers — LGTM

Passing headers into handleAndReturnErrorResponse avoids CORS surprises on failures. Good catch.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
apps/web/app/(ee)/api/track/lead/client/route.ts (3)

56-56: Propagate rate limit headers on success (see sale route comment)

Mirror the middleware-level fix so clients can read remaining quota. No action needed in this file if publishable-key middleware merges headers as suggested.


59-66: Avoid plan list duplication; centralize allowed plans

Same refactor as previously suggested; import from a single constants module.


6-6: Add Vary header in COMMON_CORS_HEADERS (shared)

As noted in the sale route, add Vary: Origin, Authorization to the shared CORS headers to make client routes safe around caches.

🧹 Nitpick comments (6)
apps/web/app/(ee)/api/track/sale/client/route.ts (6)

30-39: Prefer const for parsed request props

These values aren’t reassigned. Use const for clarity and to prevent accidental mutation.

-    let {
+    const {
       customerExternalId,
       paymentProcessor,
       invoiceId,
       amount,
       currency,
       metadata,
       eventName,
       leadEventName,
     } = trackSaleRequestSchema.parse(body);

6-6: Add Vary: Origin, Authorization to CORS headers to prevent cache key collisions

Since Authorization carries the publishable key (per learning) and Origin affects allowlisting, add Vary to avoid cross-tenant leakage via intermediary caches/CDNs. Optional: add Access-Control-Max-Age to reduce preflight cost.

apps/web/lib/api/cors.ts:

 export const COMMON_CORS_HEADERS = {
   "Access-Control-Allow-Origin": "*",
   "Access-Control-Allow-Methods": "POST, OPTIONS",
   "Access-Control-Allow-Headers": "Content-Type, Authorization",
+  "Vary": "Origin, Authorization",
+  "Access-Control-Max-Age": "600",
 };

Note: This aligns with the publishable key sent via Authorization: Bearer dub_pk_… as captured in the retrieved learnings.


24-27: Avoid hard-coding app.dub.co; centralize app origin

Use a single source of truth (e.g., APP_ORIGIN from config/constants) to build links in error messages. This prevents environment-specific leaks (staging, self-hosted) and eases future moves.

Example:

- message: `Request origin '${getHostnameFromRequest(req)}' is not included in the allowed hostnames for this workspace. Update your allowed hostnames here: https://app.dub.co/settings/analytics`,
+ message: `Request origin '${getHostnameFromRequest(req)}' is not included in the allowed hostnames for this workspace. Update your allowed hostnames here: ${APP_ORIGIN}/settings/analytics`,

64-71: Avoid plan list duplication; centralize allowed plans

Same list appears in multiple routes and defaults in middleware. Move to a shared constant (e.g., "@/lib/plans") to prevent drift and enable normalization (e.g., lowercasing) in one place.


61-61: Add rate‐limit headers to successful responses in withPublishableKey

Currently, withPublishableKey only applies the computed X-RateLimit-* and Retry-After headers when returning a 429 response. On successful requests the wrapper returns the handler’s Response (or object) without those headers, so downstream routes cannot inspect remaining quota.

To address this, update apps/web/lib/auth/publishable-key.ts to merge the headers object onto all successful handler results:

• File: apps/web/lib/auth/publishable-key.ts
– After calling the handler, detect if the returned value is a Response and set each header.
– Otherwise, wrap non-Response returns in a new Response with the headers applied.

Illustrative diff:

--- a/apps/web/lib/auth/publishable-key.ts
+++ b/apps/web/lib/auth/publishable-key.ts
@@ after line ~70
-          const searchParams = getSearchParams(req.url);
-          return await handler({ req, params, searchParams, workspace });
+          const searchParams = getSearchParams(req.url);
+          // Invoke handler and propagate rate-limit headers on all responses
+          const result = await handler({ req, params, searchParams, workspace });
+          // If it's already a Response, merge headers
+          if (result instanceof Response) {
+            for (const [key, value] of Object.entries(headers)) {
+              result.headers.set(key, value as string);
+            }
+            return result;
+          }
+          // Otherwise wrap the response data
+          return new Response(JSON.stringify(result), {
+            status: 200,
+            headers: {
+              "content-type": "application/json",
+              ...headers,
+            },
+          });

This change ensures all successful endpoints receive consistent rate-limit metadata without each route needing to handle it.


41-46: Centralize non-empty customerExternalId validation in your Zod schema and remove the runtime guard

To avoid divergent error messages and duplicated checks, move the “required and non-empty” validation into trackSaleRequestSchema and drop the manual guard in the route.

• In apps/web/lib/zod/schemas/sales.ts, update the customerExternalId definition:

- customerExternalId: z
-   .string()
-   .trim()
-   .max(100)
+ customerExternalId: z
+   .string({ required_error: "customerExternalId is required" })
+   .trim()
+   .min(1, "customerExternalId is required")
+   .max(100)

• In apps/web/app/(ee)/api/track/sale/client/route.ts, remove the manual guard at lines 41–46:

-    if (!customerExternalId) {
-      throw new DubApiError({
-        code: "bad_request",
-        message: "customerExternalId is required",
-      });
-    }

This ensures all “required and non-empty” logic lives in one place and yields consistent error messages.

📜 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 4a39baf and 859ad7f.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/track/lead/client/route.ts (1 hunks)
  • apps/web/app/(ee)/api/track/sale/client/route.ts (1 hunks)
  • apps/web/lib/analytics/verify-analytics-allowed-hostnames.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/lib/analytics/verify-analytics-allowed-hostnames.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: steven-tey
PR: dubinc/dub#2737
File: apps/web/lib/api/cors.ts:1-5
Timestamp: 2025-08-21T03:03:39.825Z
Learning: Dub publishable keys are sent via Authorization header using Bearer token format, not via custom X-Dub-Publishable-Key header. The publishable key middleware extracts keys using req.headers.get("Authorization")?.replace("Bearer ", "") and validates they start with "dub_pk_".
🧬 Code Graph Analysis (2)
apps/web/app/(ee)/api/track/lead/client/route.ts (6)
apps/web/lib/auth/publishable-key.ts (1)
  • withPublishableKey (22-113)
apps/web/lib/analytics/verify-analytics-allowed-hostnames.ts (2)
  • verifyAnalyticsAllowedHostnames (13-59)
  • getHostnameFromRequest (1-11)
apps/web/lib/api/errors.ts (1)
  • DubApiError (75-92)
apps/web/lib/zod/schemas/leads.ts (1)
  • trackLeadRequestSchema (7-73)
apps/web/lib/api/conversions/track-lead.ts (1)
  • trackLead (28-316)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/app/(ee)/api/track/sale/client/route.ts (7)
apps/web/app/(ee)/api/track/lead/client/route.ts (2)
  • POST (14-68)
  • OPTIONS (70-75)
apps/web/lib/auth/publishable-key.ts (1)
  • withPublishableKey (22-113)
apps/web/lib/analytics/verify-analytics-allowed-hostnames.ts (2)
  • verifyAnalyticsAllowedHostnames (13-59)
  • getHostnameFromRequest (1-11)
apps/web/lib/api/errors.ts (1)
  • DubApiError (75-92)
apps/web/lib/zod/schemas/sales.ts (1)
  • trackSaleRequestSchema (7-66)
apps/web/lib/api/conversions/track-sale.ts (1)
  • trackSale (28-286)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
⏰ 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: build
🔇 Additional comments (4)
apps/web/app/(ee)/api/track/sale/client/route.ts (2)

18-28: Origin allowlisting is enforced correctly

Good job guarding client-side tracking behind verifyAnalyticsAllowedHostnames and returning a clear, actionable error via DubApiError with the offending origin in the message.


75-80: OPTIONS handler for CORS preflight looks good

Responding with 204 and COMMON_CORS_HEADERS is correct and consistent with other routes.

apps/web/app/(ee)/api/track/lead/client/route.ts (2)

18-28: Origin allowlisting is enforced correctly

Consistent with sale route; clear error message and early exit on mismatch.


70-75: OPTIONS handler for CORS preflight looks good

204 + COMMON_CORS_HEADERS is consistent and correct.

@steven-tey steven-tey merged commit 1e6efbe into main Aug 21, 2025
7 checks passed
@steven-tey steven-tey deleted the publishable-key branch August 21, 2025 05:01
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.

2 participants

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载