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
@@ -5,8 +5,9 @@ import {
NestInterceptor,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, tap } from 'rxjs';
import { Observable, catchError, tap, throwError } from 'rxjs';
import type { AuthUser } from '../guards/jwt-auth.guard';
import type { RequestWithId } from '../logging/request-id.middleware';
import { HQ_OPERATION_KEY, type HqOperationMeta } from './hq-operation.decorator';
import { HqOperationLogService } from './hq-operation-log.service';
@@ -61,42 +62,51 @@ export class HqOperationInterceptor implements NestInterceptor {
);
if (!meta) return next.handle();
const req = context.switchToHttp().getRequest();
const req = context.switchToHttp().getRequest<RequestWithId & { user?: AuthUser }>();
const user = req.user as AuthUser | undefined;
if (!user || user.actorType !== 'HQ') {
return next.handle();
}
const writeLog = (status: 'SUCCESS' | 'FAILED', data?: unknown, errorMessage?: string) => {
const refId = meta.batch
? 0n
: pickRefId(meta.refIdParam ? req.params?.[meta.refIdParam] : null)
?? pickRefId(
meta.refIdField
? (data as Record<string, unknown> | null)?.[meta.refIdField]
: (data as Record<string, unknown> | null)?.id,
)
?? 0n;
const detail: Record<string, unknown> = {
method: req.method,
path: req.originalUrl ?? req.url,
requestId: req.requestId,
};
if (meta.includeBody && req.body) {
detail.requestBody = sanitizeBody(req.body);
}
if (status === 'SUCCESS' && meta.includeResponse !== false && data != null) {
detail.response = summarizeResponse(data);
}
if (errorMessage) detail.error = errorMessage;
this.logService.logSafe({
hqAccountId: user.actorId,
action: meta.action,
refType: meta.refType,
refId,
status,
detail,
});
};
return next.handle().pipe(
tap((data) => {
const refId = meta.batch
? 0n
: pickRefId(meta.refIdParam ? req.params?.[meta.refIdParam] : null)
?? pickRefId(
meta.refIdField
? (data as Record<string, unknown> | null)?.[meta.refIdField]
: (data as Record<string, unknown> | null)?.id,
)
?? 0n;
const detail: Record<string, unknown> = {
method: req.method,
path: req.originalUrl ?? req.url,
};
if (meta.includeBody && req.body) {
detail.requestBody = sanitizeBody(req.body);
}
if (meta.includeResponse !== false && data != null) {
detail.response = summarizeResponse(data);
}
this.logService.logSafe({
hqAccountId: user.actorId,
action: meta.action,
refType: meta.refType,
refId,
detail,
});
tap((data) => writeLog('SUCCESS', data)),
catchError((err: { message?: string }) => {
writeLog('FAILED', undefined, err?.message ?? '操作失败');
return throwError(() => err);
}),
);
}