Merge pull request 'Revert PR #32 — VIN decode PostHog enrichment was duplicating query_logs' (#33) from dev into main
This commit was merged in pull request #33.
This commit is contained in:
@@ -87,10 +87,6 @@ function createService(dbOrOverrides: any = {}) {
|
||||
add: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const posthogService = {
|
||||
captureForUser: vi.fn(),
|
||||
};
|
||||
|
||||
const service = new VehiclesService(
|
||||
db as any,
|
||||
prefetchQueue as any,
|
||||
@@ -100,7 +96,6 @@ function createService(dbOrOverrides: any = {}) {
|
||||
emexService as any,
|
||||
partsCatalogsService as any,
|
||||
redisService as any,
|
||||
posthogService as any,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -29,7 +29,6 @@ import { PL24Service } from "../integrations/pl24/pl24.service";
|
||||
import { VinApiService } from "../integrations/vin-api/vin-api.service";
|
||||
import { PrefetchSource } from "../jobs/prefetch.types";
|
||||
import { CATALOG_PREFETCH_QUEUE } from "../jobs/queues/catalog-prefetch.queue";
|
||||
import { PostHogService } from "../posthog/posthog.service";
|
||||
import { RedisService } from "../redis/redis.service";
|
||||
|
||||
/**
|
||||
@@ -74,121 +73,8 @@ export class VehiclesService {
|
||||
private emexService: EmexService,
|
||||
private partsCatalogsService: PartsCatalogsService,
|
||||
private redis: RedisService,
|
||||
private posthog: PostHogService,
|
||||
) {}
|
||||
|
||||
// ─── Süper Panel observability telemetry ──────────────────
|
||||
// VIN Decode Observability Module (sp-vin-001) expects enriched events
|
||||
// emitted from the backend where the provider chain + cache decisions are
|
||||
// visible. Frontend already captures `vin_decoded` / `vin_decode_success`
|
||||
// (legacy names, see search.tsx); the events below are richer companions
|
||||
// with backend-only fields the dashboard depends on.
|
||||
private static readonly PROVIDER_TIMING_KEYS: ReadonlyArray<{
|
||||
provider: string;
|
||||
timingKey: string;
|
||||
}> = [
|
||||
{ provider: "parts-catalogs", timingKey: "pcat" },
|
||||
{ provider: "emex", timingKey: "emex" },
|
||||
{ provider: "pl24", timingKey: "pl24" },
|
||||
{ provider: "vin-api", timingKey: "vin_api" },
|
||||
];
|
||||
|
||||
private sanitizeVin(vin: string): string {
|
||||
const upper = vin.toUpperCase();
|
||||
return upper.length >= 11 ? `${upper.slice(0, 11)}******` : `${upper.slice(0, 3)}***`;
|
||||
}
|
||||
|
||||
private emitDecodeTelemetry(
|
||||
userId: string,
|
||||
vin: string,
|
||||
ctx: ResolveContext,
|
||||
final:
|
||||
| {
|
||||
kind: "success";
|
||||
vehicleId: string;
|
||||
brandName: string | null;
|
||||
source: string;
|
||||
durationMs: number;
|
||||
partial: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "failure";
|
||||
errorCode: string;
|
||||
errorMessage: string;
|
||||
durationMs: number;
|
||||
},
|
||||
): void {
|
||||
try {
|
||||
const vinSanitized = this.sanitizeVin(vin);
|
||||
const brand = final.kind === "success" ? final.brandName : null;
|
||||
const cacheSource = ctx.timings.cache_source;
|
||||
const cacheHit =
|
||||
cacheSource === "db_hit" || cacheSource === "redis_positive";
|
||||
|
||||
// 1. Per-provider response events from chain timings.
|
||||
const attempts: string[] = [];
|
||||
for (const { provider, timingKey } of VehiclesService.PROVIDER_TIMING_KEYS) {
|
||||
const ms = ctx.timings[timingKey];
|
||||
if (typeof ms !== "number") continue;
|
||||
attempts.push(provider);
|
||||
const wonForThisDecode =
|
||||
final.kind === "success" && final.source === provider;
|
||||
this.posthog.captureForUser(userId, "provider_response_received", {
|
||||
provider,
|
||||
response_time_ms: ms,
|
||||
status: wonForThisDecode ? "success" : "no_data",
|
||||
cached: false,
|
||||
vin_brand: brand,
|
||||
vin_sanitized: vinSanitized,
|
||||
attempt_in_chain: attempts.length,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Fallback events between consecutive provider attempts.
|
||||
for (let i = 1; i < attempts.length; i++) {
|
||||
this.posthog.captureForUser(userId, "provider_fallback_triggered", {
|
||||
from_provider: attempts[i - 1],
|
||||
to_provider: attempts[i],
|
||||
reason: ctx.timings.aborted ? "timeout" : "no_data",
|
||||
auto: true,
|
||||
attempt_number: i + 1,
|
||||
vin_sanitized: vinSanitized,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Terminal vin_decode_{succeeded,failed} with full backend context.
|
||||
if (final.kind === "success") {
|
||||
this.posthog.captureForUser(userId, "vin_decode_succeeded", {
|
||||
vehicle_id: final.vehicleId,
|
||||
vin_brand: brand,
|
||||
provider: final.source,
|
||||
cache_hit: cacheHit,
|
||||
cache_source: cacheSource ?? null,
|
||||
response_time_ms: final.durationMs,
|
||||
partial_result: final.partial,
|
||||
fallback_used: attempts.length > 1,
|
||||
provider_attempts: attempts,
|
||||
ml_decoder_used: false,
|
||||
vin_sanitized: vinSanitized,
|
||||
pl24_circuit_open: ctx.timings.pl24_circuit_open ?? false,
|
||||
});
|
||||
} else {
|
||||
this.posthog.captureForUser(userId, "vin_decode_failed", {
|
||||
error_code: final.errorCode,
|
||||
error_message: final.errorMessage,
|
||||
provider_attempted: attempts,
|
||||
response_time_ms: final.durationMs,
|
||||
will_auto_retry: false,
|
||||
vin_sanitized: vinSanitized,
|
||||
pl24_circuit_open: ctx.timings.pl24_circuit_open ?? false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// Telemetry must never break the user-facing decode.
|
||||
this.logger.warn(`emitDecodeTelemetry failed: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async decodeVin(vin: string, userId: string, pcatCarId?: string, emexCarIndex?: number) {
|
||||
const startTime = Date.now();
|
||||
const ctx: ResolveContext = {
|
||||
@@ -213,25 +99,16 @@ export class VehiclesService {
|
||||
await this.ensureUserVehicleLink(userId, existing.id);
|
||||
ctx.timings.cache_source = "db_hit";
|
||||
ctx.timings.result_kind = "vehicle";
|
||||
const durationMs = Date.now() - startTime;
|
||||
await this.logQuery(
|
||||
userId,
|
||||
vin,
|
||||
existing.brandId,
|
||||
"cache",
|
||||
true,
|
||||
durationMs,
|
||||
Date.now() - startTime,
|
||||
undefined,
|
||||
ctx.timings,
|
||||
);
|
||||
this.emitDecodeTelemetry(userId, vin, ctx, {
|
||||
kind: "success",
|
||||
vehicleId: existing.id,
|
||||
brandName: existing.brandName,
|
||||
source: "cache",
|
||||
durationMs,
|
||||
partial: !existing.brandName,
|
||||
});
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -243,23 +120,16 @@ export class VehiclesService {
|
||||
const errMsg = ctx.timings.aborted
|
||||
? `Decode budget exceeded (${VehiclesService.RESOLVE_BUDGET_MS}ms)`
|
||||
: "Unknown VIN/brand";
|
||||
const durationMs = Date.now() - startTime;
|
||||
await this.logQuery(
|
||||
userId,
|
||||
vin,
|
||||
null,
|
||||
finalSource,
|
||||
false,
|
||||
durationMs,
|
||||
Date.now() - startTime,
|
||||
errMsg,
|
||||
ctx.timings,
|
||||
);
|
||||
this.emitDecodeTelemetry(userId, vin, ctx, {
|
||||
kind: "failure",
|
||||
errorCode: ctx.timings.aborted ? "BUDGET_EXCEEDED" : "UNKNOWN_VIN",
|
||||
errorMessage: errMsg,
|
||||
durationMs,
|
||||
});
|
||||
throw new BadRequestException("Şase numarası tanınamadı. Marka desteklenmiyor.");
|
||||
}
|
||||
|
||||
@@ -352,25 +222,16 @@ export class VehiclesService {
|
||||
await this.schedulePrefetch(savedVehicle.id, source as PrefetchSource);
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
await this.logQuery(
|
||||
userId,
|
||||
vin,
|
||||
brandId,
|
||||
source,
|
||||
true,
|
||||
durationMs,
|
||||
Date.now() - startTime,
|
||||
undefined,
|
||||
ctx.timings,
|
||||
);
|
||||
this.emitDecodeTelemetry(userId, vin, ctx, {
|
||||
kind: "success",
|
||||
vehicleId: savedVehicle.id,
|
||||
brandName: savedVehicle.brandName,
|
||||
source,
|
||||
durationMs,
|
||||
partial: !savedVehicle.brandName || !savedVehicle.model,
|
||||
});
|
||||
|
||||
return savedVehicle;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user