Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Aug 21, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Standardized CORS behavior and included rate-limit headers on authentication-related responses for more consistent client handling.
  • Tests

    • Added end-to-end tests for tracking leads and client-side sales, validating full request/response flows.
    • Expanded test utilities (helpers and harness) to support new scenarios.
  • Chores

    • Introduced a required E2E publishable key environment variable for tests.
    • Updated CI to pass the new E2E publishable key secret during API test runs.
  • Refactor

    • Minor internal immutability cleanup with no functional changes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 21, 2025

Walkthrough

Updates client sale tracking route variable immutability, augments publishable-key auth to include CORS and rate-limit headers in responses and errors, adds new E2E client tests for lead and sale tracking, extends test helpers/env/integration harness to support publishable key, and injects a new secret into the E2E workflow.

Changes

Cohort / File(s) Summary
Client sale route immutability
apps/web/app/(ee)/api/track/sale/client/route.ts
Change let to const for parsed body fields; no logic change.
Auth header/CORS integration
apps/web/lib/auth/publishable-key.ts
Import COMMON_CORS_HEADERS; build headers upfront; merge CORS and rate-limit headers; pass headers to error handler; auth logic unchanged.
E2E client tracking tests
apps/web/tests/tracks/track-lead-client.test.ts, apps/web/tests/tracks/track-sale-client.test.ts
Add end-to-end tests for client lead and sale tracking using publishable key and predefined E2E constants.
Test utilities expansion
apps/web/tests/utils/helpers.ts, apps/web/tests/utils/integration.ts, apps/web/tests/utils/env.ts
Add randomSaleAmount helper; IntegrationHarness.init now returns env; require E2E_PUBLISHABLE_KEY in test env schema.
Test refactor to shared helper
apps/web/tests/tracks/track-sale.test.ts
Replace local randomSaleAmount with import from shared helpers.
CI workflow env
.github/workflows/e2e.yaml
Provide E2E_PUBLISHABLE_KEY secret to api-tests job environment.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as API Route
  participant Auth as withPublishableKey
  participant RL as RateLimiter
  participant Err as ErrorHandler

  Client->>API: HTTP Request (with Publishable Key)
  API->>Auth: Validate key, prepare headers (CORS)
  Auth->>RL: Check/consume rate limit
  RL-->>Auth: Limit status (+ Retry-After/X-RateLimit-* headers)
  alt Allowed
    Auth-->>API: Auth OK + headers (CORS + RateLimit)
    API-->>Client: 200 OK (headers preserved)
  else Limited/Error
    Auth->>Err: handleAndReturnErrorResponse(error, headers)
    Err-->>Client: Error response (CORS + RateLimit headers)
  end
Loading
sequenceDiagram
  autonumber
  participant Test as E2E Test
  participant API as /api/track/*/client
  participant Store as Backend

  Test->>API: POST /track/click (setup)
  API->>Store: Create click
  Store-->>API: clickId
  API-->>Test: 200 { clickId }

  Test->>API: POST /track/lead|sale/client (Auth: Publishable Key)
  API->>Store: Create lead/sale linked to click/customer
  Store-->>API: Created entity
  API-->>Test: 200 { payload with customer + entity details }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

A nibble of CORS, a sprinkle of keys,
I hop through headers with elegant ease.
Tests bloom like clover—lead and sale align,
Secrets in the burrow keep everything fine.
With const-bound carrots and limits in sight,
This bunny ships green—tests passing bright! 🥕✨

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 publishableKey

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.

@vercel
Copy link
Contributor

vercel bot commented Aug 21, 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 7:43pm

@devkiran devkiran marked this pull request as ready for review August 21, 2025 10:57
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: 3

Caution

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

⚠️ Outside diff range comments (3)
apps/web/tests/utils/env.ts (1)

7-12: Import-time hard failure breaks unrelated tests; make E2E_PUBLISHABLE_KEY optional or lazily validated.

Parsing process.env at module load with a new required key causes the entire test suite to fail in CI where the key isn’t set (see pipeline error). Prefer optional schema + targeted validation in the tests that need it, or move parse behind a function to avoid side effects at import time.

Apply this diff to make the key optional and keep strong typing:

 export const integrationTestEnv = z.object({
   E2E_BASE_URL: z.string().url().min(1),
   E2E_TOKEN: z.string().min(1),
   E2E_TOKEN_OLD: z.string().min(1),
-  E2E_PUBLISHABLE_KEY: z.string().min(1),
+  // Optional at schema level to avoid import-time failures; tests that need it should assert presence.
+  E2E_PUBLISHABLE_KEY: z.string().min(1).optional(),
   CI: z.coerce
     .string()
     .default("false")
     .transform((v) => v === "true"),
 });
 
 export const env = integrationTestEnv.parse(process.env);

Follow-up (outside this file):

  • In client E2E tests (track-sale-client-test.ts, track-lead-client-test.ts), early-exit/skip if env.E2E_PUBLISHABLE_KEY is undefined.
  • Alternatively, inject E2E_PUBLISHABLE_KEY in CI for the Public API Tests job.

I can open a follow-up PR to add a tiny helper like requireEnv("E2E_PUBLISHABLE_KEY") that throws a clear skip if missing.

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

46-56: Harden Authorization parsing (case-insensitive Bearer, robust extraction).

The includes("Bearer ") + replace("Bearer ", "") approach is case-sensitive and brittle. Use a case-insensitive regex to extract the token.

Apply this diff:

-        const authorizationHeader = req.headers.get("Authorization");
-        if (authorizationHeader) {
-          if (!authorizationHeader.includes("Bearer ")) {
+        const authorizationHeader = req.headers.get("authorization");
+        if (authorizationHeader) {
+          const match = authorizationHeader.match(/^\s*Bearer\s+(.+)\s*$/i);
+          if (!match) {
             throw new DubApiError({
               code: "bad_request",
               message: "Invalid or missing publishable key.",
             });
           }
-
-          const publishableKey = authorizationHeader.replace("Bearer ", "");
+          const publishableKey = match[1];

68-75: Retry-After should be a delta, not an epoch timestamp.

Upstash ratelimit.reset is a Unix timestamp. Retry-After must be seconds to wait (delta) or an HTTP-date. Sending epoch seconds leads to massive backoff. Keep X-RateLimit-Reset as the timestamp; compute Retry-After as max(0, reset - now).

Apply this diff:

-          headers = {
-            ...COMMON_CORS_HEADERS,
-            "Retry-After": reset.toString(),
-            "X-RateLimit-Limit": limit.toString(),
-            "X-RateLimit-Remaining": remaining.toString(),
-            "X-RateLimit-Reset": reset.toString(),
-          };
+          const now = Math.floor(Date.now() / 1000);
+          const retryAfter = Math.max(0, reset - now);
+          headers = {
+            ...headers,
+            "Retry-After": retryAfter.toString(),
+            "X-RateLimit-Limit": limit.toString(),
+            "X-RateLimit-Remaining": remaining.toString(),
+            "X-RateLimit-Reset": reset.toString(),
+          };
🧹 Nitpick comments (10)
apps/web/app/(ee)/api/track/sale/client/route.ts (2)

41-46: Redundant runtime check if schema already enforces customerExternalId.

If trackSaleRequestSchema guarantees customerExternalId is present (or normalizes legacy fields into it), this manual check is unnecessary and duplicates validation logic. If not, consider moving this requirement into the schema for consistent error formatting.


75-80: Consider caching preflight for fewer OPTIONS hits.

Adding Access-Control-Max-Age can reduce preflight traffic for client integrations.

Apply this diff:

 export const OPTIONS = () => {
   return new Response(null, {
     status: 204,
     headers: COMMON_CORS_HEADERS,
   });
 };

Suggested update in apps/web/lib/api/cors.ts (outside this hunk):

export const COMMON_CORS_HEADERS = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
  "Access-Control-Max-Age": "600",
};
apps/web/tests/utils/env.ts (1)

14-15: Optional: Avoid exporting a precomputed env to reduce import-time side effects.

Export a getEnv() function returning integrationTestEnv.parse(process.env) so tests can control when validation occurs.

apps/web/tests/utils/helpers.ts (1)

31-33: Hoist sale amounts into a constant to avoid reallocations and improve typing.

Minor nit: reuse a single array and leverage as const for tighter types.

Apply this diff:

-export const randomSaleAmount = () => {
-  return randomValue([400, 900, 1900]);
-};
+const SALE_AMOUNTS = [400, 900, 1900] as const;
+export const randomSaleAmount = () => randomValue(SALE_AMOUNTS);
apps/web/tests/tracks/track-sale.test.ts (1)

133-135: Reduce flakiness by polling instead of fixed sleep.

Replace fixed 2s delay with a small polling loop (e.g., up to 10s, 200ms interval) for eventual consistency when verifying commissions.

Here’s a lightweight pattern:

async function waitFor<T>(fn: () => Promise<T>, predicate: (x: T) => boolean, timeoutMs = 10000, intervalMs = 200) {
  const start = Date.now();
  // eslint-disable-next-line no-constant-condition
  while (true) {
    const val = await fn();
    if (predicate(val)) return val;
    if (Date.now() - start > timeoutMs) throw new Error("Timeout waiting for condition");
    await new Promise((r) => setTimeout(r, intervalMs));
  }
}

// Usage inside the test: await waitFor(() => http.get({...}), res => res.status === 200 && res.data.length === 1)
apps/web/lib/auth/publishable-key.ts (2)

76-81: Expose rate-limit headers to browsers.

Without Access-Control-Expose-Headers, client JS can’t read X-RateLimit-* and Retry-After. Consider adding this to COMMON_CORS_HEADERS.

Outside this file (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-Expose-Headers": "Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset",
  "Access-Control-Max-Age": "600",
};

103-105: Optional: propagate rate-limit headers on successful responses for parity.

Right now only 429/error paths include rate-limit metadata. If you want clients to budget calls proactively, consider attaching headers to the handler’s success response as well (e.g., by cloning and augmenting the Response returned by handler).

apps/web/tests/tracks/track-lead-client.test.ts (2)

26-26: Nit: test title mentions the wrong prior endpoint.

The title says “from a prior /track/lead/client request” but the clickId comes from /track/click. The diff above fixes the wording.


1-2: Optional: reuse IntegrationHarness.http for uniform client behavior/logging.

You could replace fetch with h.http.post and pass a custom Authorization header (publishable key) to keep request plumbing, error handling, and diagnostics consistent across tests. Not required, just consistency sugar.

apps/web/tests/tracks/track-sale-client-test.ts (1)

21-29: Verify CORS header propagation in the sale-client test

The sale-client endpoint returns the standard CORS headers (via COMMON_CORS_HEADERS), but does not include any rate-limit headers on success. You can optionally assert that the critical CORS headers are correctly propagated:

• File: apps/web/tests/tracks/track-sale-client-test.ts
Around lines 21–29, after you obtain response, add:

// Assert CORS headers are present and correct
expect(response.headers.has("access-control-allow-origin")).toBe(true);
expect(response.headers.get("access-control-allow-origin")).toBe("*");

// (Optional) also check additional CORS headers if desired
expect(response.headers.get("access-control-allow-methods")).toContain("POST");
expect(response.headers.get("access-control-allow-headers")).toContain("Content-Type");

Since this route isn’t rate-limited in the happy-path, you can skip any X-RateLimit-* or Retry-After checks here.

📜 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 1e6efbe and bb49430.

📒 Files selected for processing (8)
  • apps/web/app/(ee)/api/track/sale/client/route.ts (1 hunks)
  • apps/web/lib/auth/publishable-key.ts (5 hunks)
  • apps/web/tests/tracks/track-lead-client.test.ts (1 hunks)
  • apps/web/tests/tracks/track-sale-client-test.ts (1 hunks)
  • apps/web/tests/tracks/track-sale.test.ts (1 hunks)
  • apps/web/tests/utils/env.ts (1 hunks)
  • apps/web/tests/utils/helpers.ts (2 hunks)
  • apps/web/tests/utils/integration.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-21T03:03:39.879Z
Learnt from: steven-tey
PR: dubinc/dub#2737
File: apps/web/lib/api/cors.ts:1-5
Timestamp: 2025-08-21T03:03:39.879Z
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_".

Applied to files:

  • apps/web/lib/auth/publishable-key.ts
📚 Learning: 2025-08-21T03:03:39.879Z
Learnt from: steven-tey
PR: dubinc/dub#2737
File: apps/web/lib/api/cors.ts:1-5
Timestamp: 2025-08-21T03:03:39.879Z
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 ", "").

Applied to files:

  • apps/web/lib/auth/publishable-key.ts
🧬 Code graph analysis (3)
apps/web/tests/tracks/track-lead-client.test.ts (4)
apps/web/tests/utils/integration.ts (1)
  • IntegrationHarness (14-100)
apps/web/tests/utils/resource.ts (1)
  • E2E_TRACK_CLICK_HEADERS (10-14)
apps/web/tests/utils/helpers.ts (1)
  • randomCustomer (7-17)
apps/web/tests/utils/env.ts (1)
  • env (14-14)
apps/web/tests/tracks/track-sale-client-test.ts (4)
apps/web/tests/utils/integration.ts (1)
  • IntegrationHarness (14-100)
apps/web/tests/utils/helpers.ts (2)
  • randomSaleAmount (31-33)
  • randomId (4-4)
apps/web/tests/utils/env.ts (1)
  • env (14-14)
apps/web/tests/utils/resource.ts (2)
  • E2E_CUSTOMER_ID (28-28)
  • E2E_CUSTOMER_EXTERNAL_ID (29-29)
apps/web/lib/auth/publishable-key.ts (2)
apps/web/lib/api/cors.ts (1)
  • COMMON_CORS_HEADERS (1-5)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
🪛 GitHub Actions: Public API Tests
apps/web/tests/utils/env.ts

[error] 14-14: Environment validation failed: E2E_PUBLISHABLE_KEY is required but undefined (Zod validation error).

🔇 Additional comments (5)
apps/web/app/(ee)/api/track/sale/client/route.ts (1)

30-39: Good immutability tweak; const destructuring is appropriate here.

No functional change and prevents accidental reassignment of parsed fields. Looks good.

apps/web/tests/utils/helpers.ts (1)

2-2: Nice: consolidates randomness utilities in a shared helper.

Importing randomValue here reduces duplication across tests.

apps/web/tests/tracks/track-sale.test.ts (1)

2-2: LGTM: centralizes randomSaleAmount import.

Removes the local helper and reuses the shared one; keeps tests DRY.

apps/web/tests/tracks/track-lead-client.test.ts (1)

43-55: Your script will locate the cors.ts definition and show exactly which CORS headers are included, so we can tailor the test assertions to the real header names. Let me know the output, and I’ll finalize the review comment with precise examples.

apps/web/tests/utils/integration.ts (1)

55-59: LGTM: returning env from init improves ergonomics for client-route tests.

This aligns the harness with publishable-key flows that need env.E2E_BASE_URL and env.E2E_PUBLISHABLE_KEY.

Comment on lines +43 to 46
let headers = {};

try {
const authorizationHeader = req.headers.get("Authorization");
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

Default CORS headers should be present on all error responses; also type headers explicitly.

Currently, errors thrown before rate-limit initialization (e.g., missing/invalid Authorization) return with empty headers, losing CORS in browsers. Initialize headers with COMMON_CORS_HEADERS and give it a concrete type.

Apply this diff:

-      let headers = {};
+      let headers: Record<string, string> = { ...COMMON_CORS_HEADERS };

Also applies to: 111-114

🤖 Prompt for AI Agents
In apps/web/lib/auth/publishable-key.ts around lines 43-46 and 111-114, the
error-paths initialize headers as an empty object which strips CORS on early
failures; change the header initialization to a typed Record<string,string>
populated from COMMON_CORS_HEADERS (e.g. let headers: Record<string,string> = {
...COMMON_CORS_HEADERS }) and ensure any thrown/returned error responses use
this headers variable so CORS headers are always included on error responses;
apply the same typed initialization and usage at both locations.

Comment on lines 6 to 57
describe("POST /track/lead/client", async () => {
const h = new IntegrationHarness();
const { http, env } = await h.init();

// Track a click
const clickResponse = await http.post<{ clickId: string }>({
path: "/track/click",
headers: E2E_TRACK_CLICK_HEADERS,
body: {
domain: "getacme.link",
key: "derek",
},
});

expect(clickResponse.status).toEqual(200);
expect(clickResponse.data.clickId).toStrictEqual(expect.any(String));

const clickId = clickResponse.data.clickId;
const customer = randomCustomer();

test("track a lead (with clickId from a prior /track/lead/client request)", async () => {
const response = await fetch(`${env.E2E_BASE_URL}/api/track/lead/client`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({
clickId: clickId,
eventName: "Signup",
customerExternalId: customer.externalId,
customerName: customer.name,
customerEmail: customer.email,
customerAvatar: customer.avatar,
}),
});

const leadResponse = await response.json();

expect(response.status).toEqual(200);
expect(leadResponse).toStrictEqual({
clickId,
customerName: customer.name,
customerEmail: customer.email,
customerAvatar: customer.avatar,
customer: customer,
click: {
id: clickId,
},
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not use async describe; move awaits into the test and reduce brittle assertions.

Vitest treats describe blocks as synchronous. Awaiting inside describe can cause unpredictable execution ordering. Also, asserting an exact response shape with toStrictEqual is brittle if the API adds new fields.

Here’s a consolidated fix that:

  • Makes describe synchronous.
  • Performs the click + lead flow within a single test.
  • Uses toMatchObject for forward-compatible assertions.
  • Corrects the test title to reference /track/click.
-describe("POST /track/lead/client", async () => {
-  const h = new IntegrationHarness();
-  const { http, env } = await h.init();
-
-  // Track a click
-  const clickResponse = await http.post<{ clickId: string }>({
-    path: "/track/click",
-    headers: E2E_TRACK_CLICK_HEADERS,
-    body: {
-      domain: "getacme.link",
-      key: "derek",
-    },
-  });
-
-  expect(clickResponse.status).toEqual(200);
-  expect(clickResponse.data.clickId).toStrictEqual(expect.any(String));
-
-  const clickId = clickResponse.data.clickId;
-  const customer = randomCustomer();
-
-  test("track a lead (with clickId from a prior /track/lead/client request)", async () => {
-    const response = await fetch(`${env.E2E_BASE_URL}/api/track/lead/client`, {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
-      },
-      body: JSON.stringify({
-        clickId: clickId,
-        eventName: "Signup",
-        customerExternalId: customer.externalId,
-        customerName: customer.name,
-        customerEmail: customer.email,
-        customerAvatar: customer.avatar,
-      }),
-    });
-
-    const leadResponse = await response.json();
-
-    expect(response.status).toEqual(200);
-    expect(leadResponse).toStrictEqual({
-      clickId,
-      customerName: customer.name,
-      customerEmail: customer.email,
-      customerAvatar: customer.avatar,
-      customer: customer,
-      click: {
-        id: clickId,
-      },
-    });
-  });
-});
+describe("POST /track/lead/client", () => {
+  const h = new IntegrationHarness();
+
+  test("track a lead (with clickId from a prior /track/click request)", async () => {
+    const { http, env } = await h.init();
+
+    // Track a click
+    const clickResponse = await http.post<{ clickId: string }>({
+      path: "/track/click",
+      headers: E2E_TRACK_CLICK_HEADERS,
+      body: {
+        domain: "getacme.link",
+        key: "derek",
+      },
+    });
+
+    expect(clickResponse.status).toBe(200);
+    expect(clickResponse.data.clickId).toEqual(expect.any(String));
+
+    const clickId = clickResponse.data.clickId;
+    const customer = randomCustomer();
+
+    const response = await fetch(`${env.E2E_BASE_URL}/api/track/lead/client`, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
+      },
+      body: JSON.stringify({
+        clickId,
+        eventName: "Signup",
+        customerExternalId: customer.externalId,
+        customerName: customer.name,
+        customerEmail: customer.email,
+        customerAvatar: customer.avatar,
+      }),
+    });
+
+    const leadResponse = await response.json();
+
+    expect(response.status).toBe(200);
+    expect(leadResponse).toMatchObject({
+      clickId,
+      customerName: customer.name,
+      customerEmail: customer.email,
+      customerAvatar: customer.avatar,
+      customer: {
+        externalId: customer.externalId,
+        name: customer.name,
+        email: customer.email,
+        avatar: customer.avatar,
+      },
+      click: {
+        id: clickId,
+      },
+    });
+  });
+});
📝 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.

Suggested change
describe("POST /track/lead/client", async () => {
const h = new IntegrationHarness();
const { http, env } = await h.init();
// Track a click
const clickResponse = await http.post<{ clickId: string }>({
path: "/track/click",
headers: E2E_TRACK_CLICK_HEADERS,
body: {
domain: "getacme.link",
key: "derek",
},
});
expect(clickResponse.status).toEqual(200);
expect(clickResponse.data.clickId).toStrictEqual(expect.any(String));
const clickId = clickResponse.data.clickId;
const customer = randomCustomer();
test("track a lead (with clickId from a prior /track/lead/client request)", async () => {
const response = await fetch(`${env.E2E_BASE_URL}/api/track/lead/client`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({
clickId: clickId,
eventName: "Signup",
customerExternalId: customer.externalId,
customerName: customer.name,
customerEmail: customer.email,
customerAvatar: customer.avatar,
}),
});
const leadResponse = await response.json();
expect(response.status).toEqual(200);
expect(leadResponse).toStrictEqual({
clickId,
customerName: customer.name,
customerEmail: customer.email,
customerAvatar: customer.avatar,
customer: customer,
click: {
id: clickId,
},
});
});
});
describe("POST /track/lead/client", () => {
const h = new IntegrationHarness();
test("track a lead (with clickId from a prior /track/click request)", async () => {
const { http, env } = await h.init();
// Track a click
const clickResponse = await http.post<{ clickId: string }>({
path: "/track/click",
headers: E2E_TRACK_CLICK_HEADERS,
body: {
domain: "getacme.link",
key: "derek",
},
});
expect(clickResponse.status).toBe(200);
expect(clickResponse.data.clickId).toEqual(expect.any(String));
const clickId = clickResponse.data.clickId;
const customer = randomCustomer();
const response = await fetch(`${env.E2E_BASE_URL}/api/track/lead/client`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({
clickId,
eventName: "Signup",
customerExternalId: customer.externalId,
customerName: customer.name,
customerEmail: customer.email,
customerAvatar: customer.avatar,
}),
});
const leadResponse = await response.json();
expect(response.status).toBe(200);
expect(leadResponse).toMatchObject({
clickId,
customerName: customer.name,
customerEmail: customer.email,
customerAvatar: customer.avatar,
customer: {
externalId: customer.externalId,
name: customer.name,
email: customer.email,
avatar: customer.avatar,
},
click: {
id: clickId,
},
});
});
});
🤖 Prompt for AI Agents
In apps/web/tests/tracks/track-lead-client.test.ts around lines 6 to 57, the
describe block is async and contains awaits and brittle strict shape assertions;
change the describe to be synchronous, move all awaits (initializing
IntegrationHarness, the /track/click request and its assertions) into the test
body so the test becomes async, perform the click and then the POST
/track/lead/client flow within the single test, assert response.status === 200
and replace toStrictEqual shape checks with toMatchObject to allow extra fields,
and update the test title to reference the /track/click flow appropriately.

Comment on lines 9 to 57
describe("POST /track/sale/client", async () => {
const h = new IntegrationHarness();
const { env } = await h.init();

const sale = {
eventName: "Subscription",
amount: randomSaleAmount(),
currency: "usd",
invoiceId: `INV_${randomId()}`,
paymentProcessor: "stripe",
};

test("track a sale", async () => {
const response = await fetch(`${env.E2E_BASE_URL}/api/track/sale/client`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({}),
});

const leadResponse = await response.json();

expect(response.status).toEqual(200);
expect(leadResponse).toStrictEqual({
eventName: "Subscription",
customer: {
id: E2E_CUSTOMER_ID,
name: expect.any(String),
email: expect.any(String),
avatar: expect.any(String),
externalId: E2E_CUSTOMER_EXTERNAL_ID,
},
sale: {
amount: sale.amount,
currency: sale.currency,
paymentProcessor: sale.paymentProcessor,
invoiceId: sale.invoiceId,
metadata: null,
},
amount: sale.amount,
currency: sale.currency,
paymentProcessor: sale.paymentProcessor,
metadata: null,
invoiceId: sale.invoiceId,
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Async describe and missing request body make the test flaky/incorrect.

  • describe must be synchronous in Vitest.
  • You prepare a sale object but send an empty body; expectations then don’t reflect the actual input.
  • Variable name leadResponse is misleading in a sale test.
  • Use toMatchObject to avoid brittleness if the API adds fields.

Apply this refactor:

-describe("POST /track/sale/client", async () => {
-  const h = new IntegrationHarness();
-  const { env } = await h.init();
-
-  const sale = {
-    eventName: "Subscription",
-    amount: randomSaleAmount(),
-    currency: "usd",
-    invoiceId: `INV_${randomId()}`,
-    paymentProcessor: "stripe",
-  };
-
-  test("track a sale", async () => {
-    const response = await fetch(`${env.E2E_BASE_URL}/api/track/sale/client`, {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-        Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
-      },
-      body: JSON.stringify({}),
-    });
-
-    const leadResponse = await response.json();
-
-    expect(response.status).toEqual(200);
-    expect(leadResponse).toStrictEqual({
+describe("POST /track/sale/client", () => {
+  const h = new IntegrationHarness();
+
+  test("track a sale", async () => {
+    const { env } = await h.init();
+
+    const sale = {
+      eventName: "Subscription",
+      amount: randomSaleAmount(),
+      currency: "usd",
+      invoiceId: `INV_${randomId()}`,
+      paymentProcessor: "stripe",
+    };
+
+    const response = await fetch(`${env.E2E_BASE_URL}/api/track/sale/client`, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
+      },
+      body: JSON.stringify(sale),
+    });
+
+    const saleResponse = await response.json();
+
+    expect(response.status).toBe(200);
+    expect(saleResponse).toMatchObject({
       eventName: "Subscription",
       customer: {
         id: E2E_CUSTOMER_ID,
-        name: expect.any(String),
-        email: expect.any(String),
-        avatar: expect.any(String),
         externalId: E2E_CUSTOMER_EXTERNAL_ID,
+        name: expect.any(String),
+        email: expect.any(String),
+        avatar: expect.any(String),
       },
       sale: {
         amount: sale.amount,
         currency: sale.currency,
         paymentProcessor: sale.paymentProcessor,
         invoiceId: sale.invoiceId,
         metadata: null,
       },
       amount: sale.amount,
       currency: sale.currency,
       paymentProcessor: sale.paymentProcessor,
       metadata: null,
       invoiceId: sale.invoiceId,
     });
   });
 });
📝 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.

Suggested change
describe("POST /track/sale/client", async () => {
const h = new IntegrationHarness();
const { env } = await h.init();
const sale = {
eventName: "Subscription",
amount: randomSaleAmount(),
currency: "usd",
invoiceId: `INV_${randomId()}`,
paymentProcessor: "stripe",
};
test("track a sale", async () => {
const response = await fetch(`${env.E2E_BASE_URL}/api/track/sale/client`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
},
body: JSON.stringify({}),
});
const leadResponse = await response.json();
expect(response.status).toEqual(200);
expect(leadResponse).toStrictEqual({
eventName: "Subscription",
customer: {
id: E2E_CUSTOMER_ID,
name: expect.any(String),
email: expect.any(String),
avatar: expect.any(String),
externalId: E2E_CUSTOMER_EXTERNAL_ID,
},
sale: {
amount: sale.amount,
currency: sale.currency,
paymentProcessor: sale.paymentProcessor,
invoiceId: sale.invoiceId,
metadata: null,
},
amount: sale.amount,
currency: sale.currency,
paymentProcessor: sale.paymentProcessor,
metadata: null,
invoiceId: sale.invoiceId,
});
});
});
describe("POST /track/sale/client", () => {
const h = new IntegrationHarness();
test("track a sale", async () => {
const { env } = await h.init();
const sale = {
eventName: "Subscription",
amount: randomSaleAmount(),
currency: "usd",
invoiceId: `INV_${randomId()}`,
paymentProcessor: "stripe",
};
const response = await fetch(`${env.E2E_BASE_URL}/api/track/sale/client`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.E2E_PUBLISHABLE_KEY}`,
},
body: JSON.stringify(sale),
});
const saleResponse = await response.json();
expect(response.status).toBe(200);
expect(saleResponse).toMatchObject({
eventName: "Subscription",
customer: {
id: E2E_CUSTOMER_ID,
externalId: E2E_CUSTOMER_EXTERNAL_ID,
name: expect.any(String),
email: expect.any(String),
avatar: expect.any(String),
},
sale: {
amount: sale.amount,
currency: sale.currency,
paymentProcessor: sale.paymentProcessor,
invoiceId: sale.invoiceId,
metadata: null,
},
amount: sale.amount,
currency: sale.currency,
paymentProcessor: sale.paymentProcessor,
metadata: null,
invoiceId: sale.invoiceId,
});
});
});
🤖 Prompt for AI Agents
In apps/web/tests/tracks/track-sale-client-test.ts around lines 9 to 57, the
test uses an async describe, sends an empty request body while preparing a sale
object, uses a misleading variable name, and asserts too strictly; fix by making
describe synchronous (remove async), move async initialization into a beforeAll
that awaits h.init() and sets env and sale variables, send the prepared sale
object as the POST body, rename leadResponse to saleResponse, and change strict
equality assertions to toMatchObject for the response shape to avoid brittleness
while still checking fields like eventName, customer.*, and sale.*.

@steven-tey
Copy link
Collaborator

/bug0 run

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
.github/workflows/e2e.yaml (4)

12-13: Bump actions/checkout from v2 to v4 for security and performance.

v4 includes security hardening and bugfixes. No behavior change expected for basic checkout.

Apply:

-        uses: actions/checkout@v2
+        uses: actions/checkout@v4

1-6: Set least-privilege GITHUB_TOKEN permissions at the workflow root.

Explicit permissions reduce blast radius and satisfy org security policies.

 name: Public API Tests

 on:
   deployment_status:
 
+permissions:
+  contents: read
+  # tighten further if not needed:
+  actions: read
+  checks: read
+  deployments: read
+  statuses: read
+
 jobs:

33-43: Optional: add a guard to fail fast if any required E2E secrets are missing.

Prechecking avoids confusing test failures when env vars are unset.

       - name: Run tests
         working-directory: apps/web
         env:
           E2E_BASE_URL: ${{ github.event.deployment_status.environment_url }}
           E2E_TOKEN: ${{ secrets.E2E_TOKEN }}
           E2E_TOKEN_OLD: ${{ secrets.E2E_TOKEN_OLD }}
           E2E_PUBLISHABLE_KEY: ${{ secrets.E2E_PUBLISHABLE_KEY }}
           QSTASH_TOKEN: ${{ secrets.QSTASH_TOKEN }}
           QSTASH_CURRENT_SIGNING_KEY: ${{ secrets.QSTASH_CURRENT_SIGNING_KEY }}
           NEXT_PUBLIC_NGROK_URL: ${{ github.event.deployment_status.environment_url }}
-        run: pnpm test
+        run: |
+          set -euo pipefail
+          for v in E2E_BASE_URL E2E_TOKEN E2E_PUBLISHABLE_KEY; do
+            if [ -z "${!v:-}" ]; then echo "Missing required env: $v"; exit 1; fi
+          done
+          pnpm test

3-10: Optional: add concurrency to prevent duplicate runs for the same ref/environment.

Prevents rate-limit flakiness when multiple deployments fire in quick succession.

 on:
   deployment_status:
 
+concurrency:
+  group: public-api-tests-${{ github.event.deployment_status.environment }}-${{ github.sha }}
+  cancel-in-progress: true
+
 jobs:
📜 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 bb49430 and 81dbe5c.

📒 Files selected for processing (1)
  • .github/workflows/e2e.yaml (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 (1)
.github/workflows/e2e.yaml (1)

39-39: Confirm test wiring is correct; manually verify secret exists

The workflow now correctly passes E2E_PUBLISHABLE_KEY into the test environment, and the tests consume it as expected:

  • apps/web/tests/utils/env.ts defines and parses E2E_PUBLISHABLE_KEY via integrationTestEnv.parse(process.env) (line 7).
  • apps/web/tests/tracks/track-sale-client-test.ts references it in the Authorization header (line 26).
  • apps/web/tests/tracks/track-lead-client.test.ts likewise uses env.E2E_PUBLISHABLE_KEY (line 31).

Since the CI environment in this sandbox couldn’t confirm the presence of the secret in GitHub Actions, please manually ensure that E2E_PUBLISHABLE_KEY is configured at the repository or organization level under Settings → Secrets.

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

🧹 Nitpick comments (5)
apps/web/tests/tracks/track-sale-client.test.ts (5)

36-38: Parse JSON after asserting HTTP status; also assert ok and content-type

If the endpoint ever returns a non-JSON error (or an empty body), calling response.json() first will throw and hide a clearer assertion. Check ok/status and content-type before parsing.

-    const saleResponse = await response.json();
-
-    expect(response.status).toEqual(200);
+    expect(response.ok).toBe(true);
+    expect(response.status).toBe(200);
+    expect(response.headers.get("content-type")).toContain("application/json");
+    const saleResponse = await response.json();

39-60: Use toMatchObject to make the assertion resilient to additive response fields

toStrictEqual will fail if the API adds non-breaking fields. toMatchObject keeps the test stable while still verifying the contract.

-    expect(saleResponse).toStrictEqual({
+    expect(saleResponse).toMatchObject({
       eventName: "Subscription",
       customer: {
         id: E2E_CUSTOMER_ID,
         name: expect.any(String),
         email: expect.any(String),
         avatar: expect.any(String),
         externalId: E2E_CUSTOMER_EXTERNAL_ID,
       },
       sale: {
         amount: sale.amount,
         currency: sale.currency,
         paymentProcessor: sale.paymentProcessor,
         invoiceId: sale.invoiceId,
         metadata: null,
       },
       amount: sale.amount,
       currency: sale.currency,
       paymentProcessor: sale.paymentProcessor,
       metadata: null,
       invoiceId: sale.invoiceId,
     });

38-38: Exercise new publishable-key headers: assert rate-limit and CORS header presence

PR objectives mention propagating CORS and rate-limit headers. Add assertions so regressions are caught. Header names may differ; adjust accordingly.

     expect(response.status).toBe(200);
+    // Rate limit headers
+    expect(response.headers.has("x-ratelimit-limit")).toBe(true);
+    expect(response.headers.has("x-ratelimit-remaining")).toBe(true);
+    expect(response.headers.has("x-ratelimit-reset")).toBe(true);
+    // CORS header
+    expect(response.headers.has("access-control-allow-origin")).toBe(true);

If your implementation uses different header keys (e.g., X-RateLimit-* casing or Retry-After), swap them in these checks.


8-8: Prefer consistent import style for test utilities

You’re using path aliases for helpers/resources but a relative import for IntegrationHarness. Standardize to the alias for consistency.

-import { IntegrationHarness } from "../utils/integration";
+import { IntegrationHarness } from "tests/utils/integration";

22-34: Optional: Add a negative-path test for auth and payload validation

Consider adding cases for missing/invalid publishable key and invalid payload (e.g., missing invoiceId) to cover error responses and rate-limit headers on errors as per PR goals.

I can draft a companion test (e.g., test("rejects without publishable key", ...) and test("rejects with invalid body", ...)) if you want to include them in this PR.

📜 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 81dbe5c and 0dac350.

📒 Files selected for processing (2)
  • apps/web/tests/tracks/track-lead-client.test.ts (1 hunks)
  • apps/web/tests/tracks/track-sale-client.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/tests/tracks/track-lead-client.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/tests/tracks/track-sale-client.test.ts (4)
apps/web/tests/utils/integration.ts (1)
  • IntegrationHarness (14-100)
apps/web/tests/utils/helpers.ts (2)
  • randomSaleAmount (31-33)
  • randomId (4-4)
apps/web/tests/utils/env.ts (1)
  • env (14-14)
apps/web/tests/utils/resource.ts (3)
  • E2E_TRACK_CLICK_HEADERS (10-14)
  • E2E_CUSTOMER_EXTERNAL_ID (29-29)
  • E2E_CUSTOMER_ID (28-28)
⏰ 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 (1)
apps/web/tests/tracks/track-sale-client.test.ts (1)

14-21: Good use of deterministic randomness and stable payload shape

Random amount sampling and unique invoiceId help avoid data collisions across runs, while keeping assertions straightforward.

Comment on lines +7 to +13
import { describe, expect, test } from "vitest";
import { IntegrationHarness } from "../utils/integration";

describe("POST /track/sale/client", async () => {
const h = new IntegrationHarness();
const { env } = await h.init();

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not use an async describe; initialize env in beforeAll so tests are properly registered

Vitest does not await async describe callbacks; using one can lead to tests not being collected or flaky execution. Move the async setup into beforeAll and import it from vitest.

Apply this diff:

-import { describe, expect, test } from "vitest";
+import { beforeAll, describe, expect, test } from "vitest";
@@
-describe("POST /track/sale/client", async () => {
-  const h = new IntegrationHarness();
-  const { env } = await h.init();
+describe("POST /track/sale/client", () => {
+  let env: any;
+  beforeAll(async () => {
+    const h = new IntegrationHarness();
+    ({ env } = await h.init());
+  });
📝 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.

Suggested change
import { describe, expect, test } from "vitest";
import { IntegrationHarness } from "../utils/integration";
describe("POST /track/sale/client", async () => {
const h = new IntegrationHarness();
const { env } = await h.init();
import { beforeAll, describe, expect, test } from "vitest";
import { IntegrationHarness } from "../utils/integration";
describe("POST /track/sale/client", () => {
let env: any;
beforeAll(async () => {
const h = new IntegrationHarness();
({ env } = await h.init());
});
// ...your tests here, using `env`...
});
🤖 Prompt for AI Agents
In apps/web/tests/tracks/track-sale-client.test.ts around lines 7 to 13, the
describe callback is async which Vitest does not await; move asynchronous test
setup into a beforeAll hook imported from vitest. Remove the async from
describe, declare IntegrationHarness and env variables in the outer scope, call
h = new IntegrationHarness() and await h.init() inside beforeAll to set env, and
ensure beforeAll is imported from vitest so tests are properly registered and
deterministic.

@steven-tey steven-tey merged commit a585bc5 into main Aug 21, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the publishableKey branch August 21, 2025 19:54
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.

3 participants