UNPKG

@omniconvert/server-side-testing-sdk

Version:

TypeScript SDK for server-side A/B testing and experimentation

340 lines 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExploreClient = void 0; const HttpClient_1 = require("../http/HttpClient"); const DecisionManager_1 = require("../services/DecisionManager"); const Tracker_1 = require("../services/Tracker"); const StorageFacadeFactory_1 = require("../factories/StorageFacadeFactory"); const UserProviderFactory_1 = require("../factories/UserProviderFactory"); const types_1 = require("../types"); const LoggerFactory_1 = require("../logger/LoggerFactory"); /** * Main ExploreClient class - Primary SDK interface * Provides unified access to A/B testing functionality */ class ExploreClient { constructor(config) { this.lastDecisions = []; this.config = config; // Initialize logger based on debug flag this.logger = LoggerFactory_1.LoggerFactory.createLogger(config.debug ? 'debug' : 'info', 1000, config.debug || false); // Initialize storage this.storage = StorageFacadeFactory_1.StorageFacadeFactory.create(); // Initialize HTTP client this.httpClient = new HttpClient_1.HttpClient(config.apiKey, config.baseUrl, config.cacheBypass, this.logger); // Initialize user provider this.userProvider = new UserProviderFactory_1.UserProviderFactory(this.storage, this.logger); // Get or create user const user = this.userProvider.getUser(config.userId, config.sessionParams); // Initialize decision manager this.decisionManager = new DecisionManager_1.DecisionManager(this.storage, user, undefined, this.logger); // Initialize tracker this.tracker = new Tracker_1.Tracker(this.httpClient, user, undefined, this.logger); // Set website ID on tracker from storage if available const storedWebsiteId = this.storage.getWebsiteId(); if (storedWebsiteId) { this.tracker.setWebsiteId(storedWebsiteId); } // Start session if not already started if (!this.userProvider.isSessionStarted()) { const sessionTimeout = config.sessionTimeout || 36000; // 10 hours default (separate from reinitTimeout) this.userProvider.startSession(sessionTimeout); } // Initialize the client (fetch experiments) this.initialize(); } /** * Initialize the client by fetching experiments */ async initialize() { try { await this.refreshExperiments(); this.logger.debug('ExploreClient: Initialized successfully'); } catch (error) { this.logger.error('ExploreClient: Initialization failed', error); // Don't throw here to allow graceful degradation } } /** * Refresh experiments if needed based on various conditions */ async refreshExperiments() { const experimentsConfig = this.storage.getExperiments(); const lastFetchTime = this.storage.getLastExperimentsFetchTime(); const currentTime = Math.floor(Date.now() / 1000); const reinitTimeout = this.config.reinitTimeout || 600; // 10 minutes default let shouldRefresh = false; let reason = ''; // Check if experiments config is null or empty if (!experimentsConfig || experimentsConfig.length === 0) { shouldRefresh = true; reason = 'experiments config is null or empty'; } // Check if HTTP client has cache bypass else if (this.httpClient.hasCacheBypass()) { shouldRefresh = true; reason = 'HTTP client has cache bypass enabled'; } // Check if reinit timeout has passed else if (lastFetchTime && (currentTime - lastFetchTime) > reinitTimeout) { shouldRefresh = true; reason = 'reinit timeout has passed'; } if (shouldRefresh) { this.logger.debug(`ExploreClient: Experiments refresh triggered: ${reason}`); await this.fetchAndUpdateExperiments(); } else { this.logger.debug('ExploreClient: Experiments refresh not needed'); } } /** * Fetch and update experiments and related settings */ async fetchAndUpdateExperiments() { try { await this.fetchAndStoreExperimentsConfig(); this.logger.debug('ExploreClient: Experiments and settings updated'); } catch (error) { this.logger.error('ExploreClient: Failed to update experiments', error); throw error; } } /** * Fetch experiments from the API and store them */ async fetchAndStoreExperimentsConfig() { try { const response = await this.httpClient.requestExperiments(); if (response.status > 204) { throw new types_1.ExploreClientException(`API request failed with status ${response.status}`); } const responseData = await response.json(); // Validate response format if (!responseData || typeof responseData !== 'object') { throw new types_1.ExploreClientException('Invalid response format from API'); } // Store data const success = this.storage.saveExperiments(responseData.experiments || []) && this.storage.saveWebsiteId(responseData.website_id || '') && this.storage.saveSettings(responseData.settings || {}) && this.storage.saveLastExperimentsFetchTime(Math.floor(Date.now() / 1000)); if (success) { // Update tracker with website ID this.tracker.setWebsiteId(responseData.website_id); this.logger.debug('ExploreClient: Experiments config fetched and stored successfully'); } return success; } catch (error) { this.logger.error('ExploreClient: Failed to fetch experiments', error); // Reset experiments and settings to empty arrays on error this.storage.saveExperiments([]); this.storage.saveSettings({}); if (error instanceof types_1.ExploreClientException) { throw error; } throw new types_1.ExploreClientException(`Failed to fetch experiments: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Make experiment decisions for the given context */ decide(context, experimentKey = '') { try { // Validate context context.validate(); // Set context for tracker this.tracker.setContext(context); // Make decisions this.lastDecisions = this.decisionManager.decide(context, experimentKey); // Auto-track page view and variation view this.autoTrack(context); return this.lastDecisions; } catch (error) { this.logger.error('ExploreClient: Failed to make decisions', error); return []; } } /** * Auto-track page view and variation view */ async autoTrack(context) { try { // Track page view const pageUrl = context.getParamValueOrEmptyString('UrlLocationContextParam'); if (pageUrl) { await this.tracker.trackPageView(pageUrl); } // Track variation view if decisions were made if (this.lastDecisions.length > 0) { await this.tracker.trackVariationView(this.lastDecisions); } } catch (error) { this.logger.error('ExploreClient: Auto-tracking failed', error); // Don't throw here to avoid breaking the main decision flow } } /** * Get the website ID */ getWebsiteId() { return this.storage.getWebsiteId() || ''; } /** * Get experiments configuration */ getExperimentsConfig() { return this.storage.getExperiments(); } /** * Get the tracker instance */ getTracker() { return this.tracker; } /** * Get the decision manager instance */ getDecisionManager() { return this.decisionManager; } /** * Get the last decisions made */ getLastDecisions() { return [...this.lastDecisions]; } /** * Get the current user */ getUser() { return this.userProvider.getCurrentUser(); } /** * Get the storage facade */ getStorage() { return this.storage; } /** * Get the HTTP client */ getHttpClient() { return this.httpClient; } /** * Get the user provider */ getUserProvider() { return this.userProvider; } /** * Get client configuration */ getConfig() { return { ...this.config }; } /** * Force refresh experiments (ignore cache) */ async forceRefreshExperiments() { try { return await this.fetchAndStoreExperimentsConfig(); } catch (error) { this.logger.error('ExploreClient: Force refresh failed', error); return false; } } /** * Check if the client is properly initialized */ isInitialized() { const experiments = this.storage.getExperiments(); const websiteId = this.storage.getWebsiteId(); return experiments.length > 0 && !!websiteId; } /** * Get storage information */ getStorageInfo() { // This assumes LocalStorageDriver is being used const storage = this.storage; const driver = storage.storage?.driver; if (driver && typeof driver.getStorageInfo === 'function') { return driver.getStorageInfo(); } return { used: 0, total: 0, available: 0 }; } /** * Clear all data (experiments, user data, etc.) */ clearAllData() { try { this.storage.saveExperiments([]); this.storage.saveSettings({}); this.storage.saveWebsiteId(''); this.storage.saveLastExperimentsFetchTime(0); this.decisionManager.clearAssignments(); this.userProvider.clearUser(); this.lastDecisions = []; this.logger.debug('ExploreClient: All data cleared'); } catch (error) { this.logger.error('ExploreClient: Failed to clear data', error); } } /** * Clear cached experiments to force fresh fetch from API * This is useful when experiment configurations have changed on the server * or when there are issues with cached experiment data */ clearExperimentsCache() { try { this.storage.saveExperiments([]); this.storage.saveSettings({}); this.storage.saveLastExperimentsFetchTime(0); this.logger.debug('ExploreClient: Cleared experiments cache - next API call will fetch fresh data'); } catch (error) { this.logger.error('ExploreClient: Failed to clear experiments cache', error); } } /** * Update user activity (call this on user interactions) */ updateUserActivity() { this.userProvider.updateActivity(); } /** * Get SDK version */ static getVersion() { return '1.0.0'; } /** * Check if the SDK is running in a browser environment */ static isBrowser() { return typeof window !== 'undefined' && typeof localStorage !== 'undefined'; } /** * Get logs from the logger (similar to PHP's LoggedDumper::getLogs()) */ getLogs() { return this.logger.getLogs(); } /** * Get the logger instance */ getLogger() { return this.logger; } } exports.ExploreClient = ExploreClient; //# sourceMappingURL=ExploreClient.js.map