8000 Program Analytics page by TWilson023 · Pull Request #2519 · dubinc/dub · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Program Analytics page #2519

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

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
34dbe8d
Add analytics tab+page
TWilson023 Jun 12, 2025
0101c20
Add area chart
TWilson023 Jun 12, 2025
d4ae66c
Merge branch 'main' into program-analytics
TWilson023 Jun 13, 2025
0bdae09
Big filtering refactor + implementation
TWilson023 Jun 13, 2025
0f859f0
Fix filter counts
TWilson023 Jun 13, 2025
d259cf0
Hide UTM filters
TWilson023 Jun 13, 2025
969ff68
WIP analytics partners table
TWilson023 Jun 13, 2025
c9f2ad0
Update analytics-partners-table.tsx
TWilson023 Jun 13, 2025
ae5432f
Merge branch 'main' into program-analytics
TWilson023 Jun 13, 2025
69ecbd9
Add type predicate for filtering out `null`
TWilson023 Jun 13, 2025
3276d14
Merge branch 'program-analytics' of github.com:dubinc/dub into progra…
TWilson023 Jun 13, 2025
0cad27c
Fully mock pagination
TWilson023 Jun 13, 2025
1779ed7
switch from 2 calls to 1 call
steven-tey Jun 13, 2025
ded8046
improve loading state
steven-tey Jun 13, 2025
06fd681
Update page-client.tsx
steven-tey Jun 13, 2025
78ac707
simplify filters
steven-tey Jun 13, 2025
d57a43b
Merge branch 'main' into program-analytics
steven-tey Jun 15, 2025
dd347c7
Merge branch 'main' into program-analytics
steven-tey Jun 15, 2025
5d2bb8f
Add analytics tabs
TWilson023 Jun 16, 2025
410d383
WIP partner filter
TWilson023 Jun 16, 2025
010b92f
Merge branch 'main' into program-analytics
TWilson023 Jun 16, 2025
fc52ba9
Update analytics-partners-table.tsx
TWilson023 Jun 16, 2025
d45ddc7
Add sorting by event
TWilson023 Jun 16, 2025
70f1820
Merge branch 'program-analytics' of github.com:dubinc/dub into progra…
TWilson023 Jun 16, 2025
3cde99c
Add saleAmount to filters
TWilson023 Jun 16, 2025
67ec72a
Update use-analytics-filters.tsx
TWilson023 Jun 16, 2025
c25bc59
Add analytics cards
TWilson023 Jun 16, 2025
67c1519
Types fix
TWilson023 Jun 16, 2025
a32c6a6
Merge branch 'main' into program-analytics
TWilson023 Jun 16, 2025
4f96103
Merge branch 'main' into program-analytics
TWilson023 Jun 16, 2025
9430e5a
Update record-link.ts
TWilson023 Jun 16, 2025
e3e3f55
Update analytics-partners-table.tsx
TWilson023 Jun 16, 2025
edfee16
Merge branch 'program-analytics' of github.com:dubinc/dub into progra…
TWilson023 Jun 16, 2025
4d46ec0
Update analytics-chart.tsx
TWilson023 Jun 16, 2025
b2284ee
Update analytics-chart.tsx
TWilson023 Jun 16, 2025
788def1
Update page-client.tsx
TWilson023 Jun 16, 2025
225c732
Merge branch 'main' into program-analytics
TWilson023 Jun 16, 2025
cb6f86e
speed up `GET /analytics`
steven-tey Jun 16, 2025
6d6cd7e
remove useProgramRevenue
steven-tey Jun 16, 2025
4f4ac13
Merge branch 'main' into program-analytics
steven-tey Jun 16, 2025
3b358c2
Refactor context + add funnel chart
TWilson023 Jun 16, 2025
a4d30da
Merge branch 'program-analytics' of github.com:dubinc/dub into progra…
TWilson023 Jun 16, 2025
d95c977
Merge branch 'main' into program-analytics
TWilson023 Jun 16, 2025
1bba815
Merge branch 'main' into program-analytics
steven-tey Jun 16, 2025
217bddd
Update overview-chart.tsx
steven-tey Jun 16, 2025
ca7c7b8
Update apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analyt…
steven-tey Jun 16, 2025
2798afd
Update README.md
steven-tey Jun 16, 2025
bd45b3e
Update analytics-chart.tsx
steven-tey Jun 16, 2025
0162c4f
Merge branch 'main' into program-analytics
steven-tey Jun 16, 2025
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ We love our contributors! Here's how you can contribute:

## License

Dub Technologies, Inc. is a commercial open-source company, which means some parts of this open-source repository require a commercial license. The concept is called "Open Core" where the core technology (99%) is fully open source, licensed under [AGPLv3](https://opensource.org/license/agpl-v3) and the last 1% is covered under a commercial license (["/ee" Enterprise Edition](https://github.com/dubinc/dub/tree/ee/apps/web/app/(ee))) which we believe is entirely relevant for larger organisations that require enterprise features. Enterprise features are built by the core engineering team of Dub Technologies, Inc., which is hired full-time.
Dub Technologies, Inc. is a commercial open-source company, which means some parts of this open-source repository require a commercial license. The concept is called "Open Core" where the core technology (99%) is fully open source, licensed under [AGPLv3](https://opensource.org/license/agpl-v3) and the last 1% is covered under a commercial license (["/ee" Enterprise Edition](<https://github.com/dubinc/dub/tree/ee/apps/web/app/(ee)>)) which we believe is entirely relevant for larger organisations that require enterprise features. Enterprise features are built by the core engineering team of Dub Technologies, Inc., which is hired full-time.
44 changes: 0 additions & 44 deletions apps/web/app/(ee)/api/programs/[programId]/revenue/route.ts

This file was deleted.

28 changes: 22 additions & 6 deletions apps/web/app/api/analytics/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { getAnalytics } from "@/lib/analytics/get-analytics";
import { getFolderIdsToFilter } from "@/lib/analytics/get-folder-ids-to-filter";
import { validDateRangeForPlan } from "@/lib/analytics/utils";
import { getDomainOrThrow } from "@/lib/api/domains/get-domain-or-throw";
import { DubApiError } from "@/lib/api/errors";
import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw";
import { throwIfClicksUsageExceeded } from "@/lib/api/links/usage-checks";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { prefixWorkspaceId } from "@/lib/api/workspace-id";
import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import {
Expand Down Expand Up @@ -41,13 +44,24 @@ export const GET = withWorkspace(
domain,
key,
folderId,
programId,
} = parsedParams;

let link: Link | null = null;

event = oldEvent || event;
groupBy = oldType || groupBy;

if (programId) {
const workspaceProgramId = getDefaultProgramIdOrThrow(workspace);
if (programId 5D32 !== workspaceProgramId) {
throw new DubApiError({
code: "forbidden",
message: `Program ${programId} does not belong to workspace ${prefixWorkspaceId(workspace.id)}.`,
});
}
}

if (domain) {
await getDomainOrThrow({ workspace, domain });
}
Expand Down Expand Up @@ -83,12 +97,14 @@ export const GET = withWorkspace(
throwError: true,
});

const folderIds = folderIdToVerify
? undefined
: await getFolderIdsToFilter({
workspace,
userId: session.user.id,
});
// no need to get folder ids if we are filtering by a folder or program
const folderIds =
folderIdToVerify || programId
? undefined
: await getFolderIdsToFilter({
workspace,
userId: session.user.id,
});

// Identify the request is from deprecated clicks endpoint
// (/api/analytics/clicks)
Expand Down
A53C
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { formatDateTooltip } from "@/lib/analytics/format-date-tooltip";
import { editQueryString } from "@/lib/analytics/utils";
import { AnalyticsFunnelChart } from "@/ui/analytics/analytics-funnel-chart";
import { AnalyticsContext } from "@/ui/analytics/analytics-provider";
import { AnalyticsTabs } from "@/ui/analytics/analytics-tabs";
import { ChartViewSwitcher } from "@/ui/analytics/chart-view-switcher";
import { useRouterStuff } from "@dub/ui";
import {
Areas,
ChartContext,
TimeSeriesChart,
XAxis,
YAxis,
} from "@dub/ui/charts";
import { LoadingSpinner } from "@dub/ui/icons";
import { capitalize, currencyFormatter, fetcher, nFormatter } from "@dub/utils";
import { LinearGradient } from "@visx/gradient";
import { useContext, useId, useMemo } from "react";
import useSWR from "swr";

export function AnalyticsChart() {
const id = useId();

const { queryParams } = useRouterStuff();

const {
start,
end,
interval,
selectedTab,
saleUnit,
view,
totalEvents,
queryString,
} = useContext(AnalyticsContext);

const { data, error } = useSWR<
{
start: Date;
clicks: number;
leads: number;
sales: number;
saleAmount: number;
}[]
>(
`/api/analytics?${editQueryString(queryString ?? "", {
event: "composite",
groupBy: "timeseries",
})}`,
fetcher,
);

const chartData = useMemo(
() =>
data?.map((d) => ({
date: new Date(d.start),
values: {
amount:
selectedTab === "sales" && saleUnit === "saleAmount"
? d.saleAmount / 100
: d[selectedTab],
},
})),
[data, selectedTab, saleUnit],
);

const dataLoading = !chartData && !error;

return (
<div>
<div className="border-b border-neutral-200">
<AnalyticsTabs
showConversions={true}
totalEvents={totalEvents}
tab={selectedTab}
tabHref={(id) =>
queryParams({
set: {
event: id,
},
getNewPath: true,
}) as string
}
saleUnit={saleUnit}
setSaleUnit={(option) =>
queryParams({
set: { saleUnit: option },
})
}
/>
</div>
<div className="relative h-72 md:h-96">
{dataLoading ? (
<div className="flex size-full items-center justify-center">
<LoadingSpinner />
</div>
) : error ? (
<div className="flex size-full items-center justify-center text-sm text-neutral-500">
Failed to load data
</div>
) : (
<>
{view === "timeseries" ? (
<div className="relative size-full p-6 pt-10">
<TimeSeriesChart
key={`${start?.toString()}-${end?.toString()}-${interval ?? ""}-${selectedTab}-${saleUnit}`}
data={chartData || []}
series={[
{
id: "amount",
valueAccessor: (d) => d.values.amount,
colorClassName: "text-[#8B5CF6]",
isActive: true,
},
]}
tooltipClassName="p-0"
tooltipContent={(d) => {
return (
<>
<p className="border-b border-neutral-200 px-4 py-3 text-sm text-neutral-900">
{formatDateTooltip(d.date, { interval, start, end })}
</p>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-sm bg-violet-500 shadow-[inset_0_0_0_1px_#0003]" />
<p className="capitalize text-neutral-600">
{capitalize(selectedTab)}
</p>
</div>
<p className="text-right font-medium text-neutral-900">
{selectedTab === "sales" &&
saleUnit === "saleAmount"
? currencyFormatter(d.values.amount, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: nFormatter(d.values.amount)}
</p>
</div>
</>
);
}}
>
<ChartContext.Consumer>
{(context) => (
<LinearGradient
id={`${id}-color-gradient`}
from="#7D3AEC"
to="#DA2778"
x1={0}
x2={context?.width ?? 1}
gradientUnits="userSpaceOnUse"
/>
)}
</ChartContext.Consumer>
<XAxis
tickFormat={(date) =>
formatDateTooltip(date, { interval, start, end })
}
/>
<YAxis
showGridLines
tickFormat={
selectedTab === "sales" && saleUnit === "saleAmount"
? currencyFormatter
: nFormatter
}
/>
<Areas />
</TimeSeriesChart>
</div>
) : (
<AnalyticsFunnelChart />
)}
<ChartViewSwitcher className="absolute right-3 top-3" />
</>
)}
</div>
</div>
);
}
Loading
0