|
|
@@ -34,8 +34,6 @@ interface AdClickMessage extends BaseStatsMessage {
|
|
|
machine?: string;
|
|
|
}
|
|
|
|
|
|
-const AD_META_KEY_PREFIX = 'box:app:ad:meta:';
|
|
|
-
|
|
|
@Injectable()
|
|
|
export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
|
|
|
private readonly logger = new Logger(StatsEventsConsumer.name);
|
|
|
@@ -332,11 +330,11 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- const resolvedAdType = await this.resolveAdTypeForAdsId(adsId);
|
|
|
+ const resolvedMeta = await this.resolveAdMetadata(adsId);
|
|
|
|
|
|
- if (!resolvedAdType) {
|
|
|
+ if (!resolvedMeta) {
|
|
|
this.logger.error(
|
|
|
- `Invalid ad metadata for adsId=${adsId}, uid=${payload.uid}, channelId=${payload.channelId}, messageId=${messageId}; dropping event per Task 2 – stats-consumer-enrichment-flow.md`,
|
|
|
+ `Invalid ad metadata for adsId=${adsId}, uid=${payload.uid}, channelId=${payload.channelId}, messageId=${messageId}; dropping event because Redis hash box:stats:ads is authoritative`,
|
|
|
);
|
|
|
this.counters.malformed++;
|
|
|
this.ack(msg);
|
|
|
@@ -357,8 +355,8 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
|
|
|
data: {
|
|
|
uid: payload.uid,
|
|
|
adsId: adsId,
|
|
|
- adId: adId,
|
|
|
- adType: resolvedAdType,
|
|
|
+ adId: resolvedMeta.adId ?? adId,
|
|
|
+ adType: resolvedMeta.adType,
|
|
|
clickedAt: clickTime,
|
|
|
ip: payload.ip,
|
|
|
channelId: payload.channelId,
|
|
|
@@ -381,46 +379,48 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async resolveAdTypeForAdsId(adsId: string): Promise<string | null> {
|
|
|
- // Task 2 – stats-consumer-enrichment-flow.md: Redis read-only, Mongo fallback, hostile adsId drop path.
|
|
|
- const cacheKey = `${AD_META_KEY_PREFIX}${adsId}`;
|
|
|
- let cachedValue: string | null = null;
|
|
|
+ private async resolveAdMetadata(
|
|
|
+ adsId: string,
|
|
|
+ ): Promise<{ adType: string; adId?: number } | null> {
|
|
|
+ const hashKey = 'box:stats:ads';
|
|
|
+ // Redis hash box:stats:ads is the single source of truth now; the Mongo fallback was removed per new architecture.
|
|
|
+ const ensureClient = (this.redis as any).ensureClient as
|
|
|
+ | (() => unknown)
|
|
|
+ | undefined;
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
|
+ const redisClient =
|
|
|
+ ensureClient?.call(this.redis) ?? (this.redis as any).client ?? null;
|
|
|
+
|
|
|
+ if (!redisClient) {
|
|
|
+ this.logger.error(
|
|
|
+ 'Redis client unavailable while resolving ads metadata',
|
|
|
+ );
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
try {
|
|
|
- cachedValue = await this.redis.get(cacheKey);
|
|
|
+ const rawValue = await redisClient.hget(hashKey, adsId);
|
|
|
+ if (!rawValue) return null;
|
|
|
+
|
|
|
+ const parsed = JSON.parse(rawValue);
|
|
|
+ if (
|
|
|
+ parsed &&
|
|
|
+ typeof parsed === 'object' &&
|
|
|
+ typeof parsed.adType === 'string'
|
|
|
+ ) {
|
|
|
+ return {
|
|
|
+ adType: parsed.adType,
|
|
|
+ adId: typeof parsed.adId === 'number' ? parsed.adId : undefined,
|
|
|
+ };
|
|
|
+ }
|
|
|
} catch (err) {
|
|
|
this.logger.debug(
|
|
|
- `Redis lookup failed for key=${cacheKey}, falling back to Mongo`,
|
|
|
+ `Redis lookup/parsing failed for ${hashKey} field ${adsId}`,
|
|
|
err instanceof Error ? err.stack : String(err),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- if (cachedValue) {
|
|
|
- const trimmed = cachedValue.trim();
|
|
|
- if (trimmed) {
|
|
|
- if (trimmed.startsWith('{')) {
|
|
|
- try {
|
|
|
- const parsed = JSON.parse(trimmed);
|
|
|
- if (parsed && typeof parsed === 'object' && 'adType' in parsed) {
|
|
|
- return String(parsed.adType);
|
|
|
- }
|
|
|
- } catch {
|
|
|
- // ignore parse failures and treat value as raw string below
|
|
|
- }
|
|
|
- }
|
|
|
- if (!trimmed.startsWith('{')) {
|
|
|
- return trimmed;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const client = this.prisma as any;
|
|
|
- const adRecord = await client.ads.findUnique({
|
|
|
- where: { id: adsId },
|
|
|
- select: { adType: true },
|
|
|
- });
|
|
|
-
|
|
|
- return adRecord?.adType ?? null;
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
async onModuleDestroy(): Promise<void> {
|