Skip to content

Commit 69ebc3a

Browse files
authored
use rpc and dataloader for analytics api (#1171)
* use rpc and dataloader for analytics api * format
1 parent 31df433 commit 69ebc3a

18 files changed

Lines changed: 205 additions & 156 deletions

File tree

apps/web/actions/video/upload.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ import { getCurrentUser } from "@cap/database/auth/session";
99
import { nanoId } from "@cap/database/helpers";
1010
import { s3Buckets, videos, videoUploads } from "@cap/database/schema";
1111
import { buildEnv, NODE_ENV, serverEnv } from "@cap/env";
12-
import { userIsPro } from "@cap/utils";
12+
import { dub, userIsPro } from "@cap/utils";
1313
import { S3Buckets } from "@cap/web-backend";
1414
import { type Folder, type Organisation, Video } from "@cap/web-domain";
1515
import { eq } from "drizzle-orm";
1616
import { Effect, Option } from "effect";
1717
import { revalidatePath } from "next/cache";
1818
import { runPromise } from "@/lib/server";
19-
import { dub } from "@/utils/dub";
2019

2120
async function getVideoUploadPresignedUrl({
2221
fileKey,

apps/web/actions/videos/get-analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use server";
22

3-
import { dub } from "@/utils/dub";
3+
import { dub } from "@cap/utils";
44

55
export async function getVideoAnalytics(videoId: string) {
66
if (!videoId) {

apps/web/app/(org)/dashboard/caps/Caps.tsx

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { Effect, Exit } from "effect";
1010
import { useRouter, useSearchParams } from "next/navigation";
1111
import { useEffect, useMemo, useRef, useState } from "react";
1212
import { toast } from "sonner";
13-
import { useEffectMutation } from "@/lib/EffectRuntime";
13+
import { useEffectMutation, useEffectQuery } from "@/lib/EffectRuntime";
14+
import {
15+
AnalyticsRequest,
16+
useVideosAnalyticsQuery,
17+
} from "@/lib/Requests/AnalyticsRequest";
1418
import { Rpc, withRpc } from "@/lib/Rpcs";
1519
import { useDashboardContext } from "../Contexts";
1620
import {
@@ -73,54 +77,8 @@ export const Caps = ({
7377

7478
const anyCapSelected = selectedCaps.length > 0;
7579

76-
const videoIds = data.map((video) => video.id).sort();
77-
78-
const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
79-
queryKey: ["analytics", videoIds],
80-
queryFn: async () => {
81-
if (!dubApiKeyEnabled || data.length === 0) {
82-
return {};
83-
}
84-
85-
const analyticsPromises = data.map(async (video) => {
86-
try {
87-
const response = await fetch(`/api/analytics?videoId=${video.id}`, {
88-
method: "GET",
89-
headers: {
90-
"Content-Type": "application/json",
91-
},
92-
});
93-
94-
if (response.ok) {
95-
const responseData = await response.json();
96-
return { videoId: video.id, count: responseData.count || 0 };
97-
}
98-
return { videoId: video.id, count: 0 };
99-
} catch (error) {
100-
console.warn(
101-
`Failed to fetch analytics for video ${video.id}:`,
102-
error,
103-
);
104-
return { videoId: video.id, count: 0 };
105-
}
106-
});
107-
108-
const results = await Promise.allSettled(analyticsPromises);
109-
const analyticsData: Record<string, number> = {};
110-
111-
results.forEach((result) => {
112-
if (result.status === "fulfilled" && result.value) {
113-
analyticsData[result.value.videoId] = result.value.count;
114-
}
115-
});
116-
117-
return analyticsData;
118-
},
119-
refetchOnWindowFocus: false,
120-
refetchOnMount: true,
121-
});
122-
123-
const analytics = analyticsData || {};
80+
const analyticsQuery = useVideosAnalyticsQuery(data.map((video) => video.id));
81+
const analytics = analyticsQuery.data || {};
12482

12583
useEffect(() => {
12684
const handleKeyDown = (e: KeyboardEvent) => {
@@ -318,7 +276,7 @@ export const Caps = ({
318276
}
319277
}}
320278
userId={user?.id}
321-
isLoadingAnalytics={isLoadingAnalytics}
279+
isLoadingAnalytics={analyticsQuery.isLoading}
322280
isSelected={selectedCaps.includes(video.id)}
323281
anyCapSelected={anyCapSelected}
324282
onSelectToggle={() => handleCapSelection(video.id)}

apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
useUploadingContext,
1818
} from "@/app/(org)/dashboard/caps/UploadingContext";
1919
import { UpgradeModal } from "@/components/UpgradeModal";
20-
import { ThumbnailRequest } from "@/lib/ThumbnailRequest";
20+
import { ThumbnailRequest } from "@/lib/Requests/ThumbnailRequest";
2121

2222
export const UploadCapButton = ({
2323
size = "md",

apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useMemo, useRef, useState } from "react";
88
import { toast } from "sonner";
99
import { useDashboardContext } from "@/app/(org)/dashboard/Contexts";
1010
import { useEffectMutation } from "@/lib/EffectRuntime";
11+
import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest";
1112
import { Rpc, withRpc } from "@/lib/Rpcs";
1213
import type { VideoData } from "../../../caps/Caps";
1314
import { CapCard } from "../../../caps/components/CapCard/CapCard";
@@ -108,49 +109,10 @@ export default function FolderVideosSection({
108109
});
109110
};
110111

111-
const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
112-
queryKey: ["analytics", initialVideos.map((video) => video.id)],
113-
queryFn: async () => {
114-
if (!dubApiKeyEnabled || initialVideos.length === 0) {
115-
return {};
116-
}
117-
118-
const analyticsPromises = initialVideos.map(async (video) => {
119-
try {
120-
const response = await fetch(`/api/analytics?videoId=${video.id}`, {
121-
method: "GET",
122-
headers: {
123-
"Content-Type": "application/json",
124-
},
125-
});
126-
127-
if (response.ok) {
128-
const responseData = await response.json();
129-
return { videoId: video.id, count: responseData.count || 0 };
130-
}
131-
return { videoId: video.id, count: 0 };
132-
} catch (error) {
133-
console.warn(
134-
`Failed to fetch analytics for video ${video.id}:`,
135-
error,
136-
);
137-
return { videoId: video.id, count: 0 };
138-
}
139-
});
140-
141-
const results = await Promise.allSettled(analyticsPromises);
142-
const analyticsData: Record<string, number> = {};
143-
144-
results.forEach((result) => {
145-
if (result.status === "fulfilled" && result.value) {
146-
analyticsData[result.value.videoId] = result.value.count;
147-
}
148-
});
149-
return analyticsData;
150-
},
151-
refetchOnWindowFocus: false,
152-
refetchOnMount: true,
153-
});
112+
const analyticsQuery = useVideosAnalyticsQuery(
113+
initialVideos.map((video) => video.id),
114+
dubApiKeyEnabled,
115+
);
154116

155117
const [isUploading, uploadingCapId] = useUploadingStatus();
156118
const visibleVideos = useMemo(
@@ -161,7 +123,7 @@ export default function FolderVideosSection({
161123
[initialVideos, isUploading, uploadingCapId],
162124
);
163125

164-
const analytics = analyticsData || {};
126+
const analytics = analyticsQuery.data || {};
165127

166128
return (
167129
<>
@@ -185,7 +147,7 @@ export default function FolderVideosSection({
185147
cap={video}
186148
analytics={analytics[video.id] || 0}
187149
userId={user?.id}
188-
isLoadingAnalytics={isLoadingAnalytics}
150+
isLoadingAnalytics={analyticsQuery.isLoading}
189151
isSelected={selectedCaps.includes(video.id)}
190152
anyCapSelected={selectedCaps.length > 0}
191153
isDeleting={isDeletingCaps || isDeletingCap}

apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
88
import { useQuery } from "@tanstack/react-query";
99
import { useRouter, useSearchParams } from "next/navigation";
1010
import { useState } from "react";
11+
import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest";
1112
import { useDashboardContext } from "../../Contexts";
1213
import { CapPagination } from "../../caps/components/CapPagination";
1314
import Folder, { type FolderDataType } from "../../caps/components/Folder";
@@ -94,52 +95,12 @@ export const SharedCaps = ({
9495

9596
const organizationMemberCount = organizationMembers?.length || 0;
9697

97-
const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
98-
queryKey: ["analytics", data.map((video) => video.id)],
99-
queryFn: async () => {
100-
if (!dubApiKeyEnabled || data.length === 0) {
101-
return {};
102-
}
103-
104-
const analyticsPromises = data.map(async (video) => {
105-
try {
106-
const response = await fetch(`/api/analytics?videoId=${video.id}`, {
107-
method: "GET",
108-
headers: {
109-
"Content-Type": "application/json",
110-
},
111-
});
112-
113-
if (response.ok) {
114-
const responseData = await response.json();
115-
return { videoId: video.id, count: responseData.count || 0 };
116-
}
117-
return { videoId: video.id, count: 0 };
118-
} catch (error) {
119-
console.warn(
120-
`Failed to fetch analytics for video ${video.id}:`,
121-
error,
122-
);
123-
return { videoId: video.id, count: 0 };
124-
}
125-
});
126-
127-
const results = await Promise.allSettled(analyticsPromises);
128-
const analyticsData: Record<string, number> = {};
129-
130-
results.forEach((result) => {
131-
if (result.status === "fulfilled" && result.value) {
132-
analyticsData[result.value.videoId] = result.value.count;
133-
}
134-
});
135-
136-
return analyticsData;
137-
},
138-
staleTime: 30000, // 30 seconds
139-
refetchOnWindowFocus: false,
140-
});
98+
const analyticsQuery = useVideosAnalyticsQuery(
99+
data.map((video) => video.id),
100+
dubApiKeyEnabled,
101+
);
141102

142-
const analytics = analyticsData || {};
103+
const analytics = analyticsQuery.data || {};
143104

144105
const handleVideosAdded = () => {
145106
router.refresh();
@@ -296,7 +257,7 @@ export const SharedCaps = ({
296257
key={cap.id}
297258
cap={cap}
298259
hideSharedStatus
299-
isLoadingAnalytics={isLoadingAnalytics}
260+
isLoadingAnalytics={analyticsQuery.isLoading}
300261
analytics={analytics[cap.id] || 0}
301262
organizationName={activeOrganization?.organization.name || ""}
302263
spaceName={spaceData?.name || ""}

apps/web/app/api/desktop/[...route]/video.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
videoUploads,
1212
} from "@cap/database/schema";
1313
import { buildEnv, NODE_ENV, serverEnv } from "@cap/env";
14-
import { userIsPro } from "@cap/utils";
14+
import { dub, userIsPro } from "@cap/utils";
1515
import { S3Buckets } from "@cap/web-backend";
1616
import { Organisation, Video } from "@cap/web-domain";
1717
import { zValidator } from "@hono/zod-validator";
@@ -20,7 +20,6 @@ import { Effect, Option } from "effect";
2020
import { Hono } from "hono";
2121
import { z } from "zod";
2222
import { runPromise } from "@/lib/server";
23-
import { dub } from "@/utils/dub";
2423
import { stringOrNumberOptional } from "@/utils/zod";
2524
import { withAuth } from "../../utils";
2625

apps/web/components/VideoThumbnail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import moment from "moment";
66
import Image from "next/image";
77
import { memo, useEffect, useRef } from "react";
88
import { useEffectQuery } from "@/lib/EffectRuntime";
9-
import { ThumbnailRequest } from "@/lib/ThumbnailRequest";
9+
import { ThumbnailRequest } from "@/lib/Requests/ThumbnailRequest";
1010

1111
export type ImageLoadingStatus = "loading" | "success" | "error";
1212

apps/web/lib/EffectRuntime.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
makeUseEffectMutation,
55
makeUseEffectQuery,
66
} from "./effect-react-query";
7+
import { AnalyticsRequest } from "./Requests/AnalyticsRequest";
8+
import { ThumbnailRequest } from "./Requests/ThumbnailRequest";
79
import { Rpc } from "./Rpcs";
8-
import { ThumbnailRequest } from "./ThumbnailRequest";
910

1011
export const RuntimeLayer = Layer.mergeAll(
1112
ThumbnailRequest.DataLoaderResolver.Default,
13+
AnalyticsRequest.DataLoaderResolver.Default,
1214
Rpc.Default,
1315
FetchHttpClient.layer,
1416
);

0 commit comments

Comments
 (0)