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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import { getAnalytics } from "@/lib/analytics/get-analytics";
import { DubApiError } from "@/lib/api/errors";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { withPartnerProfile } from "@/lib/auth/partner";
import { LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS } from "@/lib/partners/constants";
import { partnerProfileAnalyticsQuerySchema } from "@/lib/zod/schemas/partner-profile";
import { NextResponse } from "next/server";

// GET /api/partner-profile/programs/[programId]/analytics – get analytics for a program enrollment link
export const GET = withPartnerProfile(
async ({ partner, params, searchParams }) => {
const { program, links } = await getProgramEnrollmentOrThrow({
partnerId: partner.id,
programId: params.programId,
include: {
program: true,
links: true,
},
});
const { program, links, totalCommissions } =
await getProgramEnrollmentOrThrow({
partnerId: partner.id,
programId: params.programId,
include: {
program: true,
links: true,
},
});

let { linkId, domain, key, ...rest } =
partnerProfileAnalyticsQuerySchema.parse(searchParams);
Expand All @@ -42,7 +44,8 @@ export const GET = withPartnerProfile(
}

const response = await getAnalytics({
...(program.id === "prog_1K0QHV7MP3PR05CJSCF5VN93X"
...(program.id === "prog_1K0QHV7MP3PR05CJSCF5VN93X" &&
totalCommissions < LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS
? { event: rest.event, groupBy: "count", interval: "all" }
: rest),
workspaceId: program.workspaceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DubApiError } from "@/lib/api/errors";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { withPartnerProfile } from "@/lib/auth/partner";
import { generateRandomName } from "@/lib/names";
import { LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS } from "@/lib/partners/constants";
import { PartnerProfileCustomerSchema } from "@/lib/zod/schemas/partner-profile";
import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";
Expand All @@ -13,7 +14,7 @@ import { z } from "zod";
export const GET = withPartnerProfile(async ({ partner, params }) => {
const { customerId, programId } = params;

const { program, links, customerDataSharingEnabledAt } =
const { program, links, totalCommissions, customerDataSharingEnabledAt } =
await getProgramEnrollmentOrThrow({
partnerId: partner.id,
programId: programId,
Expand All @@ -23,7 +24,10 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
},
});

if (program.id === "prog_1K0QHV7MP3PR05CJSCF5VN93X") {
if (
program.id === "prog_1K0QHV7MP3PR05CJSCF5VN93X" &&
totalCommissions < LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS
) {
throw new DubApiError({
code: "forbidden",
message: "This feature is not available for your program.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DubApiError } from "@/lib/api/errors";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { withPartnerProfile } from "@/lib/auth/partner";
import { generateRandomName } from "@/lib/names";
import { LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS } from "@/lib/partners/constants";
import {
PartnerProfileLinkSchema,
partnerProfileEventsQuerySchema,
Expand All @@ -13,7 +14,7 @@ import { z } from "zod";
// GET /api/partner-profile/programs/[programId]/events – get events for a program enrollment link
export const GET = withPartnerProfile(
async ({ partner, params, searchParams }) => {
const { program, links, customerDataSharingEnabledAt } =
const { program, links, totalCommissions, customerDataSharingEnabledAt } =
await getProgramEnrollmentOrThrow({
partnerId: partner.id,
programId: params.programId,
Expand All @@ -23,7 +24,10 @@ export const GET = withPartnerProfile(
},
});

if (program.id === "prog_1K0QHV7MP3PR05CJSCF5VN93X") {
if (
program.id === "prog_1K0QHV7MP3PR05CJSCF5VN93X" &&
totalCommissions < LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS
) {
throw new DubApiError({
code: "forbidden",
message: "This feature is not available for your program.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import Analytics from "@/ui/analytics";
import { PageContent } from "@/ui/layout/page-content";
import { redirect } from "next/navigation";

export default async function PartnerAnalytics({
params,
}: {
params: Promise<{ programSlug: string }>;
}) {
const { programSlug } = await params;
if (programSlug === "perplexity") {
redirect(`/programs/${programSlug}`);
}
export default function PartnerAnalytics() {
return (
<PageContent title="Analytics">
<Analytics />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { memo } from "react";
import useSWR from "swr";

export function ProgramCustomerPageClient() {
const { programEnrollment } = useProgramEnrollment();
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the loading state before using showDetailedAnalytics.

The showDetailedAnalytics flag depends on programEnrollment.totalCommissions, which loads asynchronously. The useProgramEnrollment hook returns a loading state, but it's not being destructured or checked here. Consider destructuring the loading state to avoid potential race conditions.

Check if the redirect logic at line 30 should also wait for the program enrollment data to finish loading:


🏁 Script executed:

#!/bin/bash
# Check how other components use the loading state from useProgramEnrollment
rg -nP -C3 'useProgramEnrollment.*loading' --type=tsx --type=ts

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Check how other components use useProgramEnrollment hook
rg -nP -C3 'useProgramEnrollment' --type=ts

Length of output: 40447


🏁 Script executed:

#!/bin/bash
# Find the useProgramEnrollment hook definition
fd -e ts -e tsx | xargs grep -l 'useProgramEnrollment' | head -5

Length of output: 225


🏁 Script executed:

#!/bin/bash
# Check the redirect logic around line 30 in the page-client file
rg -nP -B5 -A5 'redirect' apps/web/app/\(ee\)/partners.dub.co/\(dashboard\)/programs/\[programSlug\]/\(enrolled\)/customers/\[customerId\]/page-client.tsx

Length of output: 879


🏁 Script executed:

#!/bin/bash
# Find and display the useProgramEnrollment hook implementation
cat apps/web/lib/swr/use-program-enrollment.ts

Length of output: 1117


Check the loading state from useProgramEnrollment before redirecting based on showDetailedAnalytics.

The useProgramEnrollment hook returns a loading state, but the redirect condition at line 30 only checks isLoading from usePartnerCustomer. During initial load, programEnrollment is undefined, so totalCommissions defaults to 0, which can make showDetailedAnalytics false prematurely for the "perplexity" program. This causes a redirect before the enrollment data finishes loading.

Destructure loading from useProgramEnrollment and update the redirect condition to avoid redirecting while enrollment data is still loading:

const { programEnrollment, showDetailedAnalytics, loading } = useProgramEnrollment();

Then update line 30 to:

if ((!customer && !isLoading) || (!showDetailedAnalytics && !loading)) {
  redirect(`/programs/${programSlug}`);
}
🤖 Prompt for AI Agents
In
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
around line 20, the redirect logic uses showDetailedAnalytics before program
enrollment finishes loading; destructure loading from useProgramEnrollment
(const { programEnrollment, showDetailedAnalytics, loading } =
useProgramEnrollment()) and update the redirect condition to guard against
redirecting while enrollment is loading (i.e., only redirect when (!customer &&
!isLoading) || (!showDetailedAnalytics && !loading)), ensuring you don't
redirect for the "perplexity" program prematurely.

const { programSlug, customerId } = useParams<{
programSlug: string;
customerId: string;
Expand All @@ -27,8 +27,9 @@ export function ProgramCustomerPageClient() {
customerId,
});

if ((!customer && !isLoading) || programSlug === "perplexity")
if ((!customer && !isLoading) || !showDetailedAnalytics) {
redirect(`/programs/${programSlug}`);
}
Comment on lines +30 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential race condition in redirect logic.

The redirect condition checks !showDetailedAnalytics, but this flag depends on programEnrollment.totalCommissions which may not be loaded yet. During the loading state, totalCommissions defaults to 0, which could trigger a premature redirect for "perplexity" program users before their commission data loads.

Consider checking the loading state from useProgramEnrollment before redirecting:

-  const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
+  const { programEnrollment, showDetailedAnalytics, loading: isProgramLoading } = useProgramEnrollment();
   const { programSlug, customerId } = useParams<{
     programSlug: string;
     customerId: string;
   }>();

   const { data: customer, isLoading } = usePartnerCustomer({
     customerId,
   });

-  if ((!customer && !isLoading) || !showDetailedAnalytics) {
+  if ((!customer && !isLoading) || (!showDetailedAnalytics && !isProgramLoading)) {
     redirect(`/programs/${programSlug}`);
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
around lines 30-32, the redirect uses showDetailedAnalytics which depends on
programEnrollment.totalCommissions that may be 0 while enrollment data is still
loading, causing a premature redirect; update the condition to also wait for the
enrollment loading flag (e.g., isLoading from useProgramEnrollment or an
explicit enrollmentLoading) so the redirect only runs when both customer and
enrollment data have finished loading and showDetailedAnalytics is definitively
false.


return (
<div className="mb-10 mt-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type ColumnMeta = {

export function EarningsTablePartner({ limit }: { limit?: number }) {
const { programSlug } = useParams();
const { programEnrollment } = useProgramEnrollment();
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
const { queryParams, searchParamsObj, getQueryString } = useRouterStuff();

const { sortBy = "createdAt", sortOrder = "desc" } = searchParamsObj as {
Expand Down Expand Up @@ -139,9 +139,9 @@ export function EarningsTablePartner({ limit }: { limit?: number }) {
<CustomerRowItem
customer={row.original.customer}
href={
programSlug === "perplexity"
? undefined
: `/programs/${programSlug}/customers/${row.original.customer.id}`
showDetailedAnalytics
? `/programs/${programSlug}/customers/${row.original.customer.id}`
: undefined
}
className="px-4 py-2.5"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import Events from "@/ui/analytics/events";
import { PageContent } from "@/ui/layout/page-content";
import { redirect } from "next/navigation";

export default async function ProgramEvents({
params,
}: {
params: Promise<{ programSlug: string }>;
}) {
const { programSlug } = await params;
if (programSlug === "perplexity") {
redirect(`/programs/${programSlug}`);
}
export default function ProgramEvents() {
return (
<PageContent title="Events">
<Events />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
} from "@dub/utils";
import NumberFlow from "@number-flow/react";
import Link from "next/link";
import { useParams } from "next/navigation";
import {
ComponentProps,
memo,
Expand Down Expand Up @@ -67,8 +66,7 @@ const CHARTS = [
];

export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) {
const { programSlug } = useParams();
const { programEnrollment } = useProgramEnrollment();
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();

const partnerLink = constructPartnerLink({
group: programEnrollment?.group,
Expand Down Expand Up @@ -205,12 +203,12 @@ export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) {
</div>
</Tooltip>
)}
{programSlug == "perplexity" && <StatsBadge link={link} />}
{!showDetailedAnalytics && <StatsBadge link={link} />}
<Controls link={link} />
</div>
</div>
</div>
{programSlug !== "perplexity" && <StatsCharts link={link} />}
{showDetailedAnalytics && <StatsCharts link={link} />}
</CardList.Card>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function ProgramPageClient() {
false,
);

const { programEnrollment } = useProgramEnrollment();
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
const [copied, copyToClipboard] = useCopyToClipboard();

const {
Expand Down Expand Up @@ -236,18 +236,18 @@ export default function ProgramPageClient() {

<PayoutsCard programId={program?.id} />
<NumberFlowGroup>
{programSlug === "perplexity" ? (
<>
<StatCardSimple title="Clicks" event="clicks" />
<StatCardSimple title="Leads" event="leads" />
<StatCardSimple title="Sales" event="sales" />
</>
) : (
{showDetailedAnalytics ? (
<>
<StatCard title="Clicks" event="clicks" />
<StatCard title="Leads" event="leads" />
<StatCard title="Sales" event="sales" />
</>
) : (
<>
<StatCardSimple title="Clicks" event="clicks" />
<StatCardSimple title="Leads" event="leads" />
<StatCardSimple title="Sales" event="sales" />
</>
)}
</NumberFlowGroup>
</div>
Expand Down
9 changes: 6 additions & 3 deletions apps/web/lib/partners/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ export const PAYOUTS_SHEET_ITEMS_LIMIT = 10;
export const BOUNTY_DESCRIPTION_MAX_LENGTH = 5000;
export const REFERRALS_EMBED_EARNINGS_LIMIT = 8;
export const CUSTOMER_PAGE_EVENTS_LIMIT = 8;

export const LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS = 500000; // $5000

export const PAYOUT_FAILURE_FEE_CENTS = 1000; // 10 USD
export const FOREX_MARKUP_RATE = 0.005; // 0.5%
export const MIN_WITHDRAWAL_AMOUNT_CENTS = 1000; // $10
export const BELOW_MIN_WITHDRAWAL_FEE_CENTS = 50; // $0.50
export const FAST_ACH_FEE_CENTS = 2500; // $25
export const FOREX_MARKUP_RATE = 0.005; // 0.5%

export const ALLOWED_MIN_PAYOUT_AMOUNTS = [0, 2000, 5000, 10000];
export const MIN_WITHDRAWAL_AMOUNT_CENTS = 1000; // $10
export const BELOW_MIN_WITHDRAWAL_FEE_CENTS = 50; // $0.50

export const MAX_INVITES_PER_REQUEST = 5;
export const MAX_PARTNER_USERS = 10;
Expand Down
5 changes: 5 additions & 0 deletions apps/web/lib/swr/use-program-enrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { fetcher } from "@dub/utils";
import { useSession } from "next-auth/react";
import { useParams } from "next/navigation";
import useSWR, { SWRConfiguration } from "swr";
import { LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS } from "../partners/constants";
import { ProgramEnrollmentProps } from "../types";

export default function useProgramEnrollment({
Expand Down Expand Up @@ -31,6 +32,10 @@ export default function useProgramEnrollment({

return {
programEnrollment,
showDetailedAnalytics:
programSlug !== "perplexity" ||
(programEnrollment?.totalCommissions ?? 0) >=
LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS,
Comment on lines +35 to +38
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition: showDetailedAnalytics evaluates incorrectly during loading.

When programEnrollment is still loading, it's undefined, so totalCommissions defaults to 0. For the "perplexity" program, this makes showDetailedAnalytics false during the loading state, even if the user should have access once data loads.

Components that use this flag for redirects (e.g., page-client.tsx at line 30) will redirect prematurely before enrollment data finishes loading. This causes users to be redirected away from pages they should have access to.

Consider one of these solutions:

Solution 1: Return showDetailedAnalytics as true during loading to avoid premature redirects:

  return {
    programEnrollment,
    showDetailedAnalytics:
+     status === "loading" ||
      programSlug !== "perplexity" ||
      (programEnrollment?.totalCommissions ?? 0) >=
        LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS,
    error,
    loading: status === "loading" || isLoading,
  };

Solution 2: Make consumers check the loading state before using showDetailedAnalytics. However, this is error-prone as it requires every consumer to remember to check loading.

📝 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
showDetailedAnalytics:
programSlug !== "perplexity" ||
(programEnrollment?.totalCommissions ?? 0) >=
LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS,
showDetailedAnalytics:
status === "loading" ||
programSlug !== "perplexity" ||
(programEnrollment?.totalCommissions ?? 0) >=
LARGE_PROGRAM_MIN_TOTAL_COMMISSIONS_CENTS,
🤖 Prompt for AI Agents
In apps/web/lib/swr/use-program-enrollment.ts around lines 35 to 38,
showDetailedAnalytics currently treats an undefined programEnrollment as
totalCommissions=0 which causes premature false during loading; change the logic
so that while enrollment data is loading you return true for
showDetailedAnalytics (e.g., detect loading/undefined enrollment and, for
programSlug "perplexity", short-circuit to true) so consumers don’t redirect
before data arrives; ensure once the real enrollment is loaded the original
commission threshold check is applied.

error,
loading: status === "loading" || isLoading,
};
Expand Down
3 changes: 1 addition & 2 deletions apps/web/lib/zod/schemas/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ export const DEFAULT_PARTNER_GROUP = {
} as const;

export const MAX_DEFAULT_PARTNER_LINKS = 5;

export const MAX_ADDITIONAL_PARTNER_LINKS = 20;
export const MAX_ADDITIONAL_PARTNER_LINKS = 100;

export const GROUPS_MAX_PAGE_SIZE = 100;

Expand Down
Loading