|
|
@@ -23,15 +23,6 @@ export class StatsAggregationService {
|
|
|
|
|
|
constructor(private readonly prisma: PrismaMongoService) {}
|
|
|
|
|
|
- /**
|
|
|
- * Aggregate ads click statistics from raw events.
|
|
|
- * Click-only stats collection: writes AdsGlobalStats(clicks, firstSeenAt, lastSeenAt, createAt, updateAt).
|
|
|
- *
|
|
|
- * Notes:
|
|
|
- * - No scoring / recency / popularity.
|
|
|
- * - No Redis sync.
|
|
|
- * - windowDays is ignored (no cutoff filtering), per new requirements.
|
|
|
- */
|
|
|
async aggregateAdsStats(
|
|
|
_options: AggregationOptions = {},
|
|
|
): Promise<AggregationResult> {
|
|
|
@@ -40,41 +31,49 @@ export class StatsAggregationService {
|
|
|
|
|
|
this.logger.log(`[${runId}] Starting ads clicks aggregation (all time)`);
|
|
|
|
|
|
- let identities: AdIdentity[] = [];
|
|
|
+ let adsIds: string[] = [];
|
|
|
try {
|
|
|
- identities = await this.getUniqueAdIdentities();
|
|
|
+ adsIds = await this.getUniqueAdsIds();
|
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
|
- `[${runId}] Failed to load unique ad identities`,
|
|
|
+ `[${runId}] Failed to load unique adsIds for aggregation`,
|
|
|
err instanceof Error ? err.stack : String(err),
|
|
|
);
|
|
|
return { totalProcessed: 0, successCount: 0, errorCount: 1 };
|
|
|
}
|
|
|
|
|
|
- this.logger.log(
|
|
|
- `[${runId}] Found ${identities.length} unique ads to aggregate`,
|
|
|
- );
|
|
|
+ const total = adsIds.length;
|
|
|
+
|
|
|
+ if (total === 0) {
|
|
|
+ const durationMs = Date.now() - startedAtMs;
|
|
|
+ this.logger.log(
|
|
|
+ `[${runId}] ✅ No ads to aggregate (0). Done in ${durationMs}ms`,
|
|
|
+ );
|
|
|
+ return { totalProcessed: 0, successCount: 0, errorCount: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ this.logger.log(`[${runId}] Found ${total} unique ads to aggregate`);
|
|
|
|
|
|
let successCount = 0;
|
|
|
let errorCount = 0;
|
|
|
|
|
|
- for (let i = 0; i < identities.length; i++) {
|
|
|
- const idn = identities[i];
|
|
|
+ for (let i = 0; i < total; i++) {
|
|
|
+ const adsId = adsIds[i];
|
|
|
|
|
|
try {
|
|
|
- await this.aggregateSingleAdClicks(idn, runId);
|
|
|
+ await this.aggregateSingleAdClicks(adsId, runId);
|
|
|
successCount++;
|
|
|
} catch (err) {
|
|
|
errorCount++;
|
|
|
this.logger.error(
|
|
|
- `[${runId}] Failed to aggregate ${this.identityLog(idn)}`,
|
|
|
+ `[${runId}] Failed to aggregate adsId=${adsId}`,
|
|
|
err instanceof Error ? err.stack : String(err),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- if ((i + 1) % 500 === 0) {
|
|
|
+ if ((i + 1) % 500 === 0 || i + 1 === total) {
|
|
|
this.logger.log(
|
|
|
- `[${runId}] Progress ${i + 1}/${identities.length} (ok=${successCount}, err=${errorCount})`,
|
|
|
+ `[${runId}] Progress ${i + 1}/${total} (ok=${successCount}, err=${errorCount})`,
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
@@ -86,12 +85,26 @@ export class StatsAggregationService {
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
- totalProcessed: identities.length,
|
|
|
+ totalProcessed: total,
|
|
|
successCount,
|
|
|
errorCount,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ private async getUniqueAdsIds(): Promise<string[]> {
|
|
|
+ const client = this.prisma as any;
|
|
|
+
|
|
|
+ const rows = await client.adClickEvents.findMany({
|
|
|
+ where: { adsId: { not: null } },
|
|
|
+ select: { adsId: true },
|
|
|
+ distinct: ['adsId'],
|
|
|
+ });
|
|
|
+
|
|
|
+ return rows
|
|
|
+ .map((r: any) => r?.adsId as string | undefined)
|
|
|
+ .filter((v: string | undefined): v is string => Boolean(v));
|
|
|
+ }
|
|
|
+
|
|
|
private async getUniqueAdIdentities(): Promise<AdIdentity[]> {
|
|
|
const client = this.prisma as any;
|
|
|
|
|
|
@@ -132,63 +145,47 @@ export class StatsAggregationService {
|
|
|
}
|
|
|
|
|
|
private async aggregateSingleAdClicks(
|
|
|
- idn: AdIdentity,
|
|
|
+ adsId: string,
|
|
|
runId: string,
|
|
|
): Promise<void> {
|
|
|
const client = this.prisma as any;
|
|
|
-
|
|
|
- const whereId =
|
|
|
- idn.kind === 'adsId' ? { adsId: idn.adsId } : { adId: idn.adId };
|
|
|
-
|
|
|
- const clicks = await client.adClickEvents.count({
|
|
|
- where: whereId,
|
|
|
- });
|
|
|
-
|
|
|
- if (clicks <= 0) {
|
|
|
- // No clicks: nothing to write/update
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Find first/last click time (epoch ms BigInt)
|
|
|
- const firstRow = await client.adClickEvents.findFirst({
|
|
|
- where: whereId,
|
|
|
- select: { clickedAt: true },
|
|
|
- orderBy: { clickedAt: 'asc' },
|
|
|
- });
|
|
|
-
|
|
|
- const lastRow = await client.adClickEvents.findFirst({
|
|
|
- where: whereId,
|
|
|
- select: { clickedAt: true },
|
|
|
- orderBy: { clickedAt: 'desc' },
|
|
|
- });
|
|
|
+ const whereId = { adsId };
|
|
|
+
|
|
|
+ const clicks: number = await client.adClickEvents.count({ where: whereId });
|
|
|
+ if (clicks <= 0) return;
|
|
|
+
|
|
|
+ // Fetch first + last click time (epoch ms BigInt)
|
|
|
+ const [firstRow, lastRow] = await Promise.all([
|
|
|
+ client.adClickEvents.findFirst({
|
|
|
+ where: whereId,
|
|
|
+ select: { clickedAt: true },
|
|
|
+ orderBy: { clickedAt: 'asc' },
|
|
|
+ }),
|
|
|
+ client.adClickEvents.findFirst({
|
|
|
+ where: whereId,
|
|
|
+ select: { clickedAt: true },
|
|
|
+ orderBy: { clickedAt: 'desc' },
|
|
|
+ }),
|
|
|
+ ]);
|
|
|
|
|
|
const firstSeenAt = firstRow?.clickedAt as bigint | undefined;
|
|
|
const lastSeenAt = lastRow?.clickedAt as bigint | undefined;
|
|
|
-
|
|
|
- if (!firstSeenAt || !lastSeenAt) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (!firstSeenAt || !lastSeenAt) return;
|
|
|
|
|
|
const now = nowEpochMsBigInt();
|
|
|
-
|
|
|
- // Upsert AdsGlobalStats by identity.
|
|
|
- // IMPORTANT: AdsGlobalStats.adsId and AdsGlobalStats.adId should be @unique to allow upsert.
|
|
|
- const whereGlobal =
|
|
|
- idn.kind === 'adsId' ? { adsId: idn.adsId } : { adId: idn.adId };
|
|
|
+ const clicksBig = BigInt(clicks);
|
|
|
|
|
|
await client.adsGlobalStats.upsert({
|
|
|
- where: whereGlobal,
|
|
|
+ where: { adsId },
|
|
|
update: {
|
|
|
- clicks: BigInt(clicks),
|
|
|
+ clicks: clicksBig,
|
|
|
lastSeenAt,
|
|
|
updateAt: now,
|
|
|
- // impressions kept but not used in click-only mode
|
|
|
},
|
|
|
create: {
|
|
|
- adsId: idn.kind === 'adsId' ? idn.adsId : undefined,
|
|
|
- adId: idn.kind === 'adId' ? idn.adId : undefined,
|
|
|
- impressions: BigInt(0),
|
|
|
- clicks: BigInt(clicks),
|
|
|
+ adsId,
|
|
|
+ impressions: 0n,
|
|
|
+ clicks: clicksBig,
|
|
|
firstSeenAt,
|
|
|
lastSeenAt,
|
|
|
createAt: now,
|
|
|
@@ -197,7 +194,7 @@ export class StatsAggregationService {
|
|
|
});
|
|
|
|
|
|
this.logger.debug(
|
|
|
- `[${runId}] ${this.identityLog(idn)} clicks=${clicks} first=${firstSeenAt.toString()} last=${lastSeenAt.toString()}`,
|
|
|
+ `[${runId}] adsId=${adsId} clicks=${clicks} first=${firstSeenAt.toString()} last=${lastSeenAt.toString()}`,
|
|
|
);
|
|
|
}
|
|
|
|