UNPKG

@allan1361/iota-big3-sdk-middleware

Version:

🏆 A+ Grade Certified Enterprise Middleware Framework - Phase 3 Certified (90/100) with advanced resilience patterns, comprehensive type safety, and production-ready observability

309 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ABTestingMiddleware = void 0; exports.createABTestingMiddleware = createABTestingMiddleware; const tslib_1 = require("tslib"); const crypto = tslib_1.__importStar(require("crypto")); const events_1 = require("events"); class ABTestStatistics { static calculateZScore(conversionsA, visitorsA, conversionsB, visitorsB) { const rateA = conversionsA / visitorsA; const rateB = conversionsB / visitorsB; const pooledRate = (conversionsA + conversionsB) / (visitorsA + visitorsB); const standardError = Math.sqrt(pooledRate * (1 - pooledRate) * (1 / visitorsA + 1 / visitorsB)); return (rateA - rateB) / standardError; } static calculatePValue(zScore) { const z = Math.abs(zScore); const t = 1 / (1 + 0.3275911 * z); const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3989423 * Math.exp(-z * z / 2); const q = ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t; return Math.min((p * q) * 2, 1); } static calculateConfidence(pValue) { return (1 - pValue) * 100; } static calculateSampleSize(baselineRate, minimumDetectableEffect, power = 0.8, significance = 0.05) { const zAlpha = 1.96; const zBeta = 0.84; const p1 = baselineRate; const p2 = baselineRate * (1 + minimumDetectableEffect); const pooledP = (p1 + p2) / 2; const n = (2 * pooledP * (1 - pooledP) * Math.pow(zAlpha + zBeta, 2)) / Math.pow(p1 - p2, 2); return Math.ceil(n); } } class ABTestingMiddleware extends events_1.EventEmitter { constructor(config) { super(); this.tests = new Map(); this.assignments = new Map(); this.metrics = new Map(); this.config = { defaultCookieName: config?.defaultCookieName || 'ab-tests', defaultHeaderName: config?.defaultHeaderName || 'X-AB-Test', persistAssignments: config?.persistAssignments ?? true, metricsInterval: config?.metricsInterval || 60000 }; if (this.config.metricsInterval > 0) { setInterval(() => { this.emit('metrics', this.getAllResults()); }, this.config.metricsInterval); } } createTest(config) { const totalWeight = config.variants.reduce((sum, v) => sum + v.weight, 0); if (Math.abs(totalWeight - 100) > 0.01) { throw new Error(`Variant weights must sum to 100, got ${totalWeight}`); } const variantMetrics = new Map(); config.variants.forEach(variant => { variantMetrics.set(variant.name, { testName: config.name, variant: variant.name, pageViews: 0, conversions: 0, conversionRate: 0, customEvents: {}, avgResponseTime: 0, errors: 0, startTime: new Date(), lastUpdate: new Date() }); }); this.tests.set(config.name, config); this.metrics.set(config.name, variantMetrics); this.emit('test:created', config); } removeTest(testName) { this.tests.delete(testName); this.metrics.delete(testName); this.assignments.forEach(userTests => { userTests.delete(testName); }); this.emit('test:removed', testName); } toggleTest(testName, enabled) { const test = this.tests.get(testName); if (test) { test.enabled = enabled; this.emit('test:toggled', { testName, enabled }); } } getVariantAssignment(testConfig, userId, req) { const userAssignments = this.assignments.get(userId); if (userAssignments?.has(testConfig.name)) { return userAssignments.get(testConfig.name); } if (testConfig.segmentation) { for (const rule of testConfig.segmentation) { if (rule.condition(req)) { return this.selectVariantByWeight(rule.variants, userId); } } } return this.selectVariantByWeight(testConfig.variants, userId); } selectVariantByWeight(variants, userId) { if (variants.length === 0) { throw new Error('No variants available'); } const hash = crypto.createHash('sha256').update(userId).digest(); const hashValue = hash.readUInt32BE(0) / 0xffffffff; const targetValue = hashValue * 100; let cumulativeWeight = 0; for (const variant of variants) { cumulativeWeight += variant.weight; if (targetValue <= cumulativeWeight) { return variant.name; } } return variants[0].name; } storeAssignment(userId, testName, variant) { if (!this.assignments.has(userId)) { this.assignments.set(userId, new Map()); } this.assignments.get(userId).set(testName, variant); } trackEvent(testName, variant, eventType, eventName, value) { const testMetrics = this.metrics.get(testName); if (!testMetrics) return; const variantMetrics = testMetrics.get(variant); if (!variantMetrics) return; switch (eventType) { case 'pageView': variantMetrics.pageViews++; break; case 'conversion': variantMetrics.conversions++; variantMetrics.conversionRate = (variantMetrics.conversions / variantMetrics.pageViews) * 100; break; case 'error': variantMetrics.errors++; break; case 'custom': if (eventName) { variantMetrics.customEvents[eventName] = (variantMetrics.customEvents[eventName] || 0) + (value || 1); } break; } variantMetrics.lastUpdate = new Date(); this.emit('event:tracked', { testName, variant, eventType, eventName, value }); } middleware(testName) { return async (req, res, next) => { const test = this.tests.get(testName); if (!test || !test.enabled) { return next(); } const now = new Date(); if (test.startDate && now < test.startDate) { return next(); } if (test.endDate && now > test.endDate) { return next(); } try { const userId = test.userIdExtractor ? test.userIdExtractor(req) : req.ip || 'anonymous'; if (!userId) { return next(); } const variant = this.getVariantAssignment(test, userId, req); const variantConfig = test.variants.find(v => v.name === variant); if (!variantConfig) { return next(); } if (test.stickySession && this.config.persistAssignments) { this.storeAssignment(userId, testName, variant); } req.abTest = { name: testName, variant: variant, metadata: variantConfig.metadata }; const headerName = test.headerName || this.config.defaultHeaderName; res.setHeader(headerName, `${testName}:${variant}`); if (test.cookieName || this.config.defaultCookieName) { const cookieName = test.cookieName || this.config.defaultCookieName; const existingCookie = req.cookies?.[cookieName] || {}; res.cookie(cookieName, { ...existingCookie, [testName]: variant }, { httpOnly: true, maxAge: 30 * 24 * 60 * 60 * 1000 }); } if (test.metrics?.trackPageViews !== false) { this.trackEvent(testName, variant, 'pageView'); } const startTime = Date.now(); const originalSend = res.send.bind(res); const metricsMap = this.metrics; res.send = function (data) { const responseTime = Date.now() - startTime; const testMetrics = metricsMap.get(testName); const variantMetrics = testMetrics?.get(variant); if (variantMetrics) { const currentAvg = variantMetrics.avgResponseTime; const currentCount = variantMetrics.pageViews; variantMetrics.avgResponseTime = (currentAvg * (currentCount - 1) + responseTime) / currentCount; } return originalSend(data); }; if (variantConfig.handler) { return variantConfig.handler(req, res, next); } next(); } catch (error) { this.emit('error', { testName, error }); next(); } }; } getTestResults(testName) { const test = this.tests.get(testName); const testMetrics = this.metrics.get(testName); if (!test || !testMetrics) { return null; } const variants = {}; let totalSampleSize = 0; testMetrics.forEach((metrics, variantName) => { variants[variantName] = { ...metrics }; totalSampleSize += metrics.pageViews; }); let winner; let confidence; if (test.variants.length === 2) { const variantA = test.variants[0]; const variantB = test.variants[1]; if (variantA && variantB) { const metricsA = variants[variantA.name]; const metricsB = variants[variantB.name]; if (metricsA && metricsB && metricsA.pageViews > 30 && metricsB.pageViews > 30) { const zScore = ABTestStatistics.calculateZScore(metricsA.conversions, metricsA.pageViews, metricsB.conversions, metricsB.pageViews); const pValue = ABTestStatistics.calculatePValue(zScore); confidence = ABTestStatistics.calculateConfidence(pValue); if (confidence >= 95) { winner = metricsA.conversionRate > metricsB.conversionRate ? variantA.name : variantB.name; } } } } return { testName: test.name, status: test.enabled ? 'active' : 'paused', startDate: test.startDate || new Date(), endDate: test.endDate, variants, winner, confidence, sampleSize: totalSampleSize }; } getAllResults() { const results = []; this.tests.forEach((_, testName) => { const result = this.getTestResults(testName); if (result) { results.push(result); } }); return results; } calculateRequiredSampleSize(testName, baselineConversionRate, minimumDetectableEffect, power, significance) { const test = this.tests.get(testName); if (!test || test.variants.length !== 2) { throw new Error('Sample size calculation requires exactly 2 variants'); } return ABTestStatistics.calculateSampleSize(baselineConversionRate, minimumDetectableEffect, power, significance); } destroy() { this.removeAllListeners(); this.tests.clear(); this.assignments.clear(); this.metrics.clear(); } } exports.ABTestingMiddleware = ABTestingMiddleware; function createABTestingMiddleware(config) { return new ABTestingMiddleware(config); } //# sourceMappingURL=ab-testing-middleware.js.map