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:
@@ -17,6 +17,7 @@ function makeConfig(overrides: Record<string, string>): ConfigService {
|
||||
|
||||
const browser = {} as never;
|
||||
const redis = { set: vi.fn() } as never;
|
||||
const posthog = {} as never;
|
||||
|
||||
describe("EmexService proxy-port coercion (regression)", () => {
|
||||
it("does not throw 'Invalid URL' when ports arrive as strings over a real range", () => {
|
||||
@@ -27,7 +28,7 @@ describe("EmexService proxy-port coercion (regression)", () => {
|
||||
EMEX_PROXY_PORT_END: "10099",
|
||||
});
|
||||
// Pre-fix: threw "Invalid URL" here (port "4510001" > 65535).
|
||||
expect(() => new EmexService(config, browser, redis)).not.toThrow();
|
||||
expect(() => new EmexService(config, browser, redis, posthog)).not.toThrow();
|
||||
});
|
||||
|
||||
it("constructs with a single-port range (string env)", () => {
|
||||
@@ -36,7 +37,7 @@ describe("EmexService proxy-port coercion (regression)", () => {
|
||||
EMEX_PROXY_PORT_START: "823",
|
||||
EMEX_PROXY_PORT_END: "823",
|
||||
});
|
||||
expect(() => new EmexService(config, browser, redis)).not.toThrow();
|
||||
expect(() => new EmexService(config, browser, redis, posthog)).not.toThrow();
|
||||
});
|
||||
|
||||
it("falls back to a valid default when the port env is garbage", () => {
|
||||
@@ -45,11 +46,11 @@ describe("EmexService proxy-port coercion (regression)", () => {
|
||||
EMEX_PROXY_PORT_START: "not-a-number",
|
||||
EMEX_PROXY_PORT_END: "999999",
|
||||
});
|
||||
expect(() => new EmexService(config, browser, redis)).not.toThrow();
|
||||
expect(() => new EmexService(config, browser, redis, posthog)).not.toThrow();
|
||||
});
|
||||
|
||||
it("constructs cleanly with the proxy disabled", () => {
|
||||
const config = makeConfig({ EMEX_USE_PROXY: "false" });
|
||||
expect(() => new EmexService(config, browser, redis)).not.toThrow();
|
||||
expect(() => new EmexService(config, browser, redis, posthog)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { ProxyAgent } from "undici";
|
||||
import { isBackfillContext } from "../../jobs/prefetch-context";
|
||||
import { PostHogService } from "../../posthog/posthog.service";
|
||||
import { RedisService } from "../../redis/redis.service";
|
||||
import { parseUnitLeaves, parseVehicleTree } from "./emex-tree.parser";
|
||||
import { EmexBrowserService } from "./emex.browser";
|
||||
@@ -136,6 +137,7 @@ export class EmexService {
|
||||
private configService: ConfigService,
|
||||
private browserService: EmexBrowserService,
|
||||
private redis: RedisService,
|
||||
private posthog: PostHogService,
|
||||
) {
|
||||
// __dirname is apps/api/src/integrations/emex/ or dist/integrations/emex/
|
||||
// Scraper lives at <monorepo-root>/scripts/emex-vin-scraper.js
|
||||
@@ -618,6 +620,10 @@ export class EmexService {
|
||||
| { type: "notFound" }
|
||||
| { type: "error" }
|
||||
> {
|
||||
if (!(await this.posthog.isSourceLive("emex"))) {
|
||||
this.logger.warn("EMEX disabled by kill switch (kill-source-emex)");
|
||||
return { type: "notFound" };
|
||||
}
|
||||
try {
|
||||
const vinUrl = `${EMEX_BASE_URL}/Vehicles.aspx?ft=findByVIN&c=&ssd=&vin=${vin}`;
|
||||
const html = await this.fetchEmexHtml(vinUrl);
|
||||
@@ -708,6 +714,10 @@ export class EmexService {
|
||||
* Used after the user selects a vehicle from the multi-candidate modal.
|
||||
*/
|
||||
async decodeVinByIndex(vin: string, index: number): Promise<DecodedVehicle | null> {
|
||||
if (!(await this.posthog.isSourceLive("emex"))) {
|
||||
this.logger.warn("EMEX disabled by kill switch (kill-source-emex)");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const vinUrl = `${EMEX_BASE_URL}/Vehicles.aspx?ft=findByVIN&c=&ssd=&vin=${vin}`;
|
||||
const vinHtml = await this.fetchEmexHtml(vinUrl);
|
||||
|
||||
Reference in New Issue
Block a user