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:
@@ -1,13 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { ClientApp } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
export type TrackEventInput = {
|
||||
eventName: string;
|
||||
pagePath?: string;
|
||||
refType?: string;
|
||||
refId?: bigint;
|
||||
sessionId?: string;
|
||||
sourceType?: string;
|
||||
sourceRefId?: bigint;
|
||||
extraJson?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@@ -17,35 +19,80 @@ export type TrackStoreEventInput = TrackEventInput & {
|
||||
};
|
||||
|
||||
export type TrackPartnerEventInput = TrackEventInput & {
|
||||
/** 主账号 ID,用于合伙人维度聚合 */
|
||||
partnerAccountId: bigint;
|
||||
};
|
||||
|
||||
type RawEvent = { eventName: string; params?: Record<string, unknown> };
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async trackBatch(
|
||||
userId: bigint,
|
||||
async trackBatchOptional(
|
||||
userId: bigint | null | undefined,
|
||||
clientApp: string,
|
||||
events: Array<{ eventName: string; params?: Record<string, unknown> }>,
|
||||
events: RawEvent[],
|
||||
sessionId?: string,
|
||||
) {
|
||||
if (!events?.length) return { count: 0 };
|
||||
await this.prisma.logUserAnalytics.createMany({
|
||||
data: events.map((e) => this.toRow(userId, clientApp, {
|
||||
eventName: e.eventName,
|
||||
extraJson: e.params,
|
||||
refType: typeof e.params?.refType === 'string' ? e.params.refType : undefined,
|
||||
refId: e.params?.refId != null ? BigInt(String(e.params.refId)) : undefined,
|
||||
pagePath: typeof e.params?.pagePath === 'string' ? e.params.pagePath : undefined,
|
||||
})),
|
||||
data: events.map((e) =>
|
||||
this.toUserRow(userId ?? null, clientApp, {
|
||||
eventName: e.eventName,
|
||||
...this.parseParams(e.params, sessionId),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
async trackBatch(userId: bigint, clientApp: string, events: RawEvent[], sessionId?: string) {
|
||||
return this.trackBatchOptional(userId, clientApp, events, sessionId);
|
||||
}
|
||||
|
||||
async trackStoreBatch(
|
||||
storeAccountId: bigint | undefined,
|
||||
storeId: bigint,
|
||||
clientApp: ClientApp | string,
|
||||
events: RawEvent[],
|
||||
sessionId?: string,
|
||||
) {
|
||||
if (!events?.length) return { count: 0 };
|
||||
await this.prisma.logStoreAnalytics.createMany({
|
||||
data: events.map((e) =>
|
||||
this.toStoreRow(storeAccountId, clientApp, {
|
||||
storeId,
|
||||
eventName: e.eventName,
|
||||
...this.parseParams(e.params, sessionId),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
async trackPartnerBatch(
|
||||
actorAccountId: bigint | undefined,
|
||||
partnerAccountId: bigint,
|
||||
clientApp: ClientApp | string,
|
||||
events: RawEvent[],
|
||||
sessionId?: string,
|
||||
) {
|
||||
if (!events?.length) return { count: 0 };
|
||||
await this.prisma.logPartnerAnalytics.createMany({
|
||||
data: events.map((e) =>
|
||||
this.toPartnerRow(actorAccountId, clientApp, {
|
||||
partnerAccountId,
|
||||
eventName: e.eventName,
|
||||
...this.parseParams(e.params, sessionId),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return { count: events.length };
|
||||
}
|
||||
|
||||
async trackOne(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
await this.prisma.logUserAnalytics.create({
|
||||
data: this.toRow(userId, clientApp, event),
|
||||
data: this.toUserRow(userId, clientApp, event),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +100,24 @@ export class AnalyticsService {
|
||||
void this.trackOne(userId, clientApp, event).catch(() => {});
|
||||
}
|
||||
|
||||
async trackStoreOne(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) {
|
||||
trackOneSafeOptional(
|
||||
userId: bigint | null | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackEventInput,
|
||||
) {
|
||||
void this.trackBatchOptional(
|
||||
userId,
|
||||
clientApp,
|
||||
[{ eventName: event.eventName, params: this.eventToParams(event) }],
|
||||
event.sessionId,
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
async trackStoreOne(
|
||||
storeAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackStoreEventInput,
|
||||
) {
|
||||
if (event.storeId == null) return;
|
||||
await this.prisma.logStoreAnalytics.create({
|
||||
data: this.toStoreRow(storeAccountId, clientApp, event),
|
||||
@@ -83,14 +147,61 @@ export class AnalyticsService {
|
||||
void this.trackPartnerOne(actorAccountId, clientApp, event).catch(() => {});
|
||||
}
|
||||
|
||||
private toRow(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
private parseParams(
|
||||
params?: Record<string, unknown>,
|
||||
fallbackSessionId?: string,
|
||||
): Omit<TrackEventInput, 'eventName'> {
|
||||
const p = params ?? {};
|
||||
const sessionId =
|
||||
typeof p.sessionId === 'string' ? p.sessionId.slice(0, 64) : fallbackSessionId?.slice(0, 64);
|
||||
const pagePath = typeof p.pagePath === 'string' ? p.pagePath.slice(0, 128) : undefined;
|
||||
const refType = typeof p.refType === 'string' ? p.refType : undefined;
|
||||
const refId = p.refId != null ? BigInt(String(p.refId)) : undefined;
|
||||
const sourceType = typeof p.sourceType === 'string' ? p.sourceType.slice(0, 32) : undefined;
|
||||
const sourceRefId = p.sourceRefId != null ? BigInt(String(p.sourceRefId)) : undefined;
|
||||
const {
|
||||
sessionId: _s,
|
||||
pagePath: _p,
|
||||
refType: _rt,
|
||||
refId: _ri,
|
||||
sourceType: _st,
|
||||
sourceRefId: _sr,
|
||||
...rest
|
||||
} = p;
|
||||
return {
|
||||
sessionId,
|
||||
pagePath,
|
||||
refType,
|
||||
refId,
|
||||
sourceType,
|
||||
sourceRefId,
|
||||
extraJson: Object.keys(rest).length ? rest : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private eventToParams(event: TrackEventInput): Record<string, unknown> {
|
||||
return {
|
||||
...(event.pagePath ? { pagePath: event.pagePath } : {}),
|
||||
...(event.refType ? { refType: event.refType } : {}),
|
||||
...(event.refId != null ? { refId: event.refId.toString() } : {}),
|
||||
...(event.sessionId ? { sessionId: event.sessionId } : {}),
|
||||
...(event.sourceType ? { sourceType: event.sourceType } : {}),
|
||||
...(event.sourceRefId != null ? { sourceRefId: event.sourceRefId.toString() } : {}),
|
||||
...(event.extraJson ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
private toUserRow(userId: bigint | null, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
return {
|
||||
userId,
|
||||
sessionId: event.sessionId,
|
||||
eventName: event.eventName,
|
||||
clientApp: clientApp as ClientApp,
|
||||
pagePath: event.pagePath,
|
||||
refType: event.refType,
|
||||
refId: event.refId,
|
||||
sourceType: event.sourceType,
|
||||
sourceRefId: event.sourceRefId,
|
||||
extraJson: event.extraJson as never,
|
||||
};
|
||||
}
|
||||
@@ -126,4 +237,3 @@ export class AnalyticsService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user