Skip to content

Commit a535532

Browse files
committed
refactoring, throughput increase
1 parent 7734695 commit a535532

File tree

8 files changed

+155
-168
lines changed

8 files changed

+155
-168
lines changed

apps/basket/src/hooks/auth.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ const REGEX_DOMAIN_LABEL = /^[a-zA-Z0-9-]+$/;
2626
* @returns A promise that resolves to the owner's user ID or null if not found.
2727
*/
2828
async function _resolveOwnerId(website: Website): Promise<string | null> {
29+
console.time("_resolveOwnerId");
2930
if (website.userId) {
31+
console.timeEnd("_resolveOwnerId");
3032
return website.userId;
3133
}
3234

@@ -43,6 +45,7 @@ async function _resolveOwnerId(website: Website): Promise<string | null> {
4345
});
4446

4547
if (orgMember) {
48+
console.timeEnd("_resolveOwnerId");
4649
return orgMember.userId;
4750
}
4851

@@ -66,6 +69,7 @@ async function _resolveOwnerId(website: Website): Promise<string | null> {
6669
{ websiteId: website.id },
6770
"No owner could be determined for website"
6871
);
72+
console.timeEnd("_resolveOwnerId");
6973
return null;
7074
}
7175

@@ -324,50 +328,43 @@ export function isLocalhost(hostname: string): boolean {
324328
); // IPv4 loopback
325329
}
326330

327-
const getWebsiteByIdCached = cacheable(
328-
async (id: string): Promise<Website | null> => {
331+
const getWebsiteByIdWithOwnerCached = cacheable(
332+
async (id: string): Promise<WebsiteWithOwner | null> => {
329333
try {
330334
const website = await db.query.websites.findFirst({
331335
where: eq(websites.id, id),
332336
});
333-
return website ?? null;
337+
338+
if (!website) {
339+
return null;
340+
}
341+
342+
const ownerId = await _resolveOwnerId(website);
343+
return { ...website, ownerId };
334344
} catch (error) {
335345
logger.error({ error, websiteId: id }, "Failed to get website by ID from cache");
336346
return null;
337347
}
338348
},
339349
{
340-
expireInSec: 300, // 5 minutes
341-
prefix: "website_by_id",
342-
staleWhileRevalidate: true,
343-
staleTime: 60, // 1 minute
344-
}
345-
);
346-
347-
const getOwnerIdCached = cacheable(
348-
async (website: Website): Promise<string | null> =>
349-
await _resolveOwnerId(website),
350-
{
351-
expireInSec: 300,
352-
prefix: "website_owner_id",
350+
expireInSec: 600, // 10 minutes - longer cache for better performance
351+
prefix: "website_with_owner_v2",
353352
staleWhileRevalidate: true,
354-
staleTime: 60,
353+
staleTime: 120, // 2 minutes stale time
355354
}
356355
);
357356

358357
export async function getWebsiteByIdV2(
359358
id: string
360359
): Promise<WebsiteWithOwner | null> {
360+
console.time("getWebsiteByIdV2");
361361
try {
362-
const website = await getWebsiteByIdCached(id);
363-
if (!website) {
364-
return null;
365-
}
366-
367-
const ownerId = await getOwnerIdCached(website);
368-
return { ...website, ownerId };
362+
const result = await getWebsiteByIdWithOwnerCached(id);
363+
console.timeEnd("getWebsiteByIdV2");
364+
return result;
369365
} catch (error) {
370366
logger.error({ error, websiteId: id }, "Failed to get website by ID V2");
367+
console.timeEnd("getWebsiteByIdV2");
371368
return null;
372369
}
373370
}

apps/basket/src/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@ const app = new Elysia()
4545
);
4646
})
4747
.onBeforeHandle(({ request, set }) => {
48-
// const { isBot } = await checkBotId();
49-
// if (isBot) {
50-
// return new Response(null, { status: 403 });
51-
// }
5248
const origin = request.headers.get("origin");
5349
if (origin) {
5450
set.headers ??= {};

apps/basket/src/lib/event-service.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ export async function insertError(
3636
eventId = randomUUID();
3737
}
3838

39-
if (await checkDuplicate(eventId, "error")) {
39+
const [isDuplicate, geoData] = await Promise.all([
40+
checkDuplicate(eventId, "error"),
41+
getGeo(ip),
42+
]);
43+
44+
if (isDuplicate) {
4045
return;
4146
}
4247

4348
const payload = errorData.payload;
4449
const now = Date.now();
4550

46-
const { anonymizedIP, country, region } = await getGeo(ip);
51+
const { anonymizedIP, country, region } = geoData;
4752
const { browserName, browserVersion, osName, osVersion, deviceType } =
4853
parseUserAgent(userAgent);
4954

@@ -276,6 +281,7 @@ export async function insertTrackEvent(
276281
userAgent: string,
277282
ip: string
278283
): Promise<void> {
284+
console.time("insertTrackEvent");
279285
let eventId = sanitizeString(
280286
trackData.eventId,
281287
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
@@ -285,11 +291,17 @@ export async function insertTrackEvent(
285291
eventId = randomUUID();
286292
}
287293

288-
if (await checkDuplicate(eventId, "track")) {
294+
const [isDuplicate, geoData] = await Promise.all([
295+
checkDuplicate(eventId, "track"),
296+
getGeo(ip),
297+
]);
298+
299+
if (isDuplicate) {
300+
console.timeEnd("insertTrackEvent");
289301
return;
290302
}
291303

292-
const { anonymizedIP, country, region, city } = await getGeo(ip);
304+
const { anonymizedIP, country, region, city } = geoData;
293305
const {
294306
browserName,
295307
browserVersion,
@@ -381,11 +393,13 @@ export async function insertTrackEvent(
381393

382394
try {
383395
sendEvent("analytics-events", trackEvent);
396+
console.timeEnd("insertTrackEvent");
384397
} catch (error) {
385398
logger.error(
386399
{ error, eventId },
387400
"Failed to queue track event"
388401
);
402+
console.timeEnd("insertTrackEvent");
389403
}
390404
}
391405

apps/basket/src/lib/request-validation.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cacheable } from "@databuddy/redis";
12
import { Autumn as autumn } from "autumn-js";
23
import { getWebsiteByIdV2, isValidOrigin } from "../hooks/auth";
34
import { extractIpFromRequest } from "../utils/ip-geo";
@@ -22,6 +23,25 @@ type ValidationError = {
2223
error: { status: string; message: string };
2324
};
2425

26+
const checkAutumnCached = cacheable(
27+
async (ownerId: string) => {
28+
console.time("autumnCheck");
29+
const result = await autumn.check({
30+
customer_id: ownerId,
31+
feature_id: "events",
32+
send_event: true,
33+
});
34+
console.timeEnd("autumnCheck");
35+
return result.data;
36+
},
37+
{
38+
expireInSec: 60,
39+
prefix: "autumn_check_events",
40+
staleWhileRevalidate: true,
41+
staleTime: 30,
42+
}
43+
);
44+
2545
/**
2646
* Validate incoming request for analytics events
2747
*/
@@ -30,6 +50,7 @@ export async function validateRequest(
3050
query: any,
3151
request: Request
3252
): Promise<ValidationResult | ValidationError> {
53+
console.time("validateRequest");
3354
if (!validatePayloadSize(body, VALIDATION_LIMITS.PAYLOAD_MAX_SIZE)) {
3455
await logBlockedTraffic(
3556
request,
@@ -38,6 +59,7 @@ export async function validateRequest(
3859
"payload_too_large",
3960
"Validation Error"
4061
);
62+
console.timeEnd("validateRequest");
4163
return { error: { status: "error", message: "Payload too large" } };
4264
}
4365

@@ -53,6 +75,7 @@ export async function validateRequest(
5375
"missing_client_id",
5476
"Validation Error"
5577
);
78+
console.timeEnd("validateRequest");
5679
return { error: { status: "error", message: "Missing client ID" } };
5780
}
5881

@@ -67,18 +90,15 @@ export async function validateRequest(
6790
undefined,
6891
clientId
6992
);
93+
console.timeEnd("validateRequest");
7094
return {
7195
error: { status: "error", message: "Invalid or inactive client ID" },
7296
};
7397
}
7498

7599
if (website.ownerId) {
76100
try {
77-
const { data } = await autumn.check({
78-
customer_id: website.ownerId,
79-
feature_id: "events",
80-
send_event: true,
81-
});
101+
const data = await checkAutumnCached(website.ownerId);
82102

83103
if (data && !(data.allowed || data.overage_allowed)) {
84104
await logBlockedTraffic(
@@ -90,6 +110,7 @@ export async function validateRequest(
90110
undefined,
91111
clientId
92112
);
113+
console.timeEnd("validateRequest");
93114
return { error: { status: "error", message: "Exceeded event limit" } };
94115
}
95116
} catch (error) {
@@ -108,6 +129,7 @@ export async function validateRequest(
108129
undefined,
109130
clientId
110131
);
132+
console.timeEnd("validateRequest");
111133
return { error: { status: "error", message: "Origin not authorized" } };
112134
}
113135

@@ -119,6 +141,7 @@ export async function validateRequest(
119141

120142
const ip = extractIpFromRequest(request);
121143

144+
console.timeEnd("validateRequest");
122145
return {
123146
success: true,
124147
clientId,

apps/basket/src/lib/security.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,69 @@
11
import crypto, { createHash } from "node:crypto";
22
import { redis } from "@databuddy/redis";
33
import { logger } from "./logger";
4-
/**
5-
* Get or generate a daily salt for anonymizing user IDs
6-
* The salt rotates daily to maintain privacy while allowing same-day tracking
7-
*/
4+
5+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
6+
const SALT_TTL = 60 * 60 * 24;
7+
const EXIT_EVENT_TTL = 172_800;
8+
const STANDARD_EVENT_TTL = 86_400;
9+
10+
function getCurrentDay(): number {
11+
return Math.floor(Date.now() / MS_PER_DAY);
12+
}
13+
814
export async function getDailySalt(): Promise<string> {
9-
const saltKey = `salt:${Math.floor(Date.now() / (24 * 60 * 60 * 1000))}`;
15+
console.time("getDailySalt");
16+
const saltKey = `salt:${getCurrentDay()}`;
1017
try {
11-
let salt = await redis.get(saltKey);
12-
if (!salt) {
13-
salt = crypto.randomBytes(32).toString("hex");
14-
await redis.setex(saltKey, 60 * 60 * 24, salt);
18+
const salt = await redis.get(saltKey);
19+
if (salt) {
20+
console.timeEnd("getDailySalt");
21+
return salt;
1522
}
16-
return salt;
23+
24+
const newSalt = crypto.randomBytes(32).toString("hex");
25+
redis.setex(saltKey, SALT_TTL, newSalt).catch((error) => {
26+
logger.error({ error }, "Failed to set daily salt in Redis");
27+
});
28+
console.timeEnd("getDailySalt");
29+
return newSalt;
1730
} catch (error) {
18-
logger.error({ error }, "Failed to get or set daily salt from Redis");
19-
// Fallback: generate a new salt if Redis fails
20-
// This ensures the function doesn't break, but salt won't be shared across instances
31+
logger.error({ error }, "Failed to get daily salt from Redis");
32+
console.timeEnd("getDailySalt");
2133
return crypto.randomBytes(32).toString("hex");
2234
}
2335
}
2436

25-
/**
26-
* Salt and hash an anonymous ID for privacy
27-
*/
2837
export function saltAnonymousId(anonymousId: string, salt: string): string {
2938
try {
30-
return createHash("sha256")
31-
.update(anonymousId + salt)
32-
.digest("hex");
39+
return createHash("sha256").update(anonymousId + salt).digest("hex");
3340
} catch (error) {
3441
logger.error({ error, anonymousId }, "Failed to salt anonymous ID");
35-
// Fallback: return a hash of just the anonymousId if salting fails
3642
return createHash("sha256").update(anonymousId).digest("hex");
3743
}
3844
}
3945

40-
/**
41-
* Check if an event has already been processed (deduplication)
42-
* Returns true if duplicate, false if new
43-
*/
4446
export async function checkDuplicate(
4547
eventId: string,
4648
eventType: string
4749
): Promise<boolean> {
50+
console.time("checkDuplicate");
4851
const key = `dedup:${eventType}:${eventId}`;
4952
try {
5053
if (await redis.exists(key)) {
54+
console.timeEnd("checkDuplicate");
5155
return true;
5256
}
5357

54-
const ttl = eventId.startsWith("exit_") ? 172_800 : 86_400;
55-
await redis.setex(key, ttl, "1");
58+
const ttl = eventId.startsWith("exit_") ? EXIT_EVENT_TTL : STANDARD_EVENT_TTL;
59+
redis.setex(key, ttl, "1").catch((error) => {
60+
logger.error({ error, eventId, eventType }, "Failed to set duplicate key in Redis");
61+
});
62+
console.timeEnd("checkDuplicate");
5663
return false;
5764
} catch (error) {
5865
logger.error({ error, eventId, eventType }, "Failed to check duplicate event in Redis");
59-
// Return false (not duplicate) to avoid blocking events if Redis fails
60-
// This allows events to proceed, but deduplication won't work
66+
console.timeEnd("checkDuplicate");
6167
return false;
6268
}
6369
}

0 commit comments

Comments
 (0)