Skip to content

Commit d534ab0

Browse files
committed
fix: api perf, polyfill, and sessions/profiles page optimization
1 parent 1c729a7 commit d534ab0

File tree

16 files changed

+203
-90
lines changed

16 files changed

+203
-90
lines changed

apps/api/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616
"@databuddy/rpc": "workspace:*",
1717
"@elysiajs/cors": "^1.3.3",
1818
"@elysiajs/trpc": "^1.1.0",
19-
"@elysiajs/websocket": "^0.2.8",
2019
"@logtail/edge": "^0.5.5",
2120
"@trpc/server": "^11.4.3",
2221
"autumn-js": "0.0.101-beta.1",
2322
"dayjs": "^1.11.13",
2423
"elysia": "^1.3.6",
25-
"nats": "^2.29.3",
2624
"openai": "^5.9.0"
2725
}
2826
}

apps/api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './polyfills/compression';
12
import { appRouter, createTRPCContext } from '@databuddy/rpc';
23
import cors from '@elysiajs/cors';
34
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';

apps/api/src/lib/website-utils.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ const getCachedWebsiteDomain = cacheable(
7575
}
7676
);
7777

78+
const userPreferencesCache = cacheable(
79+
async (userId: string) => {
80+
try {
81+
return await db.query.userPreferences.findFirst({
82+
where: eq(userPreferences.userId, userId),
83+
});
84+
} catch {
85+
return null;
86+
}
87+
},
88+
{
89+
expireInSec: 600,
90+
prefix: 'user-prefs',
91+
staleWhileRevalidate: true,
92+
staleTime: 120,
93+
}
94+
);
95+
96+
const getCachedSession = cacheable(
97+
async (headers: Headers) => {
98+
return await auth.api.getSession({
99+
headers,
100+
});
101+
},
102+
{
103+
expireInSec: 60,
104+
prefix: 'auth-session',
105+
staleWhileRevalidate: true,
106+
staleTime: 30,
107+
}
108+
);
109+
78110
export async function getTimezone(
79111
request: Request,
80112
session: { user?: { id: string } } | null
@@ -84,9 +116,7 @@ export async function getTimezone(
84116
const paramTimezone = url.searchParams.get('timezone');
85117

86118
if (session?.user) {
87-
const pref = await db.query.userPreferences.findFirst({
88-
where: eq(userPreferences.userId, session.user.id),
89-
});
119+
const pref = await userPreferencesCache(session.user.id);
90120
if (pref?.timezone && pref.timezone !== 'auto') {
91121
return pref.timezone;
92122
}
@@ -96,13 +126,11 @@ export async function getTimezone(
96126
}
97127

98128
export async function deriveWebsiteContext({ request }: { request: Request }) {
99-
const session = await auth.api.getSession({
100-
headers: request.headers,
101-
});
102-
103129
const url = new URL(request.url);
104130
const website_id = url.searchParams.get('website_id');
105131

132+
const session = await getCachedSession(request.headers);
133+
106134
if (!website_id) {
107135
if (!session?.user) {
108136
throw new Error('Unauthorized');
@@ -111,22 +139,25 @@ export async function deriveWebsiteContext({ request }: { request: Request }) {
111139
return { user: session.user, session, timezone };
112140
}
113141

114-
const website = await getCachedWebsite(website_id);
142+
const [website, timezone] = await Promise.all([
143+
getCachedWebsite(website_id),
144+
website_id && session?.user
145+
? getTimezone(request, session)
146+
: getTimezone(request, null)
147+
]);
115148

116149
if (!website) {
117150
throw new Error('Website not found');
118151
}
119152

120153
if (website.isPublic) {
121-
const timezone = await getTimezone(request, null);
122154
return { user: null, session: null, website, timezone };
123155
}
124156

125157
if (!session?.user) {
126158
throw new Error('Unauthorized');
127159
}
128160

129-
const timezone = await getTimezone(request, session);
130161
return { user: session.user, session, website, timezone };
131162
}
132163

apps/api/src/middleware/rate-limit.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import { auth } from '@databuddy/auth';
22
import { getRateLimitIdentifier, rateLimiters } from '@databuddy/rpc';
3+
import { cacheable } from '@databuddy/redis';
34
import { Elysia } from 'elysia';
45

56
export interface RateLimitOptions {
67
type: 'api' | 'auth' | 'expensive' | 'admin' | 'public';
78
skipAuth?: boolean;
89
}
910

11+
const getCachedAuthSession = cacheable(
12+
async (headers: Headers) => {
13+
try {
14+
return await auth.api.getSession({
15+
headers,
16+
});
17+
} catch (error) {
18+
console.error('[Rate Limit] Auth error:', error);
19+
return null;
20+
}
21+
},
22+
{
23+
expireInSec: 30,
24+
prefix: 'rate-limit-auth',
25+
staleWhileRevalidate: true,
26+
staleTime: 15,
27+
}
28+
);
29+
1030
export function createRateLimitMiddleware(options: RateLimitOptions) {
1131
return new Elysia().onRequest(async ({ request, set }) => {
1232
if (request.url.includes('/trpc/')) {
@@ -17,14 +37,9 @@ export function createRateLimitMiddleware(options: RateLimitOptions) {
1737

1838
let userId: string | undefined;
1939
if (!options.skipAuth) {
20-
try {
21-
const session = await auth.api.getSession({
22-
headers: request.headers,
23-
});
24-
userId = session?.user?.id;
25-
} catch (error) {
26-
console.error('[Rate Limit] Auth error:', error);
27-
}
40+
const session = await getCachedAuthSession(request.headers);
41+
userId = session?.user?.id;
42+
2843
}
2944

3045
const identifier = getRateLimitIdentifier(userId, request.headers);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import stream from 'node:stream';
2+
import zlib from 'node:zlib';
3+
4+
const make = (ctx, handle) =>
5+
Object.assign(ctx, {
6+
readable: stream.Readable.toWeb(handle),
7+
writable: stream.Writable.toWeb(handle),
8+
});
9+
10+
globalThis.CompressionStream ??= class CompressionStream {
11+
readable;
12+
writable;
13+
14+
constructor(format) {
15+
make(
16+
this,
17+
format === 'deflate'
18+
? zlib.createDeflate()
19+
: format === 'gzip'
20+
? zlib.createGzip()
21+
: zlib.createDeflateRaw()
22+
);
23+
}
24+
};
25+
26+
globalThis.DecompressionStream ??= class DecompressionStream {
27+
readable;
28+
writable;
29+
30+
constructor(format) {
31+
make(
32+
this,
33+
format === 'deflate'
34+
? zlib.createInflate()
35+
: format === 'gzip'
36+
? zlib.createGunzip()
37+
: zlib.createInflateRaw()
38+
);
39+
}
40+
};

apps/api/src/routes/query.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,10 @@ async function executeDynamicQuery(
151151
const startDate = queryParams.start_date || queryParams.startDate;
152152
const endDate = queryParams.end_date || queryParams.endDate;
153153
const websiteId = queryParams.website_id;
154-
const websiteDomain = websiteId
155-
? (domainCache?.[websiteId] ?? (await getWebsiteDomain(websiteId)))
156-
: null;
154+
155+
const websiteDomain = websiteId && !domainCache?.[websiteId]
156+
? await getWebsiteDomain(websiteId)
157+
: domainCache?.[websiteId] || null;
157158

158159
const getTimeUnit = (granularity?: string): 'hour' | 'day' => {
159160
if (['hourly', 'hour'].includes(granularity || '')) {

apps/dashboard/app/(main)/websites/[id]/profiles/_components/profile-row.tsx

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use client';
22

3-
import { format } from 'date-fns';
3+
import dayjs from 'dayjs';
44
import {
5-
ChevronDownIcon,
6-
ChevronRightIcon,
5+
CaretDownIcon,
6+
CaretRightIcon,
77
ClockIcon,
8-
ExternalLinkIcon,
8+
ArrowSquareOutIcon,
99
EyeIcon,
10-
Users,
11-
} from 'lucide-react';
10+
UsersIcon,
11+
} from '@phosphor-icons/react';
1212
import { FaviconImage } from '@/components/analytics/favicon-image';
1313
import { Badge } from '@/components/ui/badge';
1414
import {
@@ -143,9 +143,9 @@ export function ProfileRow({
143143
{/* Expand/Collapse and Profile Number */}
144144
<div className="flex flex-shrink-0 items-center gap-3">
145145
{isExpanded ? (
146-
<ChevronDownIcon className="h-4 w-4 text-muted-foreground transition-transform" />
146+
<CaretDownIcon className="h-4 w-4 text-muted-foreground transition-transform" />
147147
) : (
148-
<ChevronRightIcon className="h-4 w-4 text-muted-foreground transition-transform" />
148+
<CaretRightIcon className="h-4 w-4 text-muted-foreground transition-transform" />
149149
)}
150150
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 font-semibold text-primary text-sm">
151151
{index + 1}
@@ -181,7 +181,7 @@ export function ProfileRow({
181181
size={16}
182182
/>
183183
) : (
184-
<ExternalLinkIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
184+
<ArrowSquareOutIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
185185
)}
186186
<span className="truncate text-muted-foreground text-sm">
187187
{profileReferrerInfo.name}
@@ -196,7 +196,7 @@ export function ProfileRow({
196196
{/* Sessions Count */}
197197
<div className="hidden min-w-[60px] flex-col items-center gap-1 sm:flex">
198198
<div className="flex items-center gap-1 text-muted-foreground text-xs">
199-
<Users className="h-3 w-3" />
199+
<UsersIcon className="h-3 w-3" />
200200
<span>Sessions</span>
201201
</div>
202202
<span className="font-semibold text-foreground text-sm">
@@ -249,19 +249,19 @@ export function ProfileRow({
249249
First Visit
250250
</div>
251251
<div className="font-medium">
252-
{profile.first_visit
253-
? format(new Date(profile.first_visit), 'MMM d, yyyy')
254-
: 'Unknown'}
252+
{profile.first_visit
253+
? dayjs(profile.first_visit).format('MMM D, YYYY')
254+
: 'Unknown'}
255255
</div>
256256
</div>
257257
<div>
258258
<div className="mb-1 text-muted-foreground text-xs">
259259
Last Visit
260260
</div>
261261
<div className="font-medium">
262-
{profile.last_visit
263-
? format(new Date(profile.last_visit), 'MMM d, yyyy')
264-
: 'Unknown'}
262+
{profile.last_visit
263+
? dayjs(profile.last_visit).format('MMM D, YYYY')
264+
: 'Unknown'}
265265
</div>
266266
</div>
267267
<div>
@@ -305,12 +305,9 @@ export function ProfileRow({
305305
{session.session_name}
306306
</div>
307307
<div className="text-muted-foreground text-xs">
308-
{session.first_visit
309-
? format(
310-
new Date(session.first_visit),
311-
'MMM d, HH:mm'
312-
)
313-
: 'Unknown'}
308+
{session.first_visit
309+
? dayjs(session.first_visit).format('MMM D, HH:mm')
310+
: 'Unknown'}
314311
</div>
315312
</div>
316313
</div>

apps/dashboard/app/(main)/websites/[id]/profiles/_components/profile-utils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GlobeIcon } from 'lucide-react';
1+
import { GlobeIcon } from '@phosphor-icons/react';
22
import {
33
getBrowserIcon,
44
getDeviceTypeIcon,

apps/dashboard/app/(main)/websites/[id]/profiles/_components/profiles-list.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import dynamic from 'next/dynamic';
34
import { UsersIcon, SpinnerIcon } from '@phosphor-icons/react';
45
import { useCallback, useEffect, useState } from 'react';
56
import { Card, CardContent } from '@/components/ui/card';
@@ -47,9 +48,16 @@ type ProfileData = {
4748
};
4849

4950
import { WebsitePageHeader } from '../../_components/website-page-header';
50-
import { ProfileRow } from './profile-row';
5151
import { getDefaultDateRange } from './profile-utils';
5252

53+
const ProfileRow = dynamic(() => import('./profile-row').then(mod => ({ default: mod.ProfileRow })), {
54+
loading: () => (
55+
<div className="flex items-center justify-center p-4">
56+
<SpinnerIcon className="h-4 w-4 animate-spin" />
57+
</div>
58+
)
59+
});
60+
5361
interface ProfilesListProps {
5462
websiteId: string;
5563
}

apps/dashboard/app/(main)/websites/[id]/profiles/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
'use client';
22

33
import { useParams } from 'next/navigation';
4-
import { ProfilesList } from './_components';
4+
import dynamic from 'next/dynamic';
5+
import { SpinnerIcon } from '@phosphor-icons/react';
6+
7+
const ProfilesList = dynamic(() => import('./_components').then(mod => ({ default: mod.ProfilesList })), {
8+
loading: () => (
9+
<div className="flex items-center justify-center p-8">
10+
<SpinnerIcon className="h-6 w-6 animate-spin" />
11+
</div>
12+
),
13+
ssr: false
14+
});
515

616
export default function ProfilesPage() {
717
const { id: websiteId } = useParams();

0 commit comments

Comments
 (0)