From 934e69ab32479e96f7b411c171096430b5999b60 Mon Sep 17 00:00:00 2001 From: Neola06 Date: Wed, 21 Oct 2026 22:50:48 +0100 Subject: [PATCH] Add rate limiter enhancements, feature flag upgrades, Shopify integration, and tracing support --- .gitignore | 5 + backend/src/config/tracing.ts | 0 backend/src/integrations/shopify/index.ts | Bin 0 -> 222 bytes backend/src/middleware/otel.ts | 0 backend/src/middleware/rate-limit.ts | 53 ++++++++++ backend/src/services/featureFlags.ts | 119 +++++++++++++++------- backend/src/tracing.ts | 0 7 files changed, 143 insertions(+), 34 deletions(-) create mode 100644 backend/src/config/tracing.ts create mode 100644 backend/src/integrations/shopify/index.ts create mode 100644 backend/src/middleware/otel.ts create mode 100644 backend/src/tracing.ts diff --git a/.gitignore b/.gitignore index f440ba03..b6e4df38 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,8 @@ npm-debug.log* __snapshots__/ **/__snapshots__/ + +# Test snapshots +*.snap +test/__snapshots__/ +**/*.snap diff --git a/backend/src/config/tracing.ts b/backend/src/config/tracing.ts new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/integrations/shopify/index.ts b/backend/src/integrations/shopify/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..db257965d178552c1834a1f8caf9dd43d2d62f84 GIT binary patch literal 222 zcma)$%?dzJ6okLE@(y;l(); + +export function setQuotaOverride(clientKey: string, override: QuotaOverride): void { + const entry: StoredQuotaOverride = { + clientKey, + tier: override.tier, + capacity: override.capacity, + refillRate: override.refillRate, + burstAllowance: override.burstAllowance, + expiresAt: override.expiresAt instanceof Date ? override.expiresAt.toISOString() : override.expiresAt, + }; + quotaOverrides.set(clientKey, entry); +} + +export function removeQuotaOverride(clientKey: string): boolean { + return quotaOverrides.delete(clientKey); +} + +export function listQuotaOverrides(): StoredQuotaOverride[] { + const now = Date.now(); + const result: StoredQuotaOverride[] = []; + for (const [key, value] of Array.from(quotaOverrides.entries())) { + if (value.expiresAt && new Date(value.expiresAt).getTime() < now) { + quotaOverrides.delete(key); + continue; + } + result.push(value); + } + return result; +} + +export function getQuotaOverride(clientKey: string): StoredQuotaOverride | undefined { + const override = quotaOverrides.get(clientKey); + if (override?.expiresAt && new Date(override.expiresAt).getTime() < Date.now()) { + quotaOverrides.delete(clientKey); + return undefined; + } + return override; +} diff --git a/backend/src/services/featureFlags.ts b/backend/src/services/featureFlags.ts index accfb08b..fc0d573b 100644 --- a/backend/src/services/featureFlags.ts +++ b/backend/src/services/featureFlags.ts @@ -1,37 +1,77 @@ -import { createHash } from 'node:crypto'; +import { createHash } from "node:crypto"; export interface FeatureFlagMetrics { -servedTrue: number; -servedFalse: number; + servedTrue: number; + servedFalse: number; +} + +export interface FeatureFlagVariant { + key: string; + value: any; + weight: number; +} + +export interface RolloutSchedule { + startPercentage: number; + endPercentage: number; + startTime: Date; + endTime: Date; } export interface FeatureFlag { -name: string; -enabled: boolean; -rolloutPercentage: number; -targetedUsers: Set; -metrics: FeatureFlagMetrics; // <-- Added metrics tracking + name: string; + enabled: boolean; + rolloutPercentage: number; + targetedUsers: Set; + metrics: FeatureFlagMetrics; + variants?: FeatureFlagVariant[]; + rolloutSchedule?: RolloutSchedule; } class FeatureFlagService { -private flags: Map = new Map(); + private flags: Map = new Map(); -constructor() { - // Initialize with some default flags for testing - this.upsertFlag('new-checkout-flow', true, 20); // 20% A/B test - this.upsertFlag('beta-dashboard', true, 0, ['dev-user-1', 'qa-tester']); // Targeted rollout + constructor() { + this.upsertFlag("new-checkout-flow", true, 20); + this.upsertFlag("beta-dashboard", true, 0, ["dev-user-1", "qa-tester"]); } - public upsertFlag(name: string, enabled: boolean, rolloutPercentage: number = 0, targetedUsers: string[] = []): void { + public upsertFlag( + name: string, + enabled: boolean, + rolloutPercentage: number = 0, + targetedUsers: string[] = [], + variants: FeatureFlagVariant[] = [], + rolloutSchedule?: RolloutSchedule + ): void { const existing = this.flags.get(name); + let effectiveRollout = Math.max(0, Math.min(100, rolloutPercentage)); + + if (rolloutSchedule) { + const now = Date.now(); + const start = rolloutSchedule.startTime.getTime(); + const end = rolloutSchedule.endTime.getTime(); + if (now <= start) { + effectiveRollout = rolloutSchedule.startPercentage; + } else if (now >= end) { + effectiveRollout = rolloutSchedule.endPercentage; + } else { + const progress = (now - start) / (end - start); + effectiveRollout = Math.round( + rolloutSchedule.startPercentage + progress * (rolloutSchedule.endPercentage - rolloutSchedule.startPercentage) + ); + } + } + this.flags.set(name, { name, enabled, - rolloutPercentage: Math.max(0, Math.min(100, rolloutPercentage)), + rolloutPercentage: effectiveRollout, targetedUsers: new Set(targetedUsers), - // Preserve existing metrics on update, or initialize to 0 - metrics: existing?.metrics || { servedTrue: 0, servedFalse: 0 } + metrics: existing?.metrics || { servedTrue: 0, servedFalse: 0 }, + variants: variants.length > 0 ? variants : existing?.variants, + rolloutSchedule: rolloutSchedule || existing?.rolloutSchedule, }); } @@ -47,50 +87,61 @@ constructor() { this.flags.delete(name); } - /** - * Deterministic flag evaluation with analytics tracking. - * Resolves in < 1ms to prevent API latency. - */ - public evaluate(flagName: string, identifier: string): boolean { + public evaluate(flagName: string, identifier: string = ""): boolean { const flag = this.flags.get(flagName); - // Helper to track metrics before returning const trackAndReturn = (result: boolean) => { if (flag) { - result ? flag.metrics.servedTrue++ : flag.metrics.servedFalse++; + result ? (flag.metrics.servedTrue++) : (flag.metrics.servedFalse++); } return result; }; - // 1. If flag doesn't exist or is globally disabled, return false (Instant Rollback) if (!flag || !flag.enabled) { return trackAndReturn(false); } - // 2. If user is explicitly targeted, return true (User Targeting) - if (flag.targetedUsers.has(identifier)) { + if (identifier && flag.targetedUsers.has(identifier)) { return trackAndReturn(true); } - // 3. If rollout is 100%, return true if (flag.rolloutPercentage === 100) { return trackAndReturn(true); } - // 4. If rollout is 0%, return false if (flag.rolloutPercentage === 0) { return trackAndReturn(false); } - // 5. Deterministic Percentage Rollout (A/B Testing) - // Hash the flag name + identifier so the same user always gets the same experience - const hash = createHash('md5').update(`${flagName}-${identifier}`).digest('hex'); - // Take the first 4 characters, convert to integer, and map to a 1-100 range + const hash = createHash("md5").update(`${flagName}-${identifier}`).digest("hex"); const hashInt = parseInt(hash.substring(0, 4), 16); const normalizedHash = (hashInt % 100) + 1; return trackAndReturn(normalizedHash <= flag.rolloutPercentage); } + + public evaluateVariant(flagName: string, identifier: string): string | null { + const flag = this.flags.get(flagName); + + if (!flag || !flag.enabled || !flag.variants || flag.variants.length === 0) { + return null; + } + + const totalWeight = flag.variants.reduce((sum, v) => sum + v.weight, 0); + const hash = createHash("md5").update(`${flagName}-variant-${identifier}`).digest("hex"); + const hashInt = parseInt(hash.substring(0, 8), 16); + const bucket = hashInt % totalWeight; + + let cumulative = 0; + for (const variant of flag.variants) { + cumulative += variant.weight; + if (bucket < cumulative) { + return variant.key; + } + } + + return flag.variants[0]?.key || null; + } } -export const featureFlagEngine = new FeatureFlagService(); \ No newline at end of file +export const featureFlagEngine = new FeatureFlagService(); diff --git a/backend/src/tracing.ts b/backend/src/tracing.ts new file mode 100644 index 00000000..e69de29b