perf(decode): slim decode response to id + display summary

POST /vehicles/decode returned the full vehicles row while every consumer
(search, landing, service-test) only navigates by id and re-fetches via
GET /vehicles/:id. That shipped the raw upstream decode payload to the
client on every decode — rawData is 19KB avg / 127KB p95 / 207KB max in
prod — plus the provider name, for nothing.

The response is now { id, brandName, model, year } on both the DB-hit and
fresh-decode paths. Frontend drops the (now absent) source property from
the vin_decode_success event; decode source remains queryable server-side
via query_logs.source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 02:52:43 +03:00
parent 2077a9724a
commit 02c8321023
3 changed files with 27 additions and 8 deletions

View File

@@ -166,7 +166,11 @@ describe("VehiclesService", () => {
const { service } = createService(db);
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(cached);
// Decode response is intentionally slim — id + display summary only;
// rawData/source never leave the API on this endpoint.
expect(result).toMatchObject({ id: "v1" });
expect(result).not.toHaveProperty("rawData");
expect(result).not.toHaveProperty("source");
});
it("should throw BadRequestException when corgi doesn't recognize VIN", async () => {
@@ -435,7 +439,8 @@ describe("VehiclesService", () => {
});
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(savedVehicle);
expect(result).toMatchObject({ id: "v-emex" });
expect(result).not.toHaveProperty("source");
expect(partsCatalogsService.decodeVin).toHaveBeenCalled();
expect(emexService.decodeVinOrCandidates).toHaveBeenCalledWith("WBAPH5C55BA123456");
});
@@ -473,7 +478,7 @@ describe("VehiclesService", () => {
emexService.decodeVinOrCandidates.mockResolvedValue({ type: "notFound" });
const result = await service.decodeVin("WVWZZZ1JZ3W597935", "u1");
expect(result).toEqual(savedVehicle);
expect(result).toMatchObject({ id: "v-pcat" });
expect(pl24Service.decodeVin).not.toHaveBeenCalled();
});
@@ -488,7 +493,7 @@ describe("VehiclesService", () => {
});
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(savedVehicle);
expect(result).toMatchObject({ id: "v-emex" });
expect(emexService.decodeVinOrCandidates).toHaveBeenCalled();
});
@@ -511,7 +516,7 @@ describe("VehiclesService", () => {
});
const result = await service.decodeVin("VR3EFYHZ3PJ674071", "u1");
expect(result).toEqual(savedVehicle);
expect(result).toMatchObject({ id: "v-pl24" });
expect(pl24Service.decodeVin).toHaveBeenCalledWith("VR3EFYHZ3PJ674071", "u1");
});

View File

@@ -154,7 +154,7 @@ export class VehiclesService {
undefined,
ctx.timings,
);
return existing;
return this.toDecodeResponse(existing);
}
// 2. Resolve VIN via cached decode chain (Corgi → PartsCatalogs → PL24 → EMEX)
@@ -308,7 +308,22 @@ export class VehiclesService {
ctx.timings,
);
return savedVehicle;
return this.toDecodeResponse(savedVehicle);
}
/**
* Minimal decode response. Every consumer navigates by id and re-fetches the
* vehicle via GET /vehicles/:id; returning the full row shipped the raw
* upstream decode payload (rawData, p95 ~127KB) and the provider name to the
* client on every decode for nothing.
*/
private toDecodeResponse(v: {
id: string;
brandName: string | null;
model: string | null;
year: number | null;
}) {
return { id: v.id, brandName: v.brandName, model: v.model, year: v.year };
}
/**