@omniconvert/server-side-testing-sdk
Version:
TypeScript SDK for server-side A/B testing and experimentation
340 lines • 12 kB
JavaScript
"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