Skip to content

Commit

Permalink
feat: add ability to archive views
Browse files Browse the repository at this point in the history
and by association remove them from the document statistics
  • Loading branch information
mfts committed Oct 29, 2024
1 parent e4b7ef4 commit a42b343
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 128 deletions.
22 changes: 22 additions & 0 deletions components/visitors/visitor-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { ArchiveIcon } from "lucide-react";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

import { generateGravatarHash } from "@/lib/utils";
import { cn } from "@/lib/utils";

import { BadgeTooltip } from "../ui/tooltip";

export const VisitorAvatar = ({
viewerEmail,
isArchived,
className,
}: {
viewerEmail: string | null;
isArchived?: boolean;
className?: string;
}) => {
// Convert email string to a simple hash
Expand Down Expand Up @@ -38,6 +44,22 @@ export const VisitorAvatar = ({
return colors[index];
};

if (isArchived) {
return (
<BadgeTooltip
key="archived"
content="Visit is archived and excluded from the document statistics"
>
<Avatar
className={cn("hidden flex-shrink-0 sm:inline-flex", className)}
>
<AvatarFallback className="bg-gray-200/50 dark:bg-gray-200/50">
<ArchiveIcon className="h-4 w-4" />
</AvatarFallback>
</Avatar>
</BadgeTooltip>
);
}
if (!viewerEmail) {
return (
<Avatar className={cn("hidden flex-shrink-0 sm:inline-flex", className)}>
Expand Down
383 changes: 289 additions & 94 deletions components/visitors/visitors-table.tsx

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions lib/swr/use-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,19 @@ export function useDocumentVisits(page: number, limit: number) {
? `/api/teams/${teamId}/documents/${id}/views?page=${page}&limit=${limit}`
: null;

const { data: views, error } = useSWR<TStatsData>(cacheKey, fetcher, {
const {
data: views,
error,
mutate,
} = useSWR<TStatsData>(cacheKey, fetcher, {
dedupingInterval: 20000,
revalidateOnFocus: false,
});

return {
views,
loading: !error && !views,
error,
mutate,
};
}

Expand Down
7 changes: 3 additions & 4 deletions lib/swr/use-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@ export function useStats({
// this gets the data for a document's graph of all views
const router = useRouter();
const teamInfo = useTeam();
const teamId = teamInfo?.currentTeam?.id;

const { id } = router.query as {
id: string;
};

const { data: stats, error } = useSWR<TStatsData>(
id &&
teamInfo?.currentTeam &&
`/api/teams/${teamInfo.currentTeam.id}/documents/${encodeURIComponent(
id,
)}/stats${excludeTeamMembers ? "?excludeTeamMembers=true" : ""}`,
teamId &&
`/api/teams/${teamId}/documents/${encodeURIComponent(id)}/stats${excludeTeamMembers ? "?excludeTeamMembers=true" : ""}`,
fetcher,
{
dedupingInterval: 10000,
Expand Down
6 changes: 3 additions & 3 deletions lib/tinybird/endpoints/get_total_average_page_duration.pipe
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION 4
VERSION 5

NODE endpoint
SQL >
Expand All @@ -10,8 +10,8 @@ SQL >
WHERE
documentId = {{ String(documentId, required=true) }}
AND time >= {{ Int64(since, required=true) }}
AND linkId NOT IN {{ Array(excludedLinkIds, String) }}
AND viewId NOT IN {{ Array(excludedViewIds, String) }}
AND linkId NOT IN splitByChar(',', {{ String(excludedLinkIds, required=True) }})
AND viewId NOT IN splitByChar(',', {{ String(excludedViewIds, required=True) }})
GROUP BY versionNumber, pageNumber, viewId
)
SELECT versionNumber, pageNumber, AVG(distinct_duration) AS avg_duration
Expand Down
4 changes: 2 additions & 2 deletions lib/tinybird/pipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export const getTotalAvgPageDuration = tb.buildPipe({
pipe: "get_total_average_page_duration__v5",
parameters: z.object({
documentId: z.string(),
excludedLinkIds: z.array(z.string()),
excludedViewIds: z.array(z.string()),
excludedLinkIds: z.string().describe("Comma separated linkIds"),
excludedViewIds: z.string().describe("Comma separated viewIds"),
since: z.number(),
}),
data: z.object({
Expand Down
34 changes: 12 additions & 22 deletions pages/api/teams/[teamId]/documents/[id]/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { getServerSession } from "next-auth/next";
import { LIMITS } from "@/lib/constants";
import { errorhandler } from "@/lib/errorHandler";
import prisma from "@/lib/prisma";
import { getTeamWithUsersAndDocument } from "@/lib/team/helper";
import { getTotalAvgPageDuration } from "@/lib/tinybird";
import { CustomUser } from "@/lib/types";

Expand Down Expand Up @@ -51,19 +50,6 @@ export default async function handle(
},
});

// const { document } = await getTeamWithUsersAndDocument({
// teamId,
// userId,
// docId,
// checkOwner: true,
// options: {
// include: {
// views: true,
// team: true,
// },
// },
// });

const users = await prisma.user.findMany({
where: {
teams: {
Expand All @@ -89,39 +75,43 @@ export default async function handle(
});
}

const totalViews = views.length;
const activeViews = views.filter((view) => !view.isArchived);
const archivedViews = views.filter((view) => view.isArchived);

// limit the number of views to 20 on free plan
const limitedViews =
document?.team?.plan === "free" ? views.slice(0, LIMITS.views) : views;

// exclude views from the team's members
let excludedViews: View[] = [];
let internalViews: View[] = [];
if (excludeTeamMembers) {
excludedViews = limitedViews.filter((view) => {
internalViews = limitedViews.filter((view) => {
return users.some((user) => user.email === view.viewerEmail);
});
}

// combined archived and internal views
const allExcludedViews = [...internalViews, ...archivedViews];

const filteredViews = limitedViews.filter(
(view) => !excludedViews.map((view) => view.id).includes(view.id),
(view) => !allExcludedViews.map((view) => view.id).includes(view.id),
);

const groupedReactions = await prisma.reaction.groupBy({
by: ["type"],
where: {
view: {
documentId: docId,
id: { notIn: excludedViews.map((view) => view.id) },
id: { notIn: allExcludedViews.map((view) => view.id) },
},
},
_count: { type: true },
});

const duration = await getTotalAvgPageDuration({
documentId: docId,
excludedLinkIds: [],
excludedViewIds: excludedViews.map((view) => view.id),
excludedLinkIds: "",
excludedViewIds: allExcludedViews.map((view) => view.id).join(","),
since: 0,
});

Expand All @@ -135,7 +125,7 @@ export default async function handle(
duration,
total_duration,
groupedReactions,
totalViews,
totalViews: activeViews.length,
};

return res.status(200).json(stats);
Expand Down
58 changes: 58 additions & 0 deletions pages/api/teams/[teamId]/views/[id]/archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextApiRequest, NextApiResponse } from "next";

import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth/next";

import { errorhandler } from "@/lib/errorHandler";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";

export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "PUT") {
// PUT /api/teams/:teamId/views/:id/archive
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}

const userId = (session.user as CustomUser).id;
const { teamId, id } = req.query as { teamId: string; id: string };
const { isArchived } = req.body;

try {
const team = await prisma.team.findUnique({
where: {
id: teamId,
users: { some: { userId } },
},
});

if (!team) {
return res.status(403).json({ error: "Unauthorized" });
}

// Update the link in the database
const updatedView = await prisma.view.update({
where: { id, teamId },
data: {
isArchived: isArchived,
},
});

if (!updatedView) {
return res.status(404).json({ error: "View not found" });
}

return res.status(200).json(updatedView);
} catch (error) {
errorhandler(error, res);
}
}

// We only allow PUT requests
res.setHeader("Allow", ["PUT"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
2 changes: 1 addition & 1 deletion pages/documents/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function DocumentPage() {
/>

{/* Visitors */}
<VisitorsTable numPages={primaryVersion.numPages!} />
<VisitorsTable />

<LinkSheet
isOpen={isLinkSheetOpen}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "View" ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT false;

2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ model View {
feedbackResponse FeedbackResponse?
agreementResponse AgreementResponse?
isArchived Boolean @default(false) // Indicates if the view is archived and not counted in the analytics
teamId String?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
Expand Down

0 comments on commit a42b343

Please sign in to comment.