/** * Feature flag + A/B test detector. * * Detects popular feature flag / experiment SDKs loaded on the page: * LaunchDarkly, Optimizely, Split.io, Unleash, GrowthBook, Statsig, Flagsmith, Amplitude Experiment. * * Also detects common A/B test patterns: data-testid attributes, gtm.js experiment layers, * Optimizely variation IDs, VWO, etc. * * Why: ZeTa needs to know when a page shows different content to different users * so test generation can account for variants and not generate flaky tests. */ import type { Page } from 'playwright'; export interface FeatureFlagInfo { sdk: string; confidence: 'high' | 'medium' | 'low'; details?: string; } export interface AbTestInfo { tool: string; experiments?: string[]; } export interface FeatureFlagDetectionResult { url: string; flagSdks: FeatureFlagInfo[]; abTools: AbTestInfo[]; hasAnyFlags: boolean; detectedAt: string; } // SDK detectors — checked against window object + script URLs const FLAG_SDK_CHECKS: Array<{ sdk: string; windowKeys?: string[]; scriptPattern?: RegExp; }> = [ { sdk: 'LaunchDarkly', windowKeys: ['LDClient', 'LaunchDarkly', 'ldClient'], scriptPattern: /launchdarkly/i }, { sdk: 'Optimizely', windowKeys: ['optimizely', 'optimizelySdk'], scriptPattern: /optimizely/i }, { sdk: 'Split.io', windowKeys: ['splitio', 'SplitFactory'], scriptPattern: /split\.io/i }, { sdk: 'GrowthBook', windowKeys: ['growthbook', 'GrowthBook'], scriptPattern: /growthbook/i }, { sdk: 'Statsig', windowKeys: ['statsig', 'StatsigClient'], scriptPattern: /statsig/i }, { sdk: 'Flagsmith', windowKeys: ['flagsmith'], scriptPattern: /flagsmith/i }, { sdk: 'Unleash', windowKeys: ['unleash', 'UnleashClient'], scriptPattern: /unleash/i }, { sdk: 'VWO', windowKeys: ['vwo', '_vwo'], scriptPattern: /vwo\.com/i }, { sdk: 'Google Optimize', windowKeys: ['gaData', 'google_optimize'], scriptPattern: /googleoptimize/i }, { sdk: 'AB Tasty', windowKeys: ['ABTasty'], scriptPattern: /abtasty/i }, ]; export async function detectFeatureFlags(page: Page, url: string): Promise { const detectedAt = new Date().toISOString(); const result = await page.evaluate((checks) => { const flagSdks: any[] = []; const scripts = Array.from(document.querySelectorAll('script[src]')).map((s: any) => s.src ?? ''); for (const check of checks) { let found = false; let confidence = 'low'; // Check window keys if (check.windowKeys?.some((k: string) => k in (window as any))) { found = true; confidence = 'high'; } // Check script URLs if (!found && check.scriptPattern) { const re = new RegExp(check.scriptPattern.source, 'i'); const matchingSrc = scripts.find(s => re.test(s)); if (matchingSrc) { found = true; confidence = 'medium'; } } if (found) flagSdks.push({ sdk: check.sdk, confidence, details: undefined }); } // A/B tools const abTools: any[] = []; if ('_vwo_code' in window || '_vis_opt_queue' in (window as any)) abTools.push({ tool: 'VWO', experiments: [] }); if ('dataLayer' in window) { const dl = (window as any).dataLayer ?? []; const expEvents = dl.filter((e: any) => e?.event?.includes('experiment') || e?.experimentId); if (expEvents.length > 0) abTools.push({ tool: 'Google Tag Manager Experiments', experiments: expEvents.slice(0, 5).map((e: any) => e.experimentId ?? '') }); } return { flagSdks, abTools }; }, FLAG_SDK_CHECKS as any).catch(() => ({ flagSdks: [], abTools: [] })); return { url, flagSdks: result.flagSdks, abTools: result.abTools, hasAnyFlags: result.flagSdks.length > 0 || result.abTools.length > 0, detectedAt, }; }