Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds partner-profile analytics and events export GET routes, updates root events export schema to parse comma-separated columns into arrays, switches UI export buttons to use AnalyticsContext paths, and wraps the Events page with an EventsProvider. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Partner Client
participant UI as Export UI
participant API as Partner Export API
participant DB as Data/Services
participant ZIP as Archive Generator
Client->>UI: Click "Export Analytics"
UI->>UI: Resolve base path from AnalyticsContext
UI->>API: GET /partner-profile/programs/{id}/analytics/export?...
API->>DB: getProgramEnrollmentOrThrow (program, links) & access check
alt Access Denied
API-->>UI: 403 Forbidden
else Access Granted
API->>DB: Concurrent analytics fetches per endpoint
DB-->>API: analytics responses
API->>ZIP: Generate CSVs and add to ZIP
ZIP-->>API: ZIP buffer
API-->>UI: return ZIP file response
UI-->>Client: download analytics.zip
end
sequenceDiagram
participant Client as Partner Client
participant UI as Export UI
participant API as Partner Export API
participant DB as Data/Services
participant CSV as CSV Generator
Client->>UI: Click "Export Events"
UI->>UI: Resolve eventsApiPath from AnalyticsContext
UI->>API: GET /partner-profile/programs/{id}/events/export?columns=...
API->>DB: getProgramEnrollmentOrThrow & access check
alt Access Denied
API-->>UI: 403 Forbidden
else Access Granted
API->>DB: Fetch events (filtered by linkId/partner/workspace)
DB-->>API: event records
API->>API: sanitize/mask customer data, transform rows
API->>CSV: build CSV content
CSV-->>API: CSV content
API-->>UI: return CSV file response
UI-->>Client: download events.csv
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/ui/analytics/analytics-export-button.tsx (1)
41-43: Fix the error re-throwing to preserve the original message.
throw new Error(error)will stringify theerrorobject, resulting in"[object Object]"or"Error: ..."as the message. This loses the original error context.🐛 Proposed fix
} catch (error) { - throw new Error(error); + throw error instanceof Error ? error : new Error(String(error)); }Or simply re-throw directly:
} catch (error) { - throw new Error(error); + throw error; }
🤖 Fix all issues with AI agents
In @apps/web/app/(ee)/api/events/export/route.ts:
- Line 59: Remove the leftover debug logging statement
console.log("parsedParams", parsedParams) from the export route handler in
route.ts; locate where parsedParams is computed and delete that console.log call
(or replace it with a proper logger.debug/trace call if structured logging is
needed) so no console output remains in production code.
In
@apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts:
- Around line 167-177: The current object passed to .parse assigns customer.name
|| generateRandomName() into the email field when customer.email is falsy,
producing malformed "name <email>" output; change that assignment so email is an
empty string (or other valid email placeholder) when customer.email is falsy
instead of the name. Concretely, in the object built for .parse (the block
referencing customer, customer.email, customerDataSharingEnabledAt, and
generateRandomName()), ensure the email property resolves to customer.email when
present and to '' (not customer.name or generateRandomName()) when absent; keep
the existing logic that populates the name property via the conditional
...(customerDataSharingEnabledAt && { name: customer.name ||
generateRandomName() }) so names remain correct in the customer column accessor
that formats as name <email>.
- Line 83: Remove the stray debug console.log in route.ts: delete the line
console.log("parsedParams", parsedParams); from the export request handler so no
debug output is left in production; if you need retained diagnostics, replace it
with a proper logger call (e.g., processLogger.debug or requestLogger.debug)
referencing parsedParams instead of console.log.
In @apps/web/ui/analytics/events/events-export-button.tsx:
- Line 19: The debug console.log in EventsExportButton prints exportQueryString
and should be removed; locate the console.log({ exportQueryString }) statement
inside the events-export-button.tsx (within the EventsExportButton component or
related handler) and delete that line so no debug output is emitted in
production builds.
- Around line 22-24: The fetch call in EventsExportButton uses eventsApiPath
which is optional in AnalyticsContext and can be undefined in public dashboards;
add a defensive guard in the EventsExportButton component so you never call
fetch with an undefined base path: either return early / disable the export
button when eventsApiPath is falsy, or validate and throw/log a clear error
before building the URL. Locate the fetch usage around
`${eventsApiPath}/export?${exportQueryString}` and update the component (e.g.,
EventsExportButton or its click handler) to check eventsApiPath is defined and
bail out or hide/disable the UI if not.
🧹 Nitpick comments (3)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (2)
22-48: Consider extracting shared column definitions.
columnNamesandcolumnAccessorsare duplicated fromapps/web/app/(ee)/api/events/export/route.ts. Consider extracting these to a shared utility to reduce maintenance overhead.
128-135: Empty response lacks CSV headers.When
links.length === 0, the response returns an empty body. Consider returning at least the column headers for consistency with the non-empty case, so consumers can still parse the structure.apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts (1)
92-99: Consider handling the empty ZIP edge case.If all analytics endpoints return empty data, an empty ZIP file will be returned. While technically valid, this may confuse users expecting data. Consider returning an appropriate error or message.
Additionally, the
zipData as unknown as BodyInitcast suggests a type mismatch between JSZip'snodebufferoutput and the Response constructor. This works at runtime but could be cleaner.💡 Optional: Add empty ZIP handling
const zipData = await zip.generateAsync({ type: "nodebuffer" }); + // Check if any files were added to the ZIP + if (Object.keys(zip.files).length === 0) { + throw new DubApiError({ + code: "not_found", + message: "No analytics data available for the selected period.", + }); + } return new Response(zipData as unknown as BodyInit, {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/(ee)/api/events/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.tsapps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/events/page.tsxapps/web/ui/analytics/analytics-export-button.tsxapps/web/ui/analytics/events/events-export-button.tsxapps/web/ui/analytics/toggle.tsx
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2026-01-13T12:06:42.476Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3346
File: apps/web/lib/api/bounties/approve-bounty-submission.ts:165-165
Timestamp: 2026-01-13T12:06:42.476Z
Learning: Use 'noreply' as a sentinel for the replyTo email field: if replyTo === 'noreply', omit the replyTo property by spreading an empty object. Call sites can use replyTo: someEmail || 'noreply' to conditionally set replyTo without null/undefined. This pattern is broadly applicable across the TypeScript codebase; ensure this behavior is documented and that readability is preserved, using explicit conditional logic if the intent may be unclear.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.tsapps/web/app/(ee)/api/events/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.
Applied to files:
apps/web/ui/analytics/toggle.tsxapps/web/ui/analytics/events/events-export-button.tsxapps/web/ui/analytics/analytics-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
🧬 Code graph analysis (5)
apps/web/ui/analytics/toggle.tsx (1)
apps/web/ui/analytics/analytics-options.tsx (1)
AnalyticsOptions(8-39)
apps/web/ui/analytics/events/events-export-button.tsx (1)
apps/web/ui/analytics/analytics-provider.tsx (1)
AnalyticsContext(56-99)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/events/page.tsx (1)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent(10-39)
apps/web/ui/analytics/analytics-export-button.tsx (1)
apps/web/ui/analytics/analytics-provider.tsx (1)
AnalyticsContext(56-99)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (4)
apps/web/lib/types.ts (2)
LeadEvent(545-545)SaleEvent(543-543)apps/web/lib/constants/partner-profile.ts (3)
LARGE_PROGRAM_IDS(7-7)LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS(8-8)MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING(5-5)apps/web/lib/api/errors.ts (1)
DubApiError(43-60)apps/web/lib/zod/schemas/partner-profile.ts (2)
partnerProfileEventsQuerySchema(115-123)PartnerProfileLinkSchema(79-94)
🔇 Additional comments (7)
apps/web/ui/analytics/toggle.tsx (1)
271-271: LGTM!Rendering
AnalyticsOptionsunconditionally within this block enables export functionality for partner pages, which aligns with the new partner export routes introduced in this PR.apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/events/page.tsx (1)
8-10: LGTM!Wrapping
EventswithEventsProvideris necessary for the export button to accessexportQueryStringviaEventsContext.apps/web/app/(ee)/api/events/export/route.ts (1)
51-56: LGTM on schema update.Using
.extend()instead of.and()is the correct approach in Zod 4 for adding fields to an existing schema.apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts (3)
28-36: LGTM!The access control logic correctly gates this feature for large programs based on total commissions, preventing abuse while allowing qualified partners access.
42-61: LGTM!The link validation logic properly handles both
linkIdanddomain/keycombinations, ensuring the partner can only access their own links. The early return pattern withDubApiErrorprovides clear error messages.
65-90: LGTM!The concurrent analytics fetching with
Promise.allis efficient. The conditional skipping oftop_linkswhen a single link is specified andcountendpoint is a good optimization. Empty response handling prevents empty CSVs.apps/web/ui/analytics/analytics-export-button.tsx (1)
18-23: Consider usingbaseApiPathconsistently for both paths, or verify that the hardcoded endpoint works for all contexts.The current logic uses
baseApiPathfor partner pages but hardcodes/api/analytics/exportfor non-partner pages. Given thatbaseApiPathvaries across contexts (admin, workspace, dashboard), this inconsistency may be unintentional. IfbaseApiPathis meant to provide the correct API path in all contexts, consider using it consistently:const exportPath = partnerPage ? `${baseApiPath}/export` - : "/api/analytics/export"; + : `${baseApiPath}/export`;However, if the hardcoded path is intentionally a universal endpoint for non-partner analytics (while different
baseApiPathvalues are used elsewhere), this is acceptable. Verify that admin and dashboard analytics contexts (wherebaseApiPathdiffers) either do not use this export button, or that the hardcoded/api/analytics/exportendpoint is intentionally the correct path for them.
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
Outdated
Show resolved
Hide resolved
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
Show resolved
Hide resolved
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
@apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts:
- Around line 165-176: The current assignment sets email to customer.name ||
generateRandomName() when customer.email is falsy, which puts a name into the
email field; change this to produce an anonymized placeholder email instead
(e.g. use generateRandomName() combined with a fixed domain like "@anon.invalid"
or "@no-reply.example" so the email field remains syntactically an email).
Update the .parse({... customer, email: ...}) block that references
customer.email/customer.name/generateRandomName() to return that placeholder
when customer.email is missing, or alternatively adjust the consumer (the column
accessor that renders "Name <Email>") to detect missing emails and render only
the name when no valid email exists.
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (1)
22-48: Consider extracting shared column definitions to reduce duplication.
columnNamesandcolumnAccessorsare duplicated fromapps/web/app/(ee)/api/events/export/route.ts. Extract these to a shared module (e.g.,@/lib/analytics/export-columns) to maintain consistency and reduce maintenance burden.// Example: @/lib/analytics/export-columns.ts import { ClickEvent, LeadEvent, SaleEvent } from "@/lib/types"; import { COUNTRIES, capitalize } from "@dub/utils"; export type EventRow = ClickEvent | LeadEvent | SaleEvent; export const columnNames: Record<string, string> = { trigger: "Event", url: "Destination URL", // ... rest of mappings }; export const columnAccessors: Record<string, (r: EventRow) => unknown> = { // ... accessors };
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/app/(ee)/api/events/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.tsapps/web/ui/analytics/events/events-export-button.tsx
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-11-17T05:19:11.972Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
📚 Learning: 2026-01-13T12:06:42.476Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3346
File: apps/web/lib/api/bounties/approve-bounty-submission.ts:165-165
Timestamp: 2026-01-13T12:06:42.476Z
Learning: Use 'noreply' as a sentinel for the replyTo email field: if replyTo === 'noreply', omit the replyTo property by spreading an empty object. Call sites can use replyTo: someEmail || 'noreply' to conditionally set replyTo without null/undefined. This pattern is broadly applicable across the TypeScript codebase; ensure this behavior is documented and that readability is preserved, using explicit conditional logic if the intent may be unclear.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.tsapps/web/app/(ee)/api/events/export/route.ts
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (5)
apps/web/lib/auth/partner.ts (1)
withPartnerProfile(48-179)apps/web/lib/constants/partner-profile.ts (3)
LARGE_PROGRAM_IDS(7-7)LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS(8-8)MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING(5-5)apps/web/lib/api/errors.ts (1)
DubApiError(43-60)apps/web/lib/zod/schemas/partner-profile.ts (2)
partnerProfileEventsQuerySchema(115-123)PartnerProfileLinkSchema(79-94)apps/web/lib/analytics/get-events.ts (1)
getEvents(30-176)
⏰ 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 (6)
apps/web/app/(ee)/api/events/export/route.ts (1)
50-57: LGTM! Clean refactor from.and()to.extend().The schema change is cleaner. Note that
columnsis required here (no.optional()), while the new partner-profile export route makes it optional with sensible defaults. Consider aligning the behavior if this endpoint should also support default columns when none are specified.apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (4)
63-71: Good access control for large programs.The restriction based on
LARGE_PROGRAM_IDSand minimum commission threshold is a sensible gate for resource-intensive exports.
105-133: Good security validation for link ownership.Properly validates that the requested
linkIdordomain/keybelongs to the partner's enrolled links before fetching events. The empty links edge case is handled cleanly with an empty CSV response.
147-157: Good privacy hygiene stripping IP addresses.The IP address removal for partner-facing exports is appropriate. The
@ts-ignorecomment for the deprecatedipfield is acceptable—consider adding a brief note explaining why it's needed.
135-145: Smart filtering optimization based on link count.The conditional filtering logic is well-designed—using
partnerIdfor partners with many links avoids bloatedlinkIdsarrays. Theeventparameter is correctly passed togetEventsvia the...restspread operator, and thegetEventsfunction expects it (renamed aseventTypeinternally), so this is working as intended.apps/web/ui/analytics/events/events-export-button.tsx (1)
13-28: The code is safe—EventsExportButtoncan only be rendered in contexts whereAnalyticsProvidersupplieseventsApiPath. It's structurally impossible for it to receive an undefined value.
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
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/ui/analytics/analytics-export-button.tsx (1)
15-44: Loading state not reset on error path after line 32.If an error occurs during blob processing (lines 35-40),
setLoading(false)on line 44 is never reached because line 42 throws. This leaves the button in a loading state.🐛 Proposed fix using finally block
async function exportData() { setLoading(true); try { // Use partner profile export endpoint if on partner page, otherwise use regular export endpoint const exportPath = partnerPage ? `${baseApiPath}/export` : "/api/analytics/export"; const response = await fetch(`${exportPath}?${queryString}`, { method: "GET", headers: { "Content-Type": "application/json", }, }); if (!response.ok) { - setLoading(false); throw new Error(response.statusText); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `Dub Analytics Export - ${new Date().toISOString()}.zip`; a.click(); } catch (error) { - throw new Error(error); + throw error instanceof Error ? error : new Error(String(error)); + } finally { + setLoading(false); } - setLoading(false); }
🤖 Fix all issues with AI agents
In
@apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts:
- Around line 63-90: The loop should early-return when there are no links so we
don't call getAnalytics with linkIds: []; add a check before the Promise.all
(using links.length === 0 and linkId absence) to either return an empty CSV/zip
response or skip generating analytics for link-based endpoints; ensure the check
references VALID_ANALYTICS_ENDPOINTS, linkId, links, and
MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING so the downstream logic that builds the
getAnalytics args (including partnerId vs linkIds) is not invoked with an empty
links array.
In
@apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts:
- Around line 158-176: The customer transform omits name when
customerDataSharingEnabledAt is falsy and wrongly assigns customer.name to the
email fallback, causing "undefined <email>" in CSV; update the customer z.object
and parsed payload so the parsed customer always includes a name (e.g., make
name present or optional in the schema and set name: customer.name ||
generateRandomName()), and change the email fallback to a proper generated email
placeholder (use generateRandomEmail() or construct a placeholder like
`${generateRandomName()}@example.com`) instead of customer.name; ensure this
fixes the accessor that reads r.customer.name.
🧹 Nitpick comments (3)
apps/web/app/(ee)/api/events/export/route.ts (1)
50-56: Consider handling emptycolumnsstring edge case.If
columnsis an empty string,"".split(",")returns[""](an array containing one empty string), not an empty array. This could cause unexpected behavior in the data mapping at lines 105-111, where an empty string key would not match anycolumnAccessorsentry.♻️ Suggested fix to handle empty string
columns: z .string() - .transform((c) => c.split(",")) + .transform((c) => (c ? c.split(",") : [])) .pipe(z.string().array()),Alternatively, add
.min(1)validation before the transform if empty columns should be rejected.apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (2)
34-48: Type-safe column accessors may fail at runtime for mismatched event types.Several accessors are typed for specific event subtypes (e.g.,
urlforClickEvent,customerforLeadEvent | SaleEvent), butRowis a union of all three. If a user passes custom columns that don't match the event type, the accessor could access non-existent properties.The fallback
?? row?.[c]at line 184 mitigates runtime errors, but the result would beundefined. Consider adding runtime guards or restricting column options per event type in the schema.
191-196: Consider usingtext/csvMIME type.The standard MIME type for CSV per RFC 4180 is
text/csv. Whileapplication/csvworks in most browsers,text/csvis more widely recognized.Suggested change
return new Response(csvData, { headers: { - "Content-Type": "application/csv", + "Content-Type": "text/csv", "Content-Disposition": `attachment; filename=${event}_export.csv`, }, });
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/(ee)/api/events/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.tsapps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/events/page.tsxapps/web/ui/analytics/analytics-export-button.tsxapps/web/ui/analytics/events/events-export-button.tsxapps/web/ui/analytics/toggle.tsx
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-11-17T05:19:11.972Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
📚 Learning: 2026-01-13T12:06:42.476Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3346
File: apps/web/lib/api/bounties/approve-bounty-submission.ts:165-165
Timestamp: 2026-01-13T12:06:42.476Z
Learning: Use 'noreply' as a sentinel for the replyTo email field: if replyTo === 'noreply', omit the replyTo property by spreading an empty object. Call sites can use replyTo: someEmail || 'noreply' to conditionally set replyTo without null/undefined. This pattern is broadly applicable across the TypeScript codebase; ensure this behavior is documented and that readability is preserved, using explicit conditional logic if the intent may be unclear.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.tsapps/web/app/(ee)/api/events/export/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts
📚 Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.
Applied to files:
apps/web/ui/analytics/analytics-export-button.tsxapps/web/ui/analytics/events/events-export-button.tsxapps/web/ui/analytics/toggle.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/analytics/events/events-export-button.tsx
🧬 Code graph analysis (5)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (6)
apps/web/lib/auth/partner.ts (1)
withPartnerProfile(48-179)apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-67)apps/web/lib/constants/partner-profile.ts (3)
LARGE_PROGRAM_IDS(7-7)LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS(8-8)MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING(5-5)apps/web/lib/api/errors.ts (1)
DubApiError(43-60)apps/web/lib/zod/schemas/partner-profile.ts (2)
partnerProfileEventsQuerySchema(115-123)PartnerProfileLinkSchema(79-94)apps/web/lib/analytics/get-events.ts (1)
getEvents(30-176)
apps/web/ui/analytics/analytics-export-button.tsx (1)
apps/web/ui/analytics/analytics-provider.tsx (1)
AnalyticsContext(56-99)
apps/web/ui/analytics/events/events-export-button.tsx (1)
apps/web/ui/analytics/analytics-provider.tsx (1)
AnalyticsContext(56-99)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/events/page.tsx (1)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent(10-39)
apps/web/ui/analytics/toggle.tsx (1)
apps/web/ui/analytics/analytics-options.tsx (1)
AnalyticsOptions(8-39)
🔇 Additional comments (8)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/events/page.tsx (1)
2-10: LGTM!The
EventsProviderwrapper is correctly added to provide the necessary context (exportQueryString) for theEventsExportButtoncomponent used within the Events page. This aligns with the broader PR changes enabling partner analytics/events export.apps/web/ui/analytics/toggle.tsx (1)
271-271: LGTM!Removing the conditional rendering allows
AnalyticsOptions(containing export buttons) to render on partner pages. This aligns with the PR's goal of enabling partner analytics/events export, and the export button components have been updated to handle partner pages via context-based API paths.apps/web/ui/analytics/events/events-export-button.tsx (1)
14-28: No action needed.eventsApiPathis always defined whenEventsExportButtonis rendered. The component is only used in three routes — workspace analytics (/[slug]/events), admin (/admin/events), and partner program events — all of which initializeeventsApiPathbefore rendering. There is no path through the codebase whereEventsExportButtoncan render with an undefined or emptyeventsApiPath.apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts (3)
50-71: LGTM!Authentication via
withPartnerProfile, enrollment validation, and the large program access check are correctly implemented and consistent with the analytics export route.
73-103: LGTM!The query parameter parsing with the columns transform and default column mappings per event type are well structured.
105-133: LGTM!Link validation logic correctly checks ownership before allowing export, and the empty links case returns a well-formed empty CSV response.
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts (2)
16-61: LGTM!The enrollment fetch, large program access check, and link validation are correctly implemented and consistent with the events export route.
92-100: LGTM!The ZIP response is correctly constructed. The
as unknown as BodyInitcast is a common pattern to bridge Node.jsBuffertypes with web APIBodyInit.
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts
Show resolved
Hide resolved
apps/web/app/(ee)/api/partner-profile/programs/[programId]/events/export/route.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts (1)
77-102: Consider usingPromise.allSettledfor resilience.If
getAnalyticsthrows for any single endpoint, the entire export fails. UsingPromise.allSettledwould allow partial exports when some endpoints succeed while others fail, providing a better user experience.♻️ Optional: Use Promise.allSettled for partial export resilience
- await Promise.all( + const results = await Promise.allSettled( VALID_ANALYTICS_ENDPOINTS.map(async (endpoint) => { // no need to fetch top links data if there's a link specified // since this is just a single link if (endpoint === "top_links" && linkId) return; // skip clicks count if (endpoint === "count") return; const response = await getAnalytics({ ...rest, workspaceId: program.workspaceId, ...(linkId ? { linkId } : links.length > MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING ? { partnerId: partner.id } : { linkIds: links.map((link) => link.id) }), dataAvailableFrom: program.startedAt ?? program.createdAt, groupBy: endpoint, }); if (!response || response.length === 0) return; - const csvData = convertToCSV(response); - zip.file(`${endpoint}.csv`, csvData); + return { endpoint, response }; }), ); + + for (const result of results) { + if (result.status === "fulfilled" && result.value) { + const { endpoint, response } = result.value; + const csvData = convertToCSV(response); + zip.file(`${endpoint}.csv`, csvData); + } + }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts
📚 Learning: 2026-01-13T12:06:42.476Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3346
File: apps/web/lib/api/bounties/approve-bounty-submission.ts:165-165
Timestamp: 2026-01-13T12:06:42.476Z
Learning: Use 'noreply' as a sentinel for the replyTo email field: if replyTo === 'noreply', omit the replyTo property by spreading an empty object. Call sites can use replyTo: someEmail || 'noreply' to conditionally set replyTo without null/undefined. This pattern is broadly applicable across the TypeScript codebase; ensure this behavior is documented and that readability is preserved, using explicit conditional logic if the intent may be unclear.
Applied to files:
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.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: build
🔇 Additional comments (4)
apps/web/app/(ee)/api/partner-profile/programs/[programId]/analytics/export/route.ts (4)
16-36: LGTM!The access control logic properly validates program enrollment and enforces commission-based restrictions for large programs before allowing export access.
38-61: LGTM!The link resolution logic correctly validates that the requested link belongs to the partner's enrolled links, preventing unauthorized access to other partners' analytics data.
63-73: The early return logic is correct.The
&& !linkIdcondition is technically redundant (iflinks.length === 0and a linkId was requested, anot_founderror would have already been thrown), but it makes the intent clearer and serves as a defensive check.
104-112: LGTM!The response correctly sets the Content-Type and Content-Disposition headers for a ZIP file download.
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.