Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ npm-debug.log*
__snapshots__/
**/__snapshots__/


# Test snapshots
*.snap
test/__snapshots__/
**/*.snap
Empty file added backend/src/config/tracing.ts
Empty file.
Binary file added backend/src/integrations/shopify/index.ts
Binary file not shown.
Empty file added backend/src/middleware/otel.ts
Empty file.
53 changes: 53 additions & 0 deletions backend/src/middleware/rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,56 @@ export function tokenBucketRateLimit(opts: RateLimitOptions = {}) {
next();
};
}

export interface QuotaOverride {
tier?: UserTier;
capacity?: number;
refillRate?: number;
burstAllowance?: number;
expiresAt?: string | Date;
}

interface StoredQuotaOverride extends QuotaOverride {
clientKey: string;
expiresAt?: string;
}

const quotaOverrides = new Map<string, StoredQuotaOverride>();

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;
}
119 changes: 85 additions & 34 deletions backend/src/services/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
metrics: FeatureFlagMetrics; // <-- Added metrics tracking
name: string;
enabled: boolean;
rolloutPercentage: number;
targetedUsers: Set<string>;
metrics: FeatureFlagMetrics;
variants?: FeatureFlagVariant[];
rolloutSchedule?: RolloutSchedule;
}

class FeatureFlagService {
private flags: Map<string, FeatureFlag> = new Map();
private flags: Map<string, FeatureFlag> = 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,
});
}

Expand All @@ -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();
export const featureFlagEngine = new FeatureFlagService();
Empty file added backend/src/tracing.ts
Empty file.
Loading