feat(flags): server-side feature flags + upstream kill switches + live backfill config
Add server-side PostHog feature-flag evaluation to the API and wire three high-leverage uses. The flags live in PostHog (project 127747), dormant and fail-open, so this commit is a no-op until POSTHOG_PERSONAL_API_KEY is set and a switch is flipped. Phase 1 - upstream kill switches: PostHogService gains local flag evaluation (onlyEvaluateLocally + sendFeatureFlagEvents:false -> zero decode latency, no VIN leak) and isSourceLive(). Guards at each source's decode entry (parts-catalogs, emex, pl24 whole-source + per-brand via LEGACY_ARCH_SOURCE_TAG) let a flailing upstream be disabled from the PostHog UI in ~5s instead of a code-fix -> dev -> prod redeploy. Fail-open: any unresolved flag keeps the source live, so a PostHog outage can never black out decoding. Phase 2 - guarded rollout primitive: isEnabled()/variant() with VIN bucketing, ready to ramp a new decode/parser path 0->100% behind a decode-*-v2 flag (recipe in feature-flags-strategy.md). Phase 4 - remote-config ops tuning: prefetch-worker reads cfg-backfill-tuning to retune backfill batchSize/maxBacklog/businessHoursOnly live; malformed/missing -> the compiled-in constants. POSTHOG_PERSONAL_API_KEY wired into the api + worker compose blocks (empty -> flags inert, no added latency). Tests updated for the new constructor params. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ const svc = new PL24Service(
|
||||
configStub, // configService
|
||||
{} as never, // redis
|
||||
{} as never, // storage
|
||||
{} as never, // posthog
|
||||
);
|
||||
const p = svc as unknown as {
|
||||
parseVehicleResponse(
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { extractModelYear } from "@sase/shared";
|
||||
import { isBackfillContext } from "../../jobs/prefetch-context";
|
||||
import { PostHogService } from "../../posthog/posthog.service";
|
||||
import { RedisService } from "../../redis/redis.service";
|
||||
import { StorageService } from "../../storage/storage.service";
|
||||
import { PL24AuthService } from "./pl24-auth.service";
|
||||
@@ -41,6 +42,19 @@ import {
|
||||
isP5Modern,
|
||||
} from "./pl24.types";
|
||||
|
||||
/**
|
||||
* Legacy (P4) architecture → kill-switch source tag. Lets one fragile brand be
|
||||
* disabled (`kill-source-psa`, `kill-source-volvo`, …) without taking all of PL24
|
||||
* down. Unmapped legacy brands fall back to the shared `pl24-legacy` switch.
|
||||
*/
|
||||
const LEGACY_ARCH_SOURCE_TAG: Record<string, string> = {
|
||||
LEGACY_PSA: "psa",
|
||||
LEGACY_VOLVO: "volvo",
|
||||
LEGACY_FORD: "ford",
|
||||
LEGACY_OPEL: "opel",
|
||||
LEGACY_HYUNDAI_KIA: "hyundai-kia",
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PL24Service {
|
||||
private readonly logger = new Logger(PL24Service.name);
|
||||
@@ -59,6 +73,7 @@ export class PL24Service {
|
||||
private configService: ConfigService,
|
||||
private redis: RedisService,
|
||||
private storage: StorageService,
|
||||
private posthog: PostHogService,
|
||||
) {
|
||||
this.baseUrl = this.configService.get<string>("pl24.baseUrl", "https://www.partslink24.com");
|
||||
this.timeout = 30000;
|
||||
@@ -70,6 +85,10 @@ export class PL24Service {
|
||||
* Only P5 Modern architecture is supported here; legacy is dispatched to fordLegacyService.
|
||||
*/
|
||||
async decodeVin(vin: string, userId?: string): Promise<PL24DecodedVehicle | null> {
|
||||
if (!(await this.posthog.isSourceLive("pl24"))) {
|
||||
this.logger.warn("PL24 disabled by kill switch (kill-source-pl24)");
|
||||
return null;
|
||||
}
|
||||
const cleanVin = vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, "");
|
||||
this.validateVin(cleanVin);
|
||||
|
||||
@@ -88,6 +107,15 @@ export class PL24Service {
|
||||
|
||||
// Dispatch P4 legacy architectures to the generic legacy service
|
||||
if (!isP5Modern(serviceName)) {
|
||||
// Per-brand kill switch: disable one fragile legacy brand without taking all
|
||||
// of PL24 down. PSA/Volvo/Ford/etc. break independently upstream (cf. the
|
||||
// PSA illustration-dispatch outage that broke 41/43 vehicles for months).
|
||||
const legacyArch = getServiceConfig(serviceName)?.architecture ?? "";
|
||||
const brandTag = LEGACY_ARCH_SOURCE_TAG[legacyArch] ?? "pl24-legacy";
|
||||
if (!(await this.posthog.isSourceLive(brandTag))) {
|
||||
this.logger.warn(`PL24 ${brandTag} disabled by kill switch (kill-source-${brandTag})`);
|
||||
return null;
|
||||
}
|
||||
// PSA (Peugeot/Citroën/DS) uses a dedicated FI/VIN-indexed decode service.
|
||||
if (getServiceConfig(serviceName)?.architecture === "LEGACY_PSA") {
|
||||
return this.psaService.decodeVinForService(cleanVin, serviceName, userId);
|
||||
|
||||
Reference in New Issue
Block a user