From 6f4cd4f4c17288e4c25c902c7d50a4919d2df8b7 Mon Sep 17 00:00:00 2001 From: Leothosine Date: Fri, 26 Jun 2026 20:15:46 +0100 Subject: [PATCH 1/3] Add analytics batch cap and request deduplication interceptor Closes #870, #869 --- .../services/batch-size-guard.service.ts | 23 +++++++++++++ .../interceptors/request-dedup.interceptor.ts | 32 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/analytics/services/batch-size-guard.service.ts create mode 100644 src/common/interceptors/request-dedup.interceptor.ts diff --git a/src/analytics/services/batch-size-guard.service.ts b/src/analytics/services/batch-size-guard.service.ts new file mode 100644 index 00000000..92214d6a --- /dev/null +++ b/src/analytics/services/batch-size-guard.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchSizeGuardService { + private readonly maxSize: number = 10000; + private droppedCount: number = 0; + + canAdd(currentSize: number): boolean { + if (currentSize >= this.maxSize) { + this.droppedCount++; + return false; + } + return true; + } + + getDroppedCount(): number { + return this.droppedCount; + } + + resetDroppedCount(): void { + this.droppedCount = 0; + } +} diff --git a/src/common/interceptors/request-dedup.interceptor.ts b/src/common/interceptors/request-dedup.interceptor.ts new file mode 100644 index 00000000..1648fb82 --- /dev/null +++ b/src/common/interceptors/request-dedup.interceptor.ts @@ -0,0 +1,32 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class RequestDedupInterceptor implements NestInterceptor { + private cache = new Map(); + private readonly ttlMs = 60000; + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + if (req.method !== 'POST') return next.handle(); + + const key = this.fingerprint(req); + const cached = this.cache.get(key); + if (cached && Date.now() - cached.timestamp < this.ttlMs) { + const res = context.switchToHttp().getResponse(); + res.setHeader('X-Duplicate-Request', 'true'); + return of(cached.response); + } + + return next.handle().pipe( + tap((response) => { + this.cache.set(key, { response, timestamp: Date.now() }); + }), + ); + } + + private fingerprint(req: any): string { + return ${req.method}:::; + } +} From ddda1b54c6b40134b14c641806977cb5da13e59a Mon Sep 17 00:00:00 2001 From: Leothosine Date: Fri, 26 Jun 2026 21:54:59 +0100 Subject: [PATCH 2/3] fix: ci syntax error --- src/common/interceptors/request-dedup.interceptor.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/interceptors/request-dedup.interceptor.ts b/src/common/interceptors/request-dedup.interceptor.ts index 1648fb82..45724326 100644 --- a/src/common/interceptors/request-dedup.interceptor.ts +++ b/src/common/interceptors/request-dedup.interceptor.ts @@ -27,6 +27,10 @@ export class RequestDedupInterceptor implements NestInterceptor { } private fingerprint(req: any): string { - return ${req.method}:::; + const method = req.method; + const path = req.path; + const body = JSON.stringify(req.body); + const userId = req.user ? req.user.id : 'anon'; + return method + ':' + path + ':' + body + ':' + userId; } } From f36ca7670f8589d4d716e85a23ac705c0c032fd9 Mon Sep 17 00:00:00 2001 From: Leothosine Date: Fri, 26 Jun 2026 22:12:03 +0100 Subject: [PATCH 3/3] fix: use template literal for fingerprint --- src/common/interceptors/request-dedup.interceptor.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/common/interceptors/request-dedup.interceptor.ts b/src/common/interceptors/request-dedup.interceptor.ts index 45724326..99ccb7c1 100644 --- a/src/common/interceptors/request-dedup.interceptor.ts +++ b/src/common/interceptors/request-dedup.interceptor.ts @@ -27,10 +27,7 @@ export class RequestDedupInterceptor implements NestInterceptor { } private fingerprint(req: any): string { - const method = req.method; - const path = req.path; - const body = JSON.stringify(req.body); - const userId = req.user ? req.user.id : 'anon'; - return method + ':' + path + ':' + body + ':' + userId; + const id = req.user ? req.user.id : 'anon'; + return `${req.method}:${req.path}:${JSON.stringify(req.body)}:${id}`; } }