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
3 changes: 2 additions & 1 deletion apps/web/app/api/cron/import/csv/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ export async function POST(req: Request) {
await redis.incrby(`${redisKey}:processed`, rows.length);

if (rows.length === BATCH_SIZE) {
return await qstash.publishJSON({
const response = await qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/import/csv`,
body: payload,
});
return NextResponse.json(response);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
programId: programId,
});

if (program.slug === "framer") {
throw new DubApiError({
code: "forbidden",
message: "Framer program does not support customer activity",
});
}

const customer = await prisma.customer.findUnique({
where: {
id: customerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
programId: programId,
});

if (program.slug === "framer") {
throw new DubApiError({
code: "forbidden",
message: "Framer program does not support customer profile",
});
}

const customer = await prisma.customer.findUnique({
where: {
id: customerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const GET = withPartnerProfile(
return {
id: customerId,
email: customer?.email
? customer.email.replace(/(?<=^.).+(?=.@)/, "********")
? customer.email.replace(/(?<=^.).+(?=.@)/, "****")
: customer?.name || generateRandomName(),
_count,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,8 @@ const CommissionTableInner = memo(
},
{
header: "Customer",
cell: ({ row }) => {
if (!row.original.customer) {
return "-";
}

return (
cell: ({ row }) =>
row.original.customer ? (
<div className="flex items-center gap-2">
<img
src={
Expand All @@ -115,8 +111,9 @@ const CommissionTableInner = memo(
{row.original.customer.email ?? row.original.customer.name}
</Link>
</div>
);
},
) : (
"-"
),
meta: {
filterParams: ({ row }) =>
row.original.customer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { CustomerDetailsColumn } from "@/ui/customers/customer-details-column";
import { CustomerSalesTable } from "@/ui/customers/customer-sales-table";
import { ProgramRewardList } from "@/ui/partners/program-reward-list";
import { BackLink } from "@/ui/shared/back-link";
import { MoneyBill2, Tooltip, User } from "@dub/ui";
import { fetcher } from "@dub/utils";
import { MoneyBill2, Tooltip } from "@dub/ui";
import { fetcher, OG_AVATAR_URL } from "@dub/utils";
import { notFound, useParams } from "next/navigation";
import { memo } from "react";
import useSWR from "swr";
Expand Down Expand Up @@ -46,9 +46,15 @@ export function ProgramCustomerPageClient() {
<div className="mb-10 mt-2">
<BackLink href={`/programs/${programSlug}/earnings`}>Earnings</BackLink>
<div className="mt-5 flex items-center gap-4">
<div className="border-border-subtle flex size-12 items-center justify-center rounded-full border bg-gradient-to-t from-neutral-50">
<User className="size-4 text-neutral-700" />
</div>
{customer ? (
<img
src={`${OG_AVATAR_URL}${customer.id}`}
alt={customer.email ?? customer.id}
className="size-8 rounded-full"
/>
) : (
<div className="size-8 animate-pulse rounded-full bg-neutral-200" />
)}

<div className="flex flex-col gap-1">
{customer ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { PageContent } from "@/ui/layout/page-content";
import { MaxWidthWrapper } from "@dub/ui";
import { redirect } from "next/navigation";
import { ProgramCustomerPageClient } from "./page-client";

export default function ProgramCustomer() {
export default function ProgramCustomer({
params,
}: {
params: { programSlug: string; customerId: string };
}) {
if (params.programSlug === "framer") {
redirect("/programs/framer");
}
return (
<PageContent hideReferButton>
<MaxWidthWrapper className="flex flex-col gap-6">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
formatDateTimeSmart,
getApexDomain,
getPrettyUrl,
OG_AVATAR_URL,
} from "@dub/utils";
import { Cell } from "@tanstack/react-table";
import Link from "next/link";
Expand Down Expand Up @@ -130,8 +131,15 @@ export function EarningsTablePartner({ limit }: { limit?: number }) {
scroll={false}
className="flex w-full items-center justify-between gap-2 px-4 py-2.5 transition-colors hover:bg-stone-100"
>
<div className="truncate" title={row.original.customer.email}>
{row.original.customer.email}
<div className="flex items-center gap-2">
<img
src={`${OG_AVATAR_URL}${row.original.customer.id}`}
alt={row.original.customer.email}
className="size-5 rounded-full"
/>
<div className="truncate" title={row.original.customer.email}>
{row.original.customer.email}
</div>
</div>
<ChartActivity2 className="size-3.5 shrink-0" />
</Link>
Expand Down
5 changes: 4 additions & 1 deletion apps/web/lib/api/links/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ class LinkCache {
}

async get({ domain, key }: Pick<LinkProps, "domain" | "key">) {
return await redis.get<RedisLinkProps>(this._createKey({ domain, key }));
// here we use linkcache:${domain}:${key} instead of this._createKey({ domain, key })
// because the key can either be cached as case-sensitive or case-insensitive depending on the domain
// so we should get the original key from the cache
return await redis.get<RedisLinkProps>(`linkcache:${domain}:${key}`);
}

async delete({ domain, key }: Pick<LinkProps, "domain" | "key">) {
Expand Down
15 changes: 10 additions & 5 deletions apps/web/lib/middleware/utils/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import { NextRequest } from "next/server";

export const parse = (req: NextRequest) => {
let domain = req.headers.get("host") as string;
// path is the path of the URL (e.g. dub.sh/stats/github -> /stats/github)
let path = req.nextUrl.pathname;

// remove www. from domain and convert to lowercase
domain = domain.replace(/^www./, "").toLowerCase();
if (domain === "dub.localhost:8888" || domain.endsWith(".vercel.app")) {
// for local development and preview URLs
domain = SHORT_DOMAIN;
if (path.toLowerCase() === "/case-sensitive-test") {
// special case for case-sensitive link test
domain = "dub-internal-test.com";
} else {
// for local development and preview URLs
domain = SHORT_DOMAIN;
}
}

// path is the path of the URL (e.g. dub.sh/stats/github -> /stats/github)
let path = req.nextUrl.pathname;

// fullPath is the full URL path (along with search params)
const searchParams = req.nextUrl.searchParams.toString();
const searchParamsObj = Object.fromEntries(req.nextUrl.searchParams);
Expand Down
5 changes: 5 additions & 0 deletions apps/web/lib/tinybird/record-click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export async function recordClick({
return null;
}

// don't track HEAD requests to avoid non-user traffic from inflating click count
if (req.method === "HEAD") {
return null;
}

const isBot = detectBot(req);

// don't record clicks from bots
Expand Down
5 changes: 2 additions & 3 deletions apps/web/lib/zod/schemas/partner-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export const PartnerEarningsSchema = CommissionSchema.merge(
id: z.string(),
email: z
.string()
.transform((email) => email.replace(/(?<=^.).+(?=.@)/, "********")),
avatar: z.string().nullable(),
.transform((email) => email.replace(/(?<=^.).+(?=.@)/, "****")),
})
.nullable(),
link: LinkSchema.pick({
Expand Down Expand Up @@ -87,5 +86,5 @@ export const PartnerProfileCustomerSchema = CustomerEnrichedSchema.pick({
}).extend({
email: z
.string()
.transform((email) => email.replace(/(?<=^.).+(?=.@)/, "********")),
.transform((email) => email.replace(/(?<=^.).+(?=.@)/, "****")),
});
24 changes: 24 additions & 0 deletions apps/web/tests/redirects/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@ describe.runIf(env.CI)("Link Redirects", async () => {
expect(response.status).toBe(302);
});

test("with case-sensitive (correct) key", async () => {
const response = await fetch(
`${h.baseUrl}/cAsE-sensitive-test`,
fetchOptions,
);

expect(response.headers.get("location")).toBe(
"https://dub.co/changelog/case-insensitive-links",
);
expect(response.headers.get("x-powered-by")).toBe(poweredBy);
expect(response.status).toBe(302);
});

test("with case-sensitive (incorrect) key", async () => {
const response = await fetch(
`${h.baseUrl}/case-sensitive-test`,
fetchOptions,
);

expect(response.headers.get("location")).toBe("https://dub.co/");
expect(response.headers.get("x-powered-by")).toBe(poweredBy);
expect(response.status).toBe(302);
});

test("with password", async () => {
const response = await fetch(
`${h.baseUrl}/password/check?pw=dub`,
Expand Down
56 changes: 41 additions & 15 deletions apps/web/ui/customers/customer-activity-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CustomerActivityResponse } from "@/lib/types";
import { LinkLogo } from "@dub/ui";
import { DynamicTooltipWrapper, LinkLogo } from "@dub/ui";
import { CursorRays, MoneyBill2, UserCheck } from "@dub/ui/icons";
import { formatDateTimeSmart, getApexDomain, getPrettyUrl } from "@dub/utils";
import Link from "next/link";
Expand All @@ -10,17 +10,24 @@ const activityData = {
icon: CursorRays,
content: (event) => {
const { slug, programSlug } = useParams();

const analyticsBaseUrl = programSlug
? `/programs/${programSlug}/analytics`
: `/${slug}/analytics`;

const referer =
!event.click?.referer || event.click.referer === "(direct)"
? "direct"
: event.click.referer;
const refererUrl = event.click.refererUrl;

return (
<span className="flex items-center gap-1.5 [&>*]:min-w-0 [&>*]:truncate">
Found{" "}
<Link
href={
programSlug
? `/programs/${programSlug}/analytics?domain=${event.link.domain}&key=${event.link.key}`
? `${analyticsBaseUrl}?domain=${event.link.domain}&key=${event.link.key}`
: `/${slug}/links/${getPrettyUrl(event.link.shortLink)}`
}
target="_blank"
Expand All @@ -35,21 +42,40 @@ const activityData = {
</span>
</Link>
via
<Link
href={
programSlug
? `/programs/${programSlug}/analytics?referer=${referer === "direct" ? "(direct)" : referer}`
: `/${slug}/analytics?referer=${referer === "direct" ? "(direct)" : referer}`
<DynamicTooltipWrapper
tooltipProps={
refererUrl && refererUrl != "(direct)"
? {
content: (
<div className="max-w-xs px-4 py-2 text-center text-sm text-neutral-600">
Referrer URL:{" "}
<Link
href={`${analyticsBaseUrl}?refererUrl=${refererUrl}`}
target="_blank"
className="cursor-alias text-neutral-500 decoration-dotted underline-offset-2 transition-colors hover:text-neutral-950 hover:underline"
>
{getPrettyUrl(refererUrl)}
</Link>
</div>
),
}
: undefined
}
target="_blank"
className="flex items-center gap-2 rounded-md bg-neutral-100 px-1.5 py-1 font-mono text-xs leading-none transition-colors hover:bg-neutral-200/80"
>
<LinkLogo
className="size-3 shrink-0 sm:size-3"
apexDomain={referer === "direct" ? undefined : referer}
/>
<span className="min-w-0 truncate">{referer}</span>
</Link>
<div>
<Link
href={`${analyticsBaseUrl}?referer=${referer === "direct" ? "(direct)" : referer}`}
target="_blank"
className="flex items-center gap-2 rounded-md bg-neutral-100 px-1.5 py-1 font-mono text-xs leading-none transition-colors hover:bg-neutral-200/80"
>
<LinkLogo
className="size-3 shrink-0 sm:size-3"
apexDomain={referer === "direct" ? undefined : referer}
/>
<span className="min-w-0 truncate">{referer}</span>
</Link>
</div>
</DynamicTooltipWrapper>
</span>
);
},
Expand Down
12 changes: 5 additions & 7 deletions apps/web/ui/customers/customer-details-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export function CustomerDetailsColumn({
<ConditionalLink
href={`/${programSlug ? `programs/${programSlug}` : slug}/analytics?country=${encodeURIComponent(customer.country)}`}
target="_blank"
linkClassName="underline-offset-2 hover:text-neutral-950 hover:underline"
>
<div className="flex items-center gap-2">
<img
Expand Down Expand Up @@ -103,7 +102,6 @@ export function CustomerDetailsColumn({
key={key}
href={`/${programSlug ? `programs/${programSlug}` : slug}/analytics?${key}=${encodeURIComponent(value)}`}
target="_blank"
linkClassName="underline-offset-2 hover:text-neutral-950 hover:underline"
>
<span className="flex items-center gap-2">
{icon}
Expand Down Expand Up @@ -173,7 +171,6 @@ export function CustomerDetailsColumn({
href={`/${programSlug ? `programs/${programSlug}` : slug}/analytics?domain=${link.domain}&key=${link.key}`}
target="_blank"
className="min-w-0 overflow-hidden truncate"
linkClassName="underline-offset-2 hover:text-neutral-950 hover:underline"
>
{getPrettyUrl(link.shortLink)}
</ConditionalLink>
Expand All @@ -193,7 +190,6 @@ export function CustomerDetailsColumn({
href={`/${programSlug ? `programs/${programSlug}` : slug}/analytics?${key}=${encodeURIComponent(value)}`}
target="_blank"
className="truncate text-neutral-500"
linkClassName="underline-offset-2 hover:text-neutral-600 hover:underline"
>
{value}
</ConditionalLink>
Expand Down Expand Up @@ -222,13 +218,15 @@ const ConditionalLink = ({
href,
className,
children,
linkClassName,
...rest
}: HTMLProps<HTMLAnchorElement> & { linkClassName?: string }) => {
}: HTMLProps<HTMLAnchorElement>) => {
return href ? (
<Link
href={href}
className={cn("group flex items-center", className, linkClassName)}
className={cn(
"group flex items-center decoration-dotted underline-offset-2 hover:text-neutral-950 hover:underline",
className,
)}
{...rest}
>
<div className="min-w-0 truncate">{children}</div>
Expand Down
Loading