import { Injectable } from '@nestjs/common'; import type { ClientApp } from '@prisma/client'; import { PrismaService } from '../../common/prisma/prisma.module'; export type TrackEventInput = { eventName: string; pagePath?: string; refType?: string; refId?: bigint; sessionId?: string; sourceType?: string; sourceRefId?: bigint; extraJson?: Record; }; export type TrackStoreEventInput = TrackEventInput & { storeAccountId?: bigint; storeId?: bigint; }; export type TrackPartnerEventInput = TrackEventInput & { partnerAccountId: bigint; }; type RawEvent = { eventName: string; params?: Record }; @Injectable() export class AnalyticsService { constructor(private readonly prisma: PrismaService) {} async trackBatchOptional( userId: bigint | null | undefined, clientApp: string, events: RawEvent[], sessionId?: string, ) { if (!events?.length) return { count: 0 }; await this.prisma.logUserAnalytics.createMany({ 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.toUserRow(userId, clientApp, event), }); } trackOneSafe(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) { void this.trackOne(userId, clientApp, event).catch(() => {}); } 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), }); } trackStoreOneSafe(storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput) { if (event.storeId == null) return; void this.trackStoreOne(storeAccountId, clientApp, event).catch(() => {}); } async trackPartnerOne( actorAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackPartnerEventInput, ) { await this.prisma.logPartnerAnalytics.create({ data: this.toPartnerRow(actorAccountId, clientApp, event), }); } trackPartnerOneSafe( actorAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackPartnerEventInput, ) { void this.trackPartnerOne(actorAccountId, clientApp, event).catch(() => {}); } private parseParams( params?: Record, fallbackSessionId?: string, ): Omit { 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 { 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, }; } private toStoreRow( storeAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackStoreEventInput, ) { return { storeAccountId, storeId: event.storeId!, eventName: event.eventName, clientApp: clientApp as ClientApp, refType: event.refType, refId: event.refId, extraJson: event.extraJson as never, }; } private toPartnerRow( actorAccountId: bigint | undefined, clientApp: ClientApp | string, event: TrackPartnerEventInput, ) { return { partnerAccountId: event.partnerAccountId, eventName: event.eventName, clientApp: clientApp as ClientApp, refType: event.refType, refId: event.refId ?? actorAccountId, extraJson: event.extraJson as never, }; } }