Skip to content

Commit 1fe4adc

Browse files
committed
fix: vector blocked traffic batch
1 parent a535532 commit 1fe4adc

File tree

7 files changed

+129
-140
lines changed

7 files changed

+129
-140
lines changed

apps/basket/src/lib/blocked-traffic.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import { sanitizeString, VALIDATION_LIMITS } from "../utils/validation";
66
import { logger } from "./logger";
77
import { sendEvent } from "./producer";
88

9-
/**
10-
* Log blocked traffic for security and monitoring purposes
11-
*/
12-
export async function logBlockedTraffic(
9+
async function _logBlockedTrafficAsync(
1310
request: Request,
1411
body: any,
1512
_query: any,
@@ -106,3 +103,28 @@ export async function logBlockedTraffic(
106103
logger.error({ error }, "Failed to log blocked traffic");
107104
}
108105
}
106+
107+
/**
108+
* Log blocked traffic for security and monitoring purposes (fire-and-forget)
109+
*/
110+
export function logBlockedTraffic(
111+
request: Request,
112+
body: any,
113+
query: any,
114+
blockReason: string,
115+
blockCategory: string,
116+
botName?: string,
117+
clientId?: string
118+
): void {
119+
_logBlockedTrafficAsync(
120+
request,
121+
body,
122+
query,
123+
blockReason,
124+
blockCategory,
125+
botName,
126+
clientId
127+
).catch((error) => {
128+
logger.error({ error }, "Failed to log blocked traffic");
129+
});
130+
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "../utils/validation";
1717
import { logger } from "./logger";
1818
import { sendEvent, sendEventBatch } from "./producer";
19-
import { checkDuplicate } from "./security";
19+
import { checkDuplicate, getDailySalt, saltAnonymousId } from "./security";
2020

2121
/**
2222
* Insert an error event into the database
@@ -291,16 +291,25 @@ export async function insertTrackEvent(
291291
eventId = randomUUID();
292292
}
293293

294-
const [isDuplicate, geoData] = await Promise.all([
294+
const [isDuplicate, geoData, salt] = await Promise.all([
295295
checkDuplicate(eventId, "track"),
296296
getGeo(ip),
297+
getDailySalt(),
297298
]);
298299

299300
if (isDuplicate) {
300301
console.timeEnd("insertTrackEvent");
301302
return;
302303
}
303304

305+
let anonymousId = sanitizeString(
306+
trackData.anonymousId,
307+
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
308+
);
309+
if (anonymousId) {
310+
anonymousId = saltAnonymousId(anonymousId, salt);
311+
}
312+
304313
const { anonymizedIP, country, region, city } = geoData;
305314
const {
306315
browserName,
@@ -320,10 +329,7 @@ export async function insertTrackEvent(
320329
trackData.name,
321330
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
322331
),
323-
anonymous_id: sanitizeString(
324-
trackData.anonymousId,
325-
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
326-
),
332+
anonymous_id: anonymousId,
327333
time: typeof trackData.timestamp === "number" ? trackData.timestamp : now,
328334
session_id: validateSessionId(trackData.sessionId),
329335
event_type: "track",

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export async function validateRequest(
5252
): Promise<ValidationResult | ValidationError> {
5353
console.time("validateRequest");
5454
if (!validatePayloadSize(body, VALIDATION_LIMITS.PAYLOAD_MAX_SIZE)) {
55-
await logBlockedTraffic(
55+
logBlockedTraffic(
5656
request,
5757
body,
5858
query,
@@ -68,7 +68,7 @@ export async function validateRequest(
6868
VALIDATION_LIMITS.SHORT_STRING_MAX_LENGTH
6969
);
7070
if (!clientId) {
71-
await logBlockedTraffic(
71+
logBlockedTraffic(
7272
request,
7373
body,
7474
query,
@@ -81,7 +81,7 @@ export async function validateRequest(
8181

8282
const website = await getWebsiteByIdV2(clientId);
8383
if (!website || website.status !== "ACTIVE") {
84-
await logBlockedTraffic(
84+
logBlockedTraffic(
8585
request,
8686
body,
8787
query,
@@ -101,7 +101,7 @@ export async function validateRequest(
101101
const data = await checkAutumnCached(website.ownerId);
102102

103103
if (data && !(data.allowed || data.overage_allowed)) {
104-
await logBlockedTraffic(
104+
logBlockedTraffic(
105105
request,
106106
body,
107107
query,
@@ -120,7 +120,7 @@ export async function validateRequest(
120120

121121
const origin = request.headers.get("origin");
122122
if (origin && !isValidOrigin(origin, website.domain)) {
123-
await logBlockedTraffic(
123+
logBlockedTraffic(
124124
request,
125125
body,
126126
query,
@@ -155,16 +155,16 @@ export async function validateRequest(
155155
* Check if request is from a bot
156156
* Returns error object if bot detected, undefined otherwise
157157
*/
158-
export async function checkForBot(
158+
export function checkForBot(
159159
request: Request,
160160
body: any,
161161
query: any,
162162
clientId: string,
163163
userAgent: string
164-
): Promise<{ error?: { status: string } } | undefined> {
164+
): { error?: { status: string } } | undefined {
165165
const botCheck = detectBot(userAgent, request);
166166
if (botCheck.isBot) {
167-
await logBlockedTraffic(
167+
logBlockedTraffic(
168168
request,
169169
body,
170170
query,

apps/basket/src/lib/security.ts

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
11
import crypto, { createHash } from "node:crypto";
2-
import { redis } from "@databuddy/redis";
2+
import { cacheable, redis } from "@databuddy/redis";
33
import { logger } from "./logger";
44

5-
const MS_PER_DAY = 24 * 60 * 60 * 1000;
6-
const SALT_TTL = 60 * 60 * 24;
75
const EXIT_EVENT_TTL = 172_800;
86
const STANDARD_EVENT_TTL = 86_400;
97

108
function getCurrentDay(): number {
9+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
1110
return Math.floor(Date.now() / MS_PER_DAY);
1211
}
1312

14-
export async function getDailySalt(): Promise<string> {
15-
console.time("getDailySalt");
16-
const saltKey = `salt:${getCurrentDay()}`;
17-
try {
18-
const salt = await redis.get(saltKey);
19-
if (salt) {
13+
export const getDailySalt = cacheable(
14+
async (): Promise<string> => {
15+
console.time("getDailySalt");
16+
const saltKey = `salt:${getCurrentDay()}`;
17+
try {
18+
const salt = await redis.get(saltKey);
19+
if (salt) {
20+
console.timeEnd("getDailySalt");
21+
return salt;
22+
}
23+
24+
const newSalt = crypto.randomBytes(32).toString("hex");
25+
const SALT_TTL = 60 * 60 * 24;
26+
redis.setex(saltKey, SALT_TTL, newSalt).catch((error) => {
27+
logger.error({ error }, "Failed to set daily salt in Redis");
28+
});
29+
console.timeEnd("getDailySalt");
30+
return newSalt;
31+
} catch (error) {
32+
logger.error({ error }, "Failed to get daily salt from Redis");
2033
console.timeEnd("getDailySalt");
21-
return salt;
34+
return crypto.randomBytes(32).toString("hex");
2235
}
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;
30-
} catch (error) {
31-
logger.error({ error }, "Failed to get daily salt from Redis");
32-
console.timeEnd("getDailySalt");
33-
return crypto.randomBytes(32).toString("hex");
36+
},
37+
{
38+
expireInSec: 3600,
39+
prefix: "daily_salt",
40+
staleWhileRevalidate: true,
41+
staleTime: 300,
3442
}
35-
}
43+
);
3644

3745
export function saltAnonymousId(anonymousId: string, salt: string): string {
3846
try {
@@ -49,18 +57,12 @@ export async function checkDuplicate(
4957
): Promise<boolean> {
5058
console.time("checkDuplicate");
5159
const key = `dedup:${eventType}:${eventId}`;
52-
try {
53-
if (await redis.exists(key)) {
54-
console.timeEnd("checkDuplicate");
55-
return true;
56-
}
60+
const ttl = eventId.startsWith("exit_") ? EXIT_EVENT_TTL : STANDARD_EVENT_TTL;
5761

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+
try {
63+
const result = await redis.set(key, "1", "EX", ttl, "NX");
6264
console.timeEnd("checkDuplicate");
63-
return false;
65+
return result === null;
6466
} catch (error) {
6567
logger.error({ error, eventId, eventType }, "Failed to check duplicate event in Redis");
6668
console.timeEnd("checkDuplicate");

0 commit comments

Comments
 (0)