feat(analytics): persona logging upgrade and Sentry system config

Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 22:30:02 +08:00
parent ab10431001
commit 89fd333702
91 changed files with 1805 additions and 136 deletions
@@ -0,0 +1,55 @@
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import type { Response } from 'express';
import type { AuthUser } from '../guards/jwt-auth.guard';
import type { RequestWithId } from './request-id.middleware';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const started = Date.now();
const http = context.switchToHttp();
const req = http.getRequest<RequestWithId & { user?: AuthUser }>();
const res = http.getResponse<Response>();
return next.handle().pipe(
tap({
next: () => this.logLine(req, res.statusCode, Date.now() - started),
error: (err: { status?: number; message?: string }) => {
const status = err?.status ?? res.statusCode ?? 500;
this.logLine(req, status, Date.now() - started, err?.message);
},
}),
);
}
private logLine(
req: RequestWithId & { user?: AuthUser },
status: number,
latencyMs: number,
error?: string,
) {
const line = {
requestId: req.requestId,
method: req.method,
path: req.originalUrl || req.url,
status,
latencyMs,
clientApp: req.headers['x-client-app'],
actorType: req.user?.actorType,
actorId: req.user?.actorId != null ? String(req.user.actorId) : undefined,
error: error?.slice(0, 200),
};
if (status >= 500) this.logger.error(JSON.stringify(line));
else if (status >= 400) this.logger.warn(JSON.stringify(line));
else this.logger.log(JSON.stringify(line));
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RequestIdMiddleware } from './request-id.middleware';
import { LoggingInterceptor } from './logging.interceptor';
@Module({
providers: [RequestIdMiddleware, LoggingInterceptor],
exports: [RequestIdMiddleware, LoggingInterceptor],
})
export class LoggingModule {}
@@ -0,0 +1,21 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { randomUUID } from 'crypto';
import type { Request, Response, NextFunction } from 'express';
export const REQUEST_ID_HEADER = 'x-request-id';
export type RequestWithId = Request & { requestId?: string };
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: RequestWithId, res: Response, next: NextFunction) {
const incoming = req.headers[REQUEST_ID_HEADER];
const requestId =
typeof incoming === 'string' && incoming.trim()
? incoming.trim().slice(0, 64)
: randomUUID();
req.requestId = requestId;
res.setHeader('X-Request-Id', requestId);
next();
}
}