Skip to content

Conversation

zhyd1997
Copy link

@zhyd1997 zhyd1997 commented Sep 6, 2025

Close #2546

import * as z from "zod";

const baz = z
    .object({
        a: z.string(),
    });

const foo = z
    .object({
        b: z.string(),
    })
    .merge(baz);

const bar = foo
    // Try to comment this out and see the type of y
    .omit({ s: true })
    .extend({
        c: z.string(),
    })

const x: Record<string, string> = {}

// check the type of y here
const y = bar.parse(x);

Summary by CodeRabbit

  • New Features

    • Events reports: sorting and pagination are now supported in event filters and queries.
    • Partner profile queries can now accept workspace selection for analytics and event requests.
    • Bulk link updates may now include link IDs; partner creation accepts project IDs in link properties.
  • Bug Fixes

    • General analytics filters no longer expose an unsupported sort option.
    • Payout count responses may include aggregated counts; exclusion of the current month was removed.

Copy link
Contributor

vercel bot commented Sep 6, 2025

@zhyd1997 is attempting to deploy a commit to the Dub Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLAassistant commented Sep 6, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

Walkthrough

Updated multiple Zod schemas across analytics, partner-profile, links, partners, payouts and analytics types: several fields were removed from omit/pick lists or reintroduced, altering public shapes for analytics filters/queries, partner-profile queries, link bulk updates, partner creation linkProps, payouts query/response, and partner events types.

Changes

Cohort / File(s) Summary
Analytics Zod Schemas
apps/web/lib/zod/schemas/analytics.ts
Removed sortBy from analyticsFilterTB pick; changed eventsFilterTB .omit to only exclude granularity and timezone (now permits page and sortBy); changed eventsQuerySchema .omit to exclude only groupBy (keeps sortBy).
Partner-profile Zod Schemas
apps/web/lib/zod/schemas/partner-profile.ts
Removed workspaceId from the .omit lists in partnerProfileAnalyticsQuerySchema and partnerProfileEventsQuerySchema, allowing workspaceId in those query inputs.
Analytics types
apps/web/lib/analytics/types.ts
Removed groupBy from partnerEventsSchema.pick, dropping groupBy from the inferred PartnerEventsFilters type.
Links Zod Schema
apps/web/lib/zod/schemas/links.ts
bulkUpdateLinksBodySchema no longer omits id in the data payload (id may now be present).
Partners Zod Schema
apps/web/lib/zod/schemas/partners.ts
In createPartnerSchema, linkProps omit list no longer excludes projectId, allowing projectId in linkProps.
Payouts Zod Schema
apps/web/lib/zod/schemas/payouts.ts
Removed excludeCurrentMonth from payoutsCountQuerySchema pick; removed _count from omit list in PartnerPayoutResponseSchema, allowing _count in responses.
API route TS checks
apps/web/app/api/analytics/route.ts, apps/web/app/api/resend/webhook/route.ts
Added // @ts-expect-error comments to suppress TypeScript/Zod type-check issues at specific lines; no runtime logic changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix build error by ensuring parsed params include domain for events export route (#2546) This PR does not add or expose domain on the schemas used by the export route; change set focuses on other fields.
Align analytics/events query schemas with route expectations for domain/interval/start/end/columns/key/folderId (#2546) PR modifies sortBy/page/workspaceId omissions only; whether the export route's specific schema now includes domain etc. cannot be confirmed from these edits alone.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Removed sortBy from analyticsFilterTB (apps/web/lib/zod/schemas/analytics.ts) Linked issue requires domain in export route params; this change concerns sorting fields, unrelated to domain.
Allowed id in bulk update payload (apps/web/lib/zod/schemas/links.ts) Change affects link bulk-update shape, not the build error or export route schema referenced in the issue.
Reintroduced workspaceId in partner-profile queries (apps/web/lib/zod/schemas/partner-profile.ts) Enables workspaceId for partner-profile queries; unrelated to the domain parsing/build-failure objective.

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

I twitch my ears at schemas’ tune,
SortBy hops out, page hops in soon.
WorkspaceId peeks from its den,
IDs return and fields align again.
Hop on—let types and builds befriend! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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 (3)
apps/web/lib/zod/schemas/analytics.ts (3)

349-353: Drop optional() when using default() for sortBy

default() already makes the key optional-with-default in Zod. Small cleanup for clarity.

-    sortBy: z
-      .enum(["timestamp"])
-      .optional()
-      .default("timestamp")
+    sortBy: z.enum(["timestamp"]).default("timestamp")

331-335: Same cleanup for sortOrder: default() is enough

Removes redundant optional() for consistency with the sortBy change.

-const sortOrder = z
-  .enum(["asc", "desc"])
-  .default("desc")
-  .optional()
+const sortOrder = z.enum(["asc", "desc"]).default("desc")

337-357: Optional: enforce precedence between deprecated order and sortOrder

Today both exist; ambiguity can leak downstream. Consider normalizing in schema so sortOrder always wins, then fallback to order, then default.

 export const eventsQuerySchema = analyticsQuerySchema
   .omit({ groupBy: true })
   .extend({
@@
     order: sortOrder
       .describe("DEPRECATED. Use `sortOrder` instead.")
       .openapi({ deprecated: true }),
-  });
+  })
+  .transform((v) => ({
+    ...v,
+    sortOrder: v.sortOrder ?? v.order ?? "desc",
+  }));
📜 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 e643135 and 47da64c.

📒 Files selected for processing (1)
  • apps/web/lib/zod/schemas/analytics.ts (2 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: Vade Review
🔇 Additional comments (2)
apps/web/lib/zod/schemas/analytics.ts (2)

320-329: eventsFilterTB omit change LGTM

Omitting only granularity/timezone (and not page/sortBy) is consistent with the current analyticsFilterTB shape and the Tinybird events usage.


337-357: Keep domain in eventsQuerySchema — omit only groupBy

analyticsQuerySchema.omit({ groupBy: true }) preserves domain and other analytics filters used by the export/events routes; parsedParams still includes domain so the TS destructuring error is resolved.
Location: apps/web/lib/zod/schemas/analytics.ts (eventsQuerySchema).

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

Caution

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

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

28-30: Use a constant‑time signature compare.

Plain string inequality is timing-attack prone. Compare HMACs with timingSafeEqual.

-  if (signature !== svix_signature) {
+  const a = Buffer.from(signature ?? "", "base64");
+  const b = Buffer.from(svix_signature ?? "", "base64");
+  if (a.length !== b.length || !timingSafeEqual(a, b)) {
     return NextResponse.json({ message: "Invalid signature" }, { status: 400 });
   }
🧹 Nitpick comments (7)
apps/web/app/api/resend/webhook/route.ts (4)

10-11: Parse svix-signature robustly (support “v1,” and “v1=”).

The current split is brittle and fails if multiple versions are sent. Extract the first v1 signature via regex.

-  const svix_signature = req.headers.get("svix-signature")?.split("v1,")[1];
+  const rawSig = req.headers.get("svix-signature") || "";
+  const svix_signature = rawSig.match(/(?:^|,)\s*v1[=,]([^,]+)/)?.[1];

34-35: Guard JSON parsing to avoid 500s on bad payloads.

JSON.parse will throw on malformed JSON.

-  const {
-    data: { email, unsubscribed },
-  } = JSON.parse(rawBody) || {};
+  let email: string | undefined, unsubscribed: boolean | undefined;
+  try {
+    const parsed = JSON.parse(rawBody) || {};
+    ({ data: { email, unsubscribed } = {} } = parsed);
+  } catch {
+    return NextResponse.json({ message: "Invalid JSON" }, { status: 400 });
+  }

41-49: Avoid 500 on missing user record.

prisma.user.update throws if no row matches. Use updateMany or handle P2025.

-    await prisma.user.update({
+    await prisma.user.updateMany({
       where: {
         email,
       },
       data: {
         subscribed: !unsubscribed,
       },
     });

36-38: Reduce PII in logs.

Consider structured logs without raw emails, or gate behind an env flag.

apps/web/app/api/analytics/route.ts (3)

29-33: Eliminate ts-expect-error with a type guard.

Narrow oldEvent to a valid endpoint before reassignment; no suppression needed.

-    // @ts-expect-error FIXME zod transform error
-    if (!oldType && oldEvent && VALID_ANALYTICS_ENDPOINTS.includes(oldEvent)) {
+    const isAnalyticsEndpoint = (
+      s: unknown,
+    ): s is (typeof VALID_ANALYTICS_ENDPOINTS)[number] =>
+      typeof s === "string" &&
+      (VALID_ANALYTICS_ENDPOINTS as readonly string[]).includes(s as any);
+
+    if (!oldType && isAnalyticsEndpoint(oldEvent)) {
       oldType = oldEvent;
       oldEvent = undefined;
     }

53-57: Avoid reassignment with casts; derive effective params instead.

This sidesteps Zod-transform typing friction and keeps types intact without suppressions.

-    // @ts-expect-error FIXME
-    event = oldEvent || event;
-    // @ts-expect-error FIXME
-    groupBy = oldType || groupBy;
+    const effectiveEvent = (oldEvent ?? event);
+    const effectiveGroupBy = (oldType ?? groupBy);

120-133: Thread effective params through the call and timeseries check.

Use the derived variables to keep type consistency throughout.

-    const response = await getAnalytics({
-      ...parsedParams,
-      event,
-      groupBy,
+    const response = await getAnalytics({
+      ...parsedParams,
+      event: effectiveEvent,
+      groupBy: effectiveGroupBy,
       ...(link && { linkId: link.id }),
       folderIds,
       workspaceId: workspace.id,
       isMegaFolder: workspace.totalLinks > 1_000_000,
       isDeprecatedClicksEndpoint,
       // dataAvailableFrom is only relevant for timeseries groupBy
-      ...(groupBy === "timeseries" && {
+      ...(effectiveGroupBy === "timeseries" && {
         dataAvailableFrom: workspace.createdAt,
       }),
     });

Also applies to: 130-133

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67db4e6 and 539b3b0.

📒 Files selected for processing (6)
  • apps/web/app/api/analytics/route.ts (2 hunks)
  • apps/web/app/api/resend/webhook/route.ts (1 hunks)
  • apps/web/lib/analytics/types.ts (0 hunks)
  • apps/web/lib/zod/schemas/links.ts (0 hunks)
  • apps/web/lib/zod/schemas/partners.ts (0 hunks)
  • apps/web/lib/zod/schemas/payouts.ts (0 hunks)
💤 Files with no reviewable changes (4)
  • apps/web/lib/zod/schemas/payouts.ts
  • apps/web/lib/analytics/types.ts
  • apps/web/lib/zod/schemas/partners.ts
  • apps/web/lib/zod/schemas/links.ts
⏰ 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: Vade Review
🔇 Additional comments (1)
apps/web/app/api/analytics/route.ts (1)

35-50: No action required: domain is defined on analyticsQuerySchema (line 73–76) and retained by eventsQuerySchema (line 337), so it’s parsed across all callers.

Comment on lines 20 to 22
// Need to base64 decode the secret
const secretBytes = Buffer.from(secret.split("_")[1], "base64");
const signature = crypto
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

Harden secret decoding; current split can throw.

secret.split("_")[1] can be undefined if the format changes, causing a 500. Validate before decoding.

-  // Need to base64 decode the secret
-  const secretBytes = Buffer.from(secret.split("_")[1], "base64");
+  // Need to base64 decode the secret (expecting "resend_<base64>")
+  const parts = secret.split("_");
+  if (parts.length < 2 || !parts[1]) {
+    return NextResponse.json({ message: "Invalid secret" }, { status: 400 });
+  }
+  const secretBytes = Buffer.from(parts[1], "base64");
📝 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
// Need to base64 decode the secret
const secretBytes = Buffer.from(secret.split("_")[1], "base64");
const signature = crypto
// Need to base64 decode the secret (expecting "resend_<base64>")
const parts = secret.split("_");
if (parts.length < 2 || !parts[1]) {
return NextResponse.json({ message: "Invalid secret" }, { status: 400 });
}
const secretBytes = Buffer.from(parts[1], "base64");
const signature = crypto
🤖 Prompt for AI Agents
In apps/web/app/api/resend/webhook/route.ts around lines 20 to 22, the code
directly uses secret.split("_")[1] which can be undefined and cause Buffer.from
to throw; validate the secret format before decoding by checking secret is a
string, contains an underscore, and that split yields a non-empty second
segment, and handle the invalid case gracefully (return a 400/unauthorized
response or log and abort) instead of letting it throw; then safely
base64-decode the validated segment (and wrap decoding in a try/catch to handle
malformed base64) and proceed to compute the signature.

Comment on lines +23 to 26
// @ts-expect-error FIXME
.createHmac("sha256", secretBytes)
.update(signedContent)
.digest("base64");
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

Remove ts-expect-error; use Node crypto explicitly and mark runtime.

TypeScript is likely resolving crypto to the Web Crypto Crypto type (no createHmac). Avoid the suppression by:

  • Importing from node:crypto with named APIs.
  • Declaring the route’s runtime as nodejs.
-import crypto from "crypto";
+import { createHmac, timingSafeEqual } from "node:crypto";

+export const runtime = "nodejs";
...
-  const signature = crypto
-  // @ts-expect-error FIXME
-    .createHmac("sha256", secretBytes)
+  const signature = createHmac("sha256", secretBytes)
     .update(signedContent)
     .digest("base64");

Also applies to: 2-2

🤖 Prompt for AI Agents
In apps/web/app/api/resend/webhook/route.ts around lines 23 to 26, remove the
"// @ts-expect-error FIXME" and stop relying on the ambient Web Crypto type;
instead import the Node API explicitly (e.g., import { createHmac } from
"node:crypto") and use that named function to compute the HMAC, and declare the
route runtime as Node.js by exporting the runtime constant (export const runtime
= "nodejs") at the top of the file so TypeScript resolves node:crypto correctly
at runtime.

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.

Unable to build
2 participants