-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add integration tests for /track/**/client endpoints #2761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughUpdates 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
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
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 }
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
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 unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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 testThe 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 obtainresponse, 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-*orRetry-Afterchecks 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.
📒 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 thecors.tsdefinition 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.
| let headers = {}; | ||
|
|
||
| try { | ||
| const authorizationHeader = req.headers.get("Authorization"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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.
| 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, | ||
| }, | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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, | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Async 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.
| 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.*.
|
/bug0 run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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.
📒 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 existsThe workflow now correctly passes
E2E_PUBLISHABLE_KEYinto the test environment, and the tests consume it as expected:
apps/web/tests/utils/env.tsdefines and parsesE2E_PUBLISHABLE_KEYviaintegrationTestEnv.parse(process.env)(line 7).apps/web/tests/tracks/track-sale-client-test.tsreferences it in the Authorization header (line 26).apps/web/tests/tracks/track-lead-client.test.tslikewise usesenv.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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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-typeIf 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 fieldstoStrictEqual 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 presencePR 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 utilitiesYou’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 validationConsider 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.
📒 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 shapeRandom amount sampling and unique invoiceId help avoid data collisions across runs, while keeping assertions straightforward.
| 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(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
Summary by CodeRabbit
Bug Fixes
Tests
Chores
Refactor