diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts index 0daf24b22..d903bd4d0 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts @@ -13,95 +13,94 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { LogHandler } from '@optimizely/js-sdk-logging'; +import { ProjectConfig } from '../project_config'; +import { UserAttributes, UserProfileService, Experiment, Variation } from '../../shared_types'; +import { FeatureFlag } from '../project_config/entities'; -declare module '@optimizely/optimizely-sdk/lib/core/decision_service' { - import { LogHandler } from '@optimizely/js-sdk-logging'; - import { ProjectConfig } from '@optimizely/optimizely-sdk/lib/core/project_config'; +/** + * Creates an instance of the DecisionService. + * @param {Options} options Configuration options + * @return {DecisionService} An instance of the DecisionService + */ +export function createDecisionService(options: Options): DecisionService; + +export interface DecisionService { /** - * Creates an instance of the DecisionService. - * @param {Options} options Configuration options - * @return {DecisionService} An instance of the DecisionService + * Gets variation where visitor will be bucketed. + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {string} experimentKey + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string|null} The variation the user is bucketed into. */ - export function createDecisionService(options: Options): DecisionService; - - interface DecisionService { + getVariation( + configObj: ProjectConfig, + experimentKey: string, + userId: string, + attributes?: UserAttributes + ): string | null; - /** - * Gets variation where visitor will be bucketed. - * @param {ProjectConfig} configObj The parsed project configuration object - * @param {string} experimentKey - * @param {string} userId - * @param {UserAttributes} attributes - * @return {string|null} The variation the user is bucketed into. - */ - getVariation( - configObj: ProjectConfig, - experimentKey: string, - userId: string, - attributes?: import('../../shared_types').UserAttributes - ): string | null; - - /** - * Given a feature, user ID, and attributes, returns an object representing a - * decision. If the user was bucketed into a variation for the given feature - * and attributes, the returned decision object will have variation and - * experiment properties (both objects), as well as a decisionSource property. - * decisionSource indicates whether the decision was due to a rollout or an - * experiment. - * @param {ProjectConfig} configObj The parsed project configuration object - * @param {FeatureFlag} feature A feature flag object from project configuration - * @param {string} userId A string identifying the user, for bucketing - * @param {unknown} attributes Optional user attributes - * @return {Decision} An object with experiment, variation, and decisionSource - * properties. If the user was not bucketed into a variation, the variation - * property is null. - */ - getVariationForFeature( - configObj: ProjectConfig, - feature: import('../project_config/entities').FeatureFlag, - userId: string, - attributes: unknown - ): Decision; + /** + * Given a feature, user ID, and attributes, returns an object representing a + * decision. If the user was bucketed into a variation for the given feature + * and attributes, the returned decision object will have variation and + * experiment properties (both objects), as well as a decisionSource property. + * decisionSource indicates whether the decision was due to a rollout or an + * experiment. + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {FeatureFlag} feature A feature flag object from project configuration + * @param {string} userId A string identifying the user, for bucketing + * @param {unknown} attributes Optional user attributes + * @return {Decision} An object with experiment, variation, and decisionSource + * properties. If the user was not bucketed into a variation, the variation + * property is null. + */ + getVariationForFeature( + configObj: ProjectConfig, + feature: FeatureFlag, + userId: string, + attributes: unknown + ): Decision; - /** - * Removes forced variation for given userId and experimentKey - * @param {unknown} userId String representing the user id - * @param {string} experimentId Number representing the experiment id - * @param {string} experimentKey Key representing the experiment id - * @throws If the user id is not valid or not in the forced variation map - */ - removeForcedVariation(userId: unknown, experimentId: string, experimentKey: string): void; + /** + * Removes forced variation for given userId and experimentKey + * @param {unknown} userId String representing the user id + * @param {string} experimentId Number representing the experiment id + * @param {string} experimentKey Key representing the experiment id + * @throws If the user id is not valid or not in the forced variation map + */ + removeForcedVariation(userId: unknown, experimentId: string, experimentKey: string): void; - /** - * Gets the forced variation key for the given user and experiment. - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} experimentKey Key for experiment. - * @param {string} userId The user Id. - * @return {string|null} Variation key that specifies the variation which the given user and experiment should be forced into. - */ - getForcedVariation(configObj: ProjectConfig, experimentKey: string, userId: string): string | null; + /** + * Gets the forced variation key for the given user and experiment. + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Key for experiment. + * @param {string} userId The user Id. + * @return {string|null} Variation key that specifies the variation which the given user and experiment should be forced into. + */ + getForcedVariation(configObj: ProjectConfig, experimentKey: string, userId: string): string | null; - /** - * Sets the forced variation for a user in a given experiment - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} experimentKey Key for experiment. - * @param {string} userId The user Id. - * @param {unknown} variationKey Key for variation. If null, then clear the existing experiment-to-variation mapping - * @return {boolean} A boolean value that indicates if the set completed successfully. - */ - setForcedVariation(configObj: ProjectConfig, experimentKey: string, userId: string, variationKey: unknown): boolean; - } + /** + * Sets the forced variation for a user in a given experiment + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Key for experiment. + * @param {string} userId The user Id. + * @param {unknown} variationKey Key for variation. If null, then clear the existing experiment-to-variation mapping + * @return {boolean} A boolean value that indicates if the set completed successfully. + */ + setForcedVariation(configObj: ProjectConfig, experimentKey: string, userId: string, variationKey: unknown): boolean; +} - interface Options { - userProfileService: import('../../shared_types').UserProfileService | null; - logger: LogHandler; - UNSTABLE_conditionEvaluators: unknown; - } +interface Options { + userProfileService: UserProfileService | null; + logger: LogHandler; + UNSTABLE_conditionEvaluators: unknown; +} - interface Decision { - experiment: import('../../shared_types').Experiment | null; - variation: import('../../shared_types').Variation | null; - decisionSource: string; - } +interface Decision { + experiment: Experiment | null; + variation: Variation | null; + decisionSource: string; } diff --git a/packages/optimizely-sdk/lib/core/event_builder/event_helpers.d.ts b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.d.ts new file mode 100644 index 000000000..c57c8c7ae --- /dev/null +++ b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.d.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProjectConfig } from '../project_config'; +import { EventTags, UserAttributes } from '../../shared_types'; + +interface ImpressionConfig { + experimentKey: string; + variationKey: string; + userId: string; + userAttributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ImpressionEvent {} + +interface ConversionConfig { + eventKey: string; + eventTags?: EventTags; + userId: string; + userAttributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ConversionEvent {} + +/** + * Creates an ImpressionEvent object from decision data + * @param {ImpressionConfig} config + * @return {ImpressionEvent} an ImpressionEvent object + */ +export function buildImpressionEvent(config: ImpressionConfig): ImpressionEvent; + +/** + * Creates a ConversionEvent object from track + * @param {ConversionConfig} config + * @return {ConversionEvent} a ConversionEvent object + */ +export function buildConversionEvent(config: ConversionConfig): ConversionEvent; diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.d.ts b/packages/optimizely-sdk/lib/core/event_builder/index.d.ts new file mode 100644 index 000000000..757bcf7a6 --- /dev/null +++ b/packages/optimizely-sdk/lib/core/event_builder/index.d.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ProjectConfig } from '../project_config'; +import { LogHandler } from '@optimizely/js-sdk-logging'; +import { EventTags, UserAttributes } from '../../shared_types'; +import { Event as EventLoggingEndpoint } from '../../shared_types'; + +interface ImpressionOptions { + attributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; + experimentId: string; + eventKey?: string; + variationId: string; + logger?: LogHandler; + userId: string; +} + +interface ConversionEventOptions { + attributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; + eventKey: string; + logger: LogHandler; + userId: string; + eventTags?: EventTags; +} + +/** + * Create impression event params to be sent to the logging endpoint + * @param {ImpressionOptions} options Object containing values needed to build impression event + * @return {EventLoggingEndpoint} Params to be used in impression event logging endpoint call + */ +export function getImpressionEvent(options: ImpressionOptions): EventLoggingEndpoint; + +/** + * Create conversion event params to be sent to the logging endpoint + * @param {ConversionEventOptions} options Object containing values needed to build conversion event + * @return {EventLoggingEndpoint} Params to be used in conversion event logging endpoint call + */ +export function getConversionEvent(options: ConversionEventOptions): EventLoggingEndpoint; diff --git a/packages/optimizely-sdk/lib/core/notification_center/index.d.ts b/packages/optimizely-sdk/lib/core/notification_center/index.d.ts new file mode 100644 index 000000000..4f61f9233 --- /dev/null +++ b/packages/optimizely-sdk/lib/core/notification_center/index.d.ts @@ -0,0 +1,97 @@ +/* eslint-disable no-shadow */ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LogHandler, ErrorHandler } from '@optimizely/js-sdk-logging'; +import { + Event, + EventTags, + UserAttributes, + Experiment, + Variation +} from '../../shared_types'; + +export enum NOTIFICATION_TYPES { + ACTIVATE = 'ACTIVATE:experiment, user_id,attributes, variation, event', + DECISION = 'DECISION:type, userId, attributes, decisionInfo', + LOG_EVENT = 'LOG_EVENT:logEvent', + OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE', + TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', +} + +export enum DECISION_NOTIFICATION_TYPES { + AB_TEST = 'ab-test', + FEATURE = 'feature', + FEATURE_TEST = 'feature-test', + FEATURE_VARIABLE = 'feature-variable', + ALL_FEATURE_VARIABLES = 'all-feature-variables', +} + +export type Options = { + logger: LogHandler; + errorHandler: ErrorHandler; +}; + +export type SourceInfo = { + experimentKey?: string; + variationKey?: string; +}; + +export type VariableValues = { + [name: string]: unknown; +}; + +export type DecisionInfo = { + experimentKey?: string; + variationKey?: string | null; + featureKey?: string; + featureEnabled?: boolean; + source?: string; + sourceInfo?: SourceInfo; + variableKey?: string; + variableValue?: unknown; + variableValues?: VariableValues; + variableType?: string; +}; + +export interface NotificationData { + // type?: DECISION_NOTIFICATION_TYPES; + type?: string; + userId?: string; + attributes?: UserAttributes; + decisionInfo?: DecisionInfo; + experiment?: Experiment; + variation?: Variation; + logEvent?: Event; + eventKey?: string; + eventTags?: EventTags; +} + +export interface NotificationCenter { + /** + * Fires notifications for the argument type. All registered callbacks for this type will be + * called. The notificationData object will be passed on to callbacks called. + * @param {NOTIFICATION_TYPES} notificationType One of NOTIFICATION_TYPES + * @param {NotificationData} notificationData Will be passed to callbacks called + */ + sendNotifications(notificationType: NOTIFICATION_TYPES, notificationData?: NotificationData): void; +} + +/** + * Create an instance of NotificationCenter + * @param {Options} options + * @returns {NotificationCenter} An instance of NotificationCenter + */ +export function createNotificationCenter(options: Options): NotificationCenter; diff --git a/packages/optimizely-sdk/lib/core/project_config/entities.ts b/packages/optimizely-sdk/lib/core/project_config/entities.ts index 71d3fab1c..a9817a495 100644 --- a/packages/optimizely-sdk/lib/core/project_config/entities.ts +++ b/packages/optimizely-sdk/lib/core/project_config/entities.ts @@ -16,9 +16,20 @@ export interface FeatureVariable { type: string; + key: string; + id: string; + defaultValue: string; } export interface FeatureFlag { - variables: FeatureVariable[]; + rolloutId: string; + key: string; + id: string; + experimentIds: string[], + variables: FeatureVariable[], + variableKeyMap?: {[key: string]: FeatureVariable} } +export interface FeatureKeyMap { + [key: string]: FeatureFlag +} diff --git a/packages/optimizely-sdk/lib/core/project_config/index.d.ts b/packages/optimizely-sdk/lib/core/project_config/index.d.ts index c92f06b25..65b6f76c0 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.d.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.d.ts @@ -13,97 +13,143 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { LogHandler, ErrorHandler } from '@optimizely/js-sdk-logging'; +import { EventDispatcher } from '@optimizely/js-sdk-event-processor'; +import { DatafileOptions, UserProfileService, Experiment, Variation } from '../../shared_types'; +import { FeatureFlag, FeatureVariable } from './entities'; -declare module '@optimizely/optimizely-sdk/lib/core/project_config' { - import { LogHandler } from '@optimizely/js-sdk-logging'; - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface ProjectConfig {} - /** - * Determine for given experiment if event is running, which determines whether should be dispatched or not - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} experimentKey Experiment key for which the status is to be determined - * @return {boolean} True is the experiment is running - * False is the experiment is not running - * - */ - export function isRunning(configObj: ProjectConfig, experimentKey: string): boolean; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ProjectConfig { + revision: string; + projectId: string; + featureKeyMap?: { + [key: string]: FeatureFlag + }; + clientEngine: string; + clientVersion?: string; + errorHandler: ErrorHandler; + eventDispatcher: EventDispatcher; + isValidInstance: boolean; + // TODO[OASIS-6649]: Don't use object type + // eslint-disable-next-line @typescript-eslint/ban-types + datafile: object | string; + // TODO[OASIS-6649]: Don't use object type + // eslint-disable-next-line @typescript-eslint/ban-types + jsonSchemaValidator?: object; + sdkKey?: string; + userProfileService?: UserProfileService | null; + UNSTABLE_conditionEvaluators?: unknown; + eventFlushInterval?: number; + eventBatchSize?: number; + datafileOptions?: DatafileOptions; + eventMaxQueueSize?: number; + logger: LogHandler; + experimentKeyMap:{[key: string]: Experiment}; +} +/** + * Determine for given experiment if event is running, which determines whether should be dispatched or not + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Experiment key for which the status is to be determined + * @return {boolean} True is the experiment is running + * False is the experiment is not running + * + */ +export function isRunning(configObj: ProjectConfig, experimentKey: string): boolean; - /** - * Get the variation ID given the experiment key and variation key - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} experimentKey Key of the experiment the variation belongs to - * @param {string} variationKey The variation key - * @return {string} the variation ID - */ - export function getVariationIdFromExperimentAndVariationKey(configObj: ProjectConfig, experimentKey: string, variationKey: string): string; +/** + * Get the variation ID given the experiment key and variation key + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Key of the experiment the variation belongs to + * @param {string} variationKey The variation key + * @return {string} the variation ID + */ +export function getVariationIdFromExperimentAndVariationKey(configObj: ProjectConfig, experimentKey: string, variationKey: string): string; - /** - * Get experiment ID for the provided experiment key - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} experimentKey Experiment key for which ID is to be determined - * @return {string} Experiment ID corresponding to the provided experiment key - * @throws If experiment key is not in datafile - */ - export function getExperimentId(configObj: ProjectConfig, experimentKey: string): string | never; +/** + * Get experiment ID for the provided experiment key + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Experiment key for which ID is to be determined + * @return {string} Experiment ID corresponding to the provided experiment key + * @throws If experiment key is not in datafile + */ +export function getExperimentId(configObj: ProjectConfig, experimentKey: string): string | never; + +/** + * Check if the event with a specific key is present in the datafile + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} eventKey Event key for which event is to be determined + * @returns {boolean} True if key exists in the datafile + * False if key does not exist in the datafile + */ +export function eventWithKeyExists(configObj: ProjectConfig, eventKey: string): boolean; - /** - * Check if the event with a specific key is present in the datafile - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} eventKey Event key for which event is to be determined - * @returns {boolean} True if key exists in the datafile - * False if key does not exist in the datafile - */ - export function eventWithKeyExists(configObj: ProjectConfig, eventKey: string): boolean; +/** + * Check if the experiment is belongs to any feature + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentId Experiment ID of an experiment + * @returns {boolean} True if experiement belongs to any feature + * False if experiement does not belong to any feature + */ +export function isFeatureExperiment(configObj: ProjectConfig, experimentId: string): boolean; - /** - * Check if the experiment is belongs to any feature - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} experimentId Experiment ID of an experiment - * @returns {boolean} True if experiement belongs to any feature - * False if experiement does not belong to any feature - */ - export function isFeatureExperiment(configObj: ProjectConfig, experimentId: string): boolean; +/** + * Get feature from provided feature key. Log an error if no feature exists in + * the project config with the given key. + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} featureKey Key of a feature for which feature is to be determined + * @param {LogHandler} logger Logger instance + * @return {FeatureFlag|null} Feature object, or null if no feature with the given + * key exists + */ +export function getFeatureFromKey(configObj: ProjectConfig, featureKey: string, logger: LogHandler): FeatureFlag | null; - /** - * Get feature from provided feature key. Log an error if no feature exists in - * the project config with the given key. - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} featureKey Key of a feature for which feature is to be determined - * @param {LogHandler} logger Logger instance - * @return {FeatureFlag|null} Feature object, or null if no feature with the given - * key exists - */ - export function getFeatureFromKey(configObj: ProjectConfig, featureKey: string, logger: LogHandler): import('./entities').FeatureFlag | null; +/** + * Get the variable with the given key associated with the feature with the + * given key. If the feature key or the variable key are invalid, log an error + * message. + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} featureKey Key of a feature for which feature is to be determined + * @param {string} variableKey Key of a variable for which variable is to be determined + * @param {LogHandler} logger Logger instances + * @return {FeatureVariable|null} Variable object, or null one or both of the given + * feature and variable keys are invalid + */ +export function getVariableForFeature(configObj: ProjectConfig, featureKey: string, variableKey: string, logger: LogHandler): FeatureVariable | null; - /** - * Get the variable with the given key associated with the feature with the - * given key. If the feature key or the variable key are invalid, log an error - * message. - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} featureKey Key of a feature for which feature is to be determined - * @param {string} variableKey Key of a variable for which variable is to be determined - * @param {LogHandler} logger Logger instances - * @return {FeatureVariable|null} Variable object, or null one or both of the given - * feature and variable keys are invalid - */ - export function getVariableForFeature(configObj: ProjectConfig, featureKey: string, variableKey: string, logger: LogHandler): import('./entities').FeatureVariable | null; +/** + * Given a variable value in string form, try to cast it to the argument type. + * If the type cast succeeds, return the type casted value, otherwise log an + * error and return null. + * @param {string} variableValue Variable value in string form + * @param {string} type Type of the variable whose value was passed + * in the first argument. Must be one of + * FEATURE_VARIABLE_TYPES in + * lib/utils/enums/index.js. The return value's + * type is determined by this argument (boolean + * for BOOLEAN, number for INTEGER or DOUBLE, + * and string for STRING). + * @param {LogHandler} logger Logger instance + * @returns {T} Variable value of the appropriate type, or + * null if the type cast failed + */ +export function getTypeCastValue(variableValue: string, type: string, logger: LogHandler): T; - /** - * Given a variable value in string form, try to cast it to the argument type. - * If the type cast succeeds, return the type casted value, otherwise log an - * error and return null. - * @param {string} variableValue Variable value in string form - * @param {string} type Type of the variable whose value was passed - * in the first argument. Must be one of - * FEATURE_VARIABLE_TYPES in - * lib/utils/enums/index.js. The return value's - * type is determined by this argument (boolean - * for BOOLEAN, number for INTEGER or DOUBLE, - * and string for STRING). - * @param {LogHandler} logger Logger instance - * @returns {T} Variable value of the appropriate type, or - * null if the type cast failed - */ - export function getTypeCastValue(variableValue: string, type: string, logger: LogHandler): T; -} +/** + * Get the value of the given variable for the given variation. If the given + * variable has no value for the given variation, return null. Log an error message if the variation is invalid. If the + * variable or variation are invalid, return null. + * @param {ProjectConfig} projectConfig + * @param {FeatureVariable} variable + * @param {Variation} variation + * @param {LogHandler} logger + * @return {string|null} The value of the given variable for the given + * variation, or null if the given variable has no value + * for the given variation or if the variation or variable are invalid + */ +export function getVariableValueForVariation( + projectConfig: ProjectConfig, + variable: FeatureVariable, + variation: Variation, + logger: LogHandler +): string | null; diff --git a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.d.ts b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.d.ts index 5c586558e..f5601f8f0 100644 --- a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.d.ts +++ b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.d.ts @@ -13,73 +13,71 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ProjectConfig } from '../project_config'; +import { OptimizelyConfig } from '../../shared_types' -declare module '@optimizely/optimizely-sdk/lib/core/projet_config_manager' { - import { ProjectConfig } from '@optimizely/optimizely-sdk/lib/core/project_config'; - - /** - * ProjectConfigManager provides project config objects via its methods - * getConfig and onUpdate. It uses a DatafileManager to fetch datafiles. It is - * responsible for parsing and validating datafiles, and converting datafile - * JSON objects into project config objects. - * @param {ProjectConfig} config - * @param {Object|string} config.datafile - * @param {Object} config.datafileOptions - * @param {Object} config.jsonSchemaValidator - * @param {string} config.sdkKey - */ - export function ProjectConfigManager(config: ProjectConfig): ProjectConfigManager; +/** +* ProjectConfigManager provides project config objects via its methods +* getConfig and onUpdate. It uses a DatafileManager to fetch datafiles. It is +* responsible for parsing and validating datafiles, and converting datafile +* JSON objects into project config objects. +* @param {ProjectConfig} config +* @param {Object|string} config.datafile +* @param {Object} config.datafileOptions +* @param {Object} config.jsonSchemaValidator +* @param {string} config.sdkKey +*/ +export function createProjectConfigManager(config: Partial): ProjectConfigManager; - interface ProjectConfigManager { +export interface ProjectConfigManager { - /** - * Returns the current project config object, or null if no project config object - * is available - * @return {ProjectConfig|null} - */ - getConfig(): ProjectConfig | null; + /** + * Returns the current project config object, or null if no project config object + * is available + * @return {ProjectConfig|null} + */ + getConfig(): ProjectConfig | null; - /** - * Add a listener for project config updates. The listener will be called - * whenever this instance has a new project config object available. - * Returns a dispose function that removes the subscription - * @param {Function} listener - * @return {Function} - */ - onUpdate(): (listener: (config: ProjectConfig) => void) => () => void; + /** + * Add a listener for project config updates. The listener will be called + * whenever this instance has a new project config object available. + * Returns a dispose function that removes the subscription + * @param {Function} listener + * @return {Function} + */ + onUpdate(listener: (config: ProjectConfig) => void): (() => void) | null; - /** - * Returns a Promise that fulfills when this ProjectConfigManager is ready to - * use (meaning it has a valid project config object), or has failed to become - * ready. - * - * Failure can be caused by the following: - * - At least one of sdkKey or datafile is not provided in the constructor argument - * - The provided datafile was invalid - * - The datafile provided by the datafile manager was invalid - * - The datafile manager failed to fetch a datafile - * - * The returned Promise is fulfilled with a result object containing these - * properties: - * - success (boolean): True if this instance is ready to use with a valid - * project config object, or false if it failed to - * become ready - * - reason (string=): If success is false, this is a string property with - * an explanatory message. - * @return {Promise} - */ - onReady(): Promise<{ success: boolean; reason?: string }>; + /** + * Returns a Promise that fulfills when this ProjectConfigManager is ready to + * use (meaning it has a valid project config object), or has failed to become + * ready. + * + * Failure can be caused by the following: + * - At least one of sdkKey or datafile is not provided in the constructor argument + * - The provided datafile was invalid + * - The datafile provided by the datafile manager was invalid + * - The datafile manager failed to fetch a datafile + * + * The returned Promise is fulfilled with a result object containing these + * properties: + * - success (boolean): True if this instance is ready to use with a valid + * project config object, or false if it failed to + * become ready + * - reason (string=): If success is false, this is a string property with + * an explanatory message. + * @return {Promise} + */ + onReady(): Promise<{ success: boolean; reason?: string }>; - /** - * Returns the optimizely config object - * @return {OptimizelyConfig} - */ - getOptimizelyConfig(): import('../../shared_types').OptimizelyConfig; + /** + * Returns the optimizely config object + * @return {OptimizelyConfig} + */ + getOptimizelyConfig(): OptimizelyConfig; - /** - * Stop the internal datafile manager and remove all update listeners - * @return {void} - */ - stop(): void; - } + /** + * Stop the internal datafile manager and remove all update listeners + * @return {void} + */ + stop(): void; } diff --git a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.js b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.js index 7e32cd766..39c66b12a 100644 --- a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.js +++ b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.js @@ -51,7 +51,7 @@ function getErrorMessage(maybeError, defaultMessage) { * @param {Object=} config.jsonSchemaValidator * @param {string=} config.sdkKey */ -export function ProjectConfigManager(config) { +function ProjectConfigManager(config) { try { this.__initialize(config); } catch (ex) { @@ -282,6 +282,10 @@ ProjectConfigManager.prototype.stop = function() { this.__updateListeners = []; }; +export var createProjectConfigManager = function(config) { + return new ProjectConfigManager(config); +} + export default { - ProjectConfigManager: ProjectConfigManager, + createProjectConfigManager: createProjectConfigManager, }; diff --git a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js index 24e4752bf..359a94599 100644 --- a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js @@ -56,7 +56,7 @@ describe('lib/core/project_config/project_config_manager', function() { }); it('should call the error handler and fulfill onReady with an unsuccessful result if neither datafile nor sdkKey are passed into the constructor', function() { - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ }); sinon.assert.calledOnce(globalStubErrorHandler.handleError); var errorMessage = globalStubErrorHandler.handleError.lastCall.args[0].message; @@ -70,7 +70,7 @@ describe('lib/core/project_config/project_config_manager', function() { it('should call the error handler and fulfill onReady with an unsuccessful result if the datafile JSON is malformed', function() { var invalidDatafileJSON = 'abc'; - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: invalidDatafileJSON, }); sinon.assert.calledOnce(globalStubErrorHandler.handleError); @@ -86,7 +86,7 @@ describe('lib/core/project_config/project_config_manager', function() { it('should call the error handler and fulfill onReady with an unsuccessful result if the datafile is not valid', function() { var invalidDatafile = testData.getTestProjectConfig(); delete invalidDatafile['projectId']; - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: invalidDatafile, jsonSchemaValidator: jsonSchemaValidator, }); @@ -104,7 +104,7 @@ describe('lib/core/project_config/project_config_manager', function() { }); it('should call the error handler and fulfill onReady with an unsuccessful result if the datafile version is not supported', function() { - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: testData.getUnsupportedVersionConfig(), jsonSchemaValidator: jsonSchemaValidator, }); @@ -128,7 +128,7 @@ describe('lib/core/project_config/project_config_manager', function() { }); it('should skip JSON schema validation if jsonSchemaValidator is not provided', function() { - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: testData.getTestProjectConfig(), }); sinon.assert.notCalled(jsonSchemaValidator.validate); @@ -136,7 +136,7 @@ describe('lib/core/project_config/project_config_manager', function() { }); it('should not skip JSON schema validation if jsonSchemaValidator is provided', function() { - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: testData.getTestProjectConfig(), jsonSchemaValidator: jsonSchemaValidator, }); @@ -151,7 +151,7 @@ describe('lib/core/project_config/project_config_manager', function() { it('should return a valid datafile from getConfig and resolve onReady with a successful result', function() { var configWithFeatures = testData.getTestProjectConfigWithFeatures(); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: cloneDeep(configWithFeatures), }); assert.deepEqual(manager.getConfig(), projectConfig.createProjectConfig(configWithFeatures)); @@ -164,7 +164,7 @@ describe('lib/core/project_config/project_config_manager', function() { it('does not call onUpdate listeners after becoming ready when constructed with a valid datafile and without sdkKey', function() { var configWithFeatures = testData.getTestProjectConfigWithFeatures(); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: configWithFeatures, }); var onUpdateSpy = sinon.spy(); @@ -177,7 +177,7 @@ describe('lib/core/project_config/project_config_manager', function() { describe('with a datafile manager', function() { it('passes the correct options to datafile manager', function() { var config = testData.getTestProjectConfig() - new projectConfigManager.ProjectConfigManager({ + projectConfigManager.createProjectConfigManager({ datafile: config, sdkKey: '12345', datafileOptions: { @@ -207,7 +207,7 @@ describe('lib/core/project_config/project_config_manager', function() { on: sinon.stub().returns(function() {}), onReady: sinon.stub().returns(Promise.resolve()), }); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ sdkKey: '12345', }); assert.isNull(manager.getConfig()); @@ -245,7 +245,7 @@ describe('lib/core/project_config/project_config_manager', function() { on: sinon.stub().returns(function() {}), onReady: sinon.stub().returns(Promise.resolve()), }); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ sdkKey: '12345', }); var onUpdateSpy = sinon.spy(); @@ -272,7 +272,7 @@ describe('lib/core/project_config/project_config_manager', function() { on: sinon.stub().returns(function() {}), onReady: sinon.stub().returns(Promise.resolve()), }); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ sdkKey: '12345', }); return manager.onReady().then(function() { @@ -310,7 +310,7 @@ describe('lib/core/project_config/project_config_manager', function() { on: sinon.stub().returns(function() {}), onReady: sinon.stub().returns(Promise.resolve()), }); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ jsonSchemaValidator: jsonSchemaValidator, sdkKey: '12345', }); @@ -329,7 +329,7 @@ describe('lib/core/project_config/project_config_manager', function() { on: sinon.stub().returns(function() {}), onReady: sinon.stub().returns(Promise.reject(new Error('Failed to become ready'))), }); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ jsonSchemaValidator: jsonSchemaValidator, sdkKey: '12345', }); @@ -341,7 +341,7 @@ describe('lib/core/project_config/project_config_manager', function() { }); it('calls stop on its datafile manager when its stop method is called', function() { - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ sdkKey: '12345', }); manager.stop(); @@ -359,7 +359,7 @@ describe('lib/core/project_config/project_config_manager', function() { onReady: sinon.stub().returns(Promise.resolve()), }); var configWithFeatures = testData.getTestProjectConfigWithFeatures(); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: configWithFeatures, sdkKey: '12345', }); @@ -386,7 +386,7 @@ describe('lib/core/project_config/project_config_manager', function() { onReady: sinon.stub().returns(Promise.resolve()), }); var configWithFeatures = testData.getTestProjectConfigWithFeatures(); - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: JSON.stringify(configWithFeatures), sdkKey: '12345', }); @@ -413,7 +413,7 @@ describe('lib/core/project_config/project_config_manager', function() { }); it('should return the same config until revision is changed', function() { - var manager = new projectConfigManager.ProjectConfigManager({ + var manager = projectConfigManager.createProjectConfigManager({ datafile: testData.getTestProjectConfig(), sdkKey: '12345', }); diff --git a/packages/optimizely-sdk/lib/index.d.ts b/packages/optimizely-sdk/lib/index.d.ts index 3bc79687c..b0fecf25c 100644 --- a/packages/optimizely-sdk/lib/index.d.ts +++ b/packages/optimizely-sdk/lib/index.d.ts @@ -18,6 +18,7 @@ declare module '@optimizely/optimizely-sdk' { import { LogHandler, ErrorHandler } from '@optimizely/js-sdk-logging'; import * as enums from '@optimizely/optimizely-sdk/lib/utils/enums'; import * as logging from '@optimizely/optimizely-sdk/lib/plugins/logger'; + import { EventDispatcher } from '@optimizely/js-sdk-event-processor'; export { enums, logging }; export function setLogger(logger: LogHandler | null): void; @@ -30,19 +31,12 @@ declare module '@optimizely/optimizely-sdk' { export const eventDispatcher: EventDispatcher; - interface DatafileOptions { - autoUpdate?: boolean; - updateInterval?: number; - urlTemplate?: string; - datafileAccessToken?: string; - } - // The options object given to Optimizely.createInstance. export interface Config { // TODO[OASIS-6649]: Don't use object type // eslint-disable-next-line @typescript-eslint/ban-types datafile?: object | string; - datafileOptions?: DatafileOptions; + datafileOptions?: import('./shared_types').DatafileOptions; errorHandler?: ErrorHandler; eventDispatcher?: EventDispatcher; logger?: LogHandler; @@ -72,7 +66,7 @@ declare module '@optimizely/optimizely-sdk' { eventKey: string, userId: string, attributes?: import('./shared_types').UserAttributes, - eventTags?: EventTags + eventTags?: import('./shared_types').EventTags ): void; getVariation( experimentKey: string, @@ -130,36 +124,12 @@ declare module '@optimizely/optimizely-sdk' { featureKey: string, userId: string, attributes?: import('./shared_types').UserAttributes - ): { [variableKey: string]: unknown }; + ): { [variableKey: string]: unknown } | null; getOptimizelyConfig(): import('./shared_types').OptimizelyConfig | null; onReady(options?: { timeout?: number }): Promise<{ success: boolean; reason?: string }>; close(): Promise<{ success: boolean; reason?: string }>; } - // An event to be submitted to Optimizely, enabling tracking the reach and impact of - // tests and feature rollouts. - export interface Event { - // URL to which to send the HTTP request. - url: string; - // HTTP method with which to send the event. - httpVerb: 'POST'; - // Value to send in the request body, JSON-serialized. - // TODO[OASIS-6649]: Don't use any type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params: any; - } - - export interface EventDispatcher { - /** - * @param event - * Event being submitted for eventual dispatch. - * @param callback - * After the event has at least been queued for dispatch, call this function to return - * control back to the Client. - */ - dispatchEvent: (event: Event, callback: () => void) => void; - } - // NotificationCenter-related types export interface NotificationCenter { addNotificationListener( @@ -184,13 +154,9 @@ declare module '@optimizely/optimizely-sdk' { logEvent: Event; } - export type EventTags = { - [key: string]: string | number | boolean; - }; - export interface TrackListenerPayload extends ListenerPayload { eventKey: string; - eventTags: EventTags; + eventTags: import('./shared_types').EventTags; logEvent: Event; } } diff --git a/packages/optimizely-sdk/lib/index.react_native.js b/packages/optimizely-sdk/lib/index.react_native.ts similarity index 75% rename from packages/optimizely-sdk/lib/index.react_native.js rename to packages/optimizely-sdk/lib/index.react_native.ts index d69bbcae3..31b5af1d1 100644 --- a/packages/optimizely-sdk/lib/index.react_native.js +++ b/packages/optimizely-sdk/lib/index.react_native.ts @@ -13,47 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { +import { getLogger, setLogHandler, setLogLevel, setErrorHandler, getErrorHandler, LogLevel, + ErrorHandler, + LogHandler, } from '@optimizely/js-sdk-logging'; import * as enums from './utils/enums'; import { assign } from './utils/fns'; import Optimizely from './optimizely'; -import configValidator from './utils/config_validator'; +import * as configValidator from './utils/config_validator'; import defaultErrorHandler from './plugins/error_handler'; import loggerPlugin from './plugins/logger/index.react_native'; import defaultEventDispatcher from './plugins/event_dispatcher/index.browser'; import eventProcessorConfigValidator from './utils/event_processor_config_validator'; +import * as projectConfig from './core/project_config'; -var logger = getLogger(); +const logger = getLogger(); setLogHandler(loggerPlugin.createLogger()); setLogLevel(LogLevel.INFO); -var DEFAULT_EVENT_BATCH_SIZE = 10; -var DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s -var DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; +const DEFAULT_EVENT_BATCH_SIZE = 10; +const DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s +const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; + +interface Config { + datafile?: projectConfig.ProjectConfig; + errorHandler?: ErrorHandler; + eventDispatcher?: (...args: unknown[]) => unknown; + logger?: LogHandler; + logLevel?: LogLevel; + userProfileService?: import('./shared_types').UserProfileService; + eventBatchSize?: number; + eventFlushInterval?: number; + sdkKey?: string; + isValidInstance?: boolean; +} /** * Creates an instance of the Optimizely class - * @param {Object} config - * @param {Object|string} config.datafile - * @param {Object} config.errorHandler - * @param {Object} config.eventDispatcher - * @param {Object} config.logger - * @param {Object} config.logLevel - * @param {Object} config.userProfileService - * @param {Object} config.eventBatchSize - * @param {Object} config.eventFlushInterval - * @param {string} config.sdkKey - * @return {Object} the Optimizely object + * @param {Config} config + * @return {Optimizely} the Optimizely object */ -var createInstance = function(config) { +const createInstance = function (config: Config): Optimizely | null { try { config = config || {}; @@ -78,7 +85,7 @@ var createInstance = function(config) { config.isValidInstance = false; } - config = assign( + const optimizelyConfig = assign( { clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, eventBatchSize: DEFAULT_EVENT_BATCH_SIZE, @@ -92,11 +99,11 @@ var createInstance = function(config) { logger: logger, errorHandler: getErrorHandler(), } - ); + ) as projectConfig.ProjectConfig; if (!eventProcessorConfigValidator.validateEventBatchSize(config.eventBatchSize)) { logger.warn('Invalid eventBatchSize %s, defaulting to %s', config.eventBatchSize, DEFAULT_EVENT_BATCH_SIZE); - config.eventBatchSize = DEFAULT_EVENT_BATCH_SIZE; + optimizelyConfig.eventBatchSize = DEFAULT_EVENT_BATCH_SIZE; } if (!eventProcessorConfigValidator.validateEventFlushInterval(config.eventFlushInterval)) { logger.warn( @@ -104,15 +111,15 @@ var createInstance = function(config) { config.eventFlushInterval, DEFAULT_EVENT_FLUSH_INTERVAL ); - config.eventFlushInterval = DEFAULT_EVENT_FLUSH_INTERVAL; + optimizelyConfig.eventFlushInterval = DEFAULT_EVENT_FLUSH_INTERVAL; } - return new Optimizely(config); + return new Optimizely(optimizelyConfig); } catch (e) { logger.error(e); return null; } -} +}; /** * Entry point into the Optimizely Javascript SDK for React Native @@ -125,7 +132,7 @@ export { setLogHandler as setLogger, setLogLevel, createInstance, -} +}; export default { logging: loggerPlugin, diff --git a/packages/optimizely-sdk/lib/optimizely/index.js b/packages/optimizely-sdk/lib/optimizely/index.js deleted file mode 100644 index fd15337da..000000000 --- a/packages/optimizely-sdk/lib/optimizely/index.js +++ /dev/null @@ -1,1271 +0,0 @@ -/**************************************************************************** - * Copyright 2016-2020, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ -import { sprintf, objectValues } from '@optimizely/js-sdk-utils'; -import * as eventProcessor from '@optimizely/js-sdk-event-processor'; - -import { isSafeInteger } from '../utils/fns' -import { validate } from '../utils/attributes_validator'; -import decisionService from '../core/decision_service'; -import * as enums from '../utils/enums'; -import { getImpressionEvent, getConversionEvent } from '../core/event_builder/index.js'; -import { buildConversionEvent, buildImpressionEvent } from '../core/event_builder/event_helpers'; -import * as eventTagsValidator from '../utils/event_tags_validator'; -import notificationCenter from '../core/notification_center'; -import projectConfig from '../core/project_config'; -import * as userProfileServiceValidator from '../utils/user_profile_service_validator'; -import * as stringValidator from '../utils/string_value_validator'; -import projectConfigManager from '../core/project_config/project_config_manager'; - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MODULE_NAME = 'OPTIMIZELY'; -var DECISION_SOURCES = enums.DECISION_SOURCES; -var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; -var DECISION_NOTIFICATION_TYPES = enums.DECISION_NOTIFICATION_TYPES; -var NOTIFICATION_TYPES = enums.NOTIFICATION_TYPES; - -var DEFAULT_ONREADY_TIMEOUT = 30000; - -/** - * The Optimizely class - * @param {Object} config - * @param {string} config.clientEngine - * @param {string} config.clientVersion - * @param {Object|string} config.datafile - * @param {Object} config.errorHandler - * @param {Object} config.eventDispatcher - * @param {Object} config.logger - * @param {Object} config.userProfileService - * @param {Object} config.eventBatchSize - * @param {Object} config.eventFlushInterval - * @param {string} config.sdkKey - */ -function Optimizely(config) { - var clientEngine = config.clientEngine; - if (enums.VALID_CLIENT_ENGINES.indexOf(clientEngine) === -1) { - config.logger.log( - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, MODULE_NAME, clientEngine) - ); - clientEngine = enums.NODE_CLIENT_ENGINE; - } - - this.clientEngine = clientEngine; - this.clientVersion = config.clientVersion || enums.NODE_CLIENT_VERSION; - this.errorHandler = config.errorHandler; - this.eventDispatcher = config.eventDispatcher; - this.__isOptimizelyConfigValid = config.isValidInstance; - this.logger = config.logger; - - this.projectConfigManager = new projectConfigManager.ProjectConfigManager({ - datafile: config.datafile, - datafileOptions: config.datafileOptions, - jsonSchemaValidator: config.jsonSchemaValidator, - sdkKey: config.sdkKey, - }); - - this.__disposeOnUpdate = this.projectConfigManager.onUpdate( - function(configObj) { - this.logger.log( - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.UPDATED_OPTIMIZELY_CONFIG, MODULE_NAME, configObj.revision, configObj.projectId) - ); - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); - }.bind(this) - ); - - var projectConfigManagerReadyPromise = this.projectConfigManager.onReady(); - - var userProfileService = null; - if (config.userProfileService) { - try { - if (userProfileServiceValidator.validate(config.userProfileService)) { - userProfileService = config.userProfileService; - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.VALID_USER_PROFILE_SERVICE, MODULE_NAME)); - } - } catch (ex) { - this.logger.log(LOG_LEVEL.WARNING, ex.message); - } - } - - this.decisionService = decisionService.createDecisionService({ - userProfileService: userProfileService, - logger: this.logger, - UNSTABLE_conditionEvaluators: config.UNSTABLE_conditionEvaluators, - }); - - this.notificationCenter = notificationCenter.createNotificationCenter({ - logger: this.logger, - errorHandler: this.errorHandler, - }); - - this.eventProcessor = new eventProcessor.LogTierV1EventProcessor({ - dispatcher: this.eventDispatcher, - flushInterval: config.eventFlushInterval, - batchSize: config.eventBatchSize, - maxQueueSize: config.eventMaxQueueSize, - notificationCenter: this.notificationCenter, - }); - - var eventProcessorStartedPromise = this.eventProcessor.start(); - - this.__readyPromise = Promise.all([projectConfigManagerReadyPromise, eventProcessorStartedPromise]).then(function(promiseResults) { - // Only return status from project config promise because event processor promise does not return any status. - return promiseResults[0]; - }) - - this.__readyTimeouts = {}; - this.__nextReadyTimeoutId = 0; -} - -/** - * Returns a truthy value if this instance currently has a valid project config - * object, and the initial configuration object that was passed into the - * constructor was also valid. - * @return {*} - */ -Optimizely.prototype.__isValidInstance = function() { - return this.__isOptimizelyConfigValid && this.projectConfigManager.getConfig(); -}; - -/** - * Buckets visitor and sends impression event to Optimizely. - * @param {string} experimentKey - * @param {string} userId - * @param {Object} attributes - * @return {string|null} variation key - */ -Optimizely.prototype.activate = function(experimentKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'activate')); - return null; - } - - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { - return this.__notActivatingExperiment(experimentKey, userId); - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - - try { - var variationKey = this.getVariation(experimentKey, userId, attributes); - if (variationKey === null) { - return this.__notActivatingExperiment(experimentKey, userId); - } - - // If experiment is not set to 'Running' status, log accordingly and return variation key - if (!projectConfig.isRunning(configObj, experimentKey)) { - var shouldNotDispatchActivateLogMessage = sprintf( - LOG_MESSAGES.SHOULD_NOT_DISPATCH_ACTIVATE, - MODULE_NAME, - experimentKey - ); - this.logger.log(LOG_LEVEL.DEBUG, shouldNotDispatchActivateLogMessage); - return variationKey; - } - - this._sendImpressionEvent(experimentKey, variationKey, userId, attributes); - - return variationKey; - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - var failedActivationLogMessage = sprintf( - LOG_MESSAGES.NOT_ACTIVATING_USER, - MODULE_NAME, - userId, - experimentKey - ); - this.logger.log(LOG_LEVEL.INFO, failedActivationLogMessage); - this.errorHandler.handleError(ex); - return null; - } - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Create an impression event and call the event dispatcher's dispatch method to - * send this event to Optimizely. Then use the notification center to trigger - * any notification listeners for the ACTIVATE notification type. - * @param {string} experimentKey Key of experiment that was activated - * @param {string} variationKey Key of variation shown in experiment that was activated - * @param {string} userId ID of user to whom the variation was shown - * @param {Object} attributes Optional user attributes - */ -Optimizely.prototype._sendImpressionEvent = function(experimentKey, variationKey, userId, attributes) { - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return; - } - - var impressionEvent = buildImpressionEvent({ - experimentKey: experimentKey, - variationKey: variationKey, - userId: userId, - userAttributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: configObj, - }); - // TODO is it okay to not pass a projectConfig as second argument - this.eventProcessor.process(impressionEvent); - this.__emitNotificationCenterActivate(experimentKey, variationKey, userId, attributes); -}; - -/** - * Emit the ACTIVATE notification on the notificationCenter - * @param {string} experimentKey Key of experiment that was activated - * @param {string} variationKey Key of variation shown in experiment that was activated - * @param {string} userId ID of user to whom the variation was shown - * @param {Object} attributes Optional user attributes - */ -Optimizely.prototype.__emitNotificationCenterActivate = function(experimentKey, variationKey, userId, attributes) { - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return; - } - - var variationId = projectConfig.getVariationIdFromExperimentAndVariationKey(configObj, experimentKey, variationKey); - var experimentId = projectConfig.getExperimentId(configObj, experimentKey); - var impressionEventOptions = { - attributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: configObj, - experimentId: experimentId, - userId: userId, - variationId: variationId, - logger: this.logger, - }; - var impressionEvent = getImpressionEvent(impressionEventOptions); - var experiment = configObj.experimentKeyMap[experimentKey]; - var variation; - if (experiment && experiment.variationKeyMap) { - variation = experiment.variationKeyMap[variationKey]; - } - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, { - experiment: experiment, - userId: userId, - attributes: attributes, - variation: variation, - logEvent: impressionEvent, - }); -}; - -/** - * Sends conversion event to Optimizely. - * @param {string} eventKey - * @param {string} userId - * @param {string} attributes - * @param {Object} eventTags Values associated with the event. - */ -Optimizely.prototype.track = function(eventKey, userId, attributes, eventTags) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'track')); - return; - } - - if (!this.__validateInputs({ user_id: userId, event_key: eventKey }, attributes, eventTags)) { - return; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return; - } - - if (!projectConfig.eventWithKeyExists(configObj, eventKey)) { - this.logger.log( - LOG_LEVEL.WARNING, - sprintf(enums.LOG_MESSAGES.EVENT_KEY_NOT_FOUND, MODULE_NAME, eventKey) - ); - this.logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.NOT_TRACKING_USER, MODULE_NAME, userId)); - return; - } - - // remove null values from eventTags - eventTags = this.__filterEmptyValues(eventTags); - var conversionEvent = buildConversionEvent({ - eventKey: eventKey, - eventTags: eventTags, - userId: userId, - userAttributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: configObj, - }); - this.logger.log(LOG_LEVEL.INFO, sprintf(enums.LOG_MESSAGES.TRACK_EVENT, MODULE_NAME, eventKey, userId)); - // TODO is it okay to not pass a projectConfig as second argument - this.eventProcessor.process(conversionEvent); - this.__emitNotificationCenterTrack(eventKey, userId, attributes, eventTags); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - var failedTrackLogMessage = sprintf(LOG_MESSAGES.NOT_TRACKING_USER, MODULE_NAME, userId); - this.logger.log(LOG_LEVEL.ERROR, failedTrackLogMessage); - } -}; - -/** - * Send TRACK event to notificationCenter - * @param {string} eventKey - * @param {string} userId - * @param {string} attributes - * @param {Object} eventTags Values associated with the event. - */ -Optimizely.prototype.__emitNotificationCenterTrack = function(eventKey, userId, attributes, eventTags) { - try { - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return; - } - - var conversionEventOptions = { - attributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: configObj, - eventKey: eventKey, - eventTags: eventTags, - logger: this.logger, - userId: userId, - }; - var conversionEvent = getConversionEvent(conversionEventOptions); - - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.TRACK, { - eventKey: eventKey, - userId: userId, - attributes: attributes, - eventTags: eventTags, - logEvent: conversionEvent, - }); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - } -}; - -/** - * Gets variation where visitor will be bucketed. - * @param {string} experimentKey - * @param {string} userId - * @param {Object} attributes - * @return {string|null} variation key - */ -Optimizely.prototype.getVariation = function(experimentKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getVariation')); - return null; - } - - try { - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { - return null; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - - var experiment = configObj.experimentKeyMap[experimentKey]; - if (!experiment) { - this.logger.log( - LOG_LEVEL.DEBUG, - sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey) - ); - return null; - } - - var variationKey = this.decisionService.getVariation(configObj, experimentKey, userId, attributes); - var decisionNotificationType = projectConfig.isFeatureExperiment(configObj, experiment.id) - ? DECISION_NOTIFICATION_TYPES.FEATURE_TEST - : DECISION_NOTIFICATION_TYPES.AB_TEST; - - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { - type: decisionNotificationType, - userId: userId, - attributes: attributes || {}, - decisionInfo: { - experimentKey: experimentKey, - variationKey: variationKey, - }, - }); - - return variationKey; - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return null; - } - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Force a user into a variation for a given experiment. - * @param {string} experimentKey - * @param {string} userId - * @param {string|null} variationKey user will be forced into. If null, then clear the existing experiment-to-variation mapping. - * @return boolean A boolean value that indicates if the set completed successfully. - */ -Optimizely.prototype.setForcedVariation = function(experimentKey, userId, variationKey) { - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId })) { - return false; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return false; - } - - try { - return this.decisionService.setForcedVariation(configObj, experimentKey, userId, variationKey); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return false; - } -}; - -/** - * Gets the forced variation for a given user and experiment. - * @param {string} experimentKey - * @param {string} userId - * @return {string|null} The forced variation key. - */ -Optimizely.prototype.getForcedVariation = function(experimentKey, userId) { - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId })) { - return null; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - - try { - return this.decisionService.getForcedVariation(configObj, experimentKey, userId); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return null; - } -}; - -/** - * Validate string inputs, user attributes and event tags. - * @param {string} stringInputs Map of string keys and associated values - * @param {Object} userAttributes Optional parameter for user's attributes - * @param {Object} eventTags Optional parameter for event tags - * @return {boolean} True if inputs are valid - * - */ -Optimizely.prototype.__validateInputs = function(stringInputs, userAttributes, eventTags) { - try { - // Null, undefined or non-string user Id is invalid. - if (stringInputs.hasOwnProperty('user_id')) { - var userId = stringInputs.user_id; - if (typeof userId !== 'string' || userId === null || userId === 'undefined') { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, MODULE_NAME, 'user_id')); - } - - delete stringInputs.user_id; - } - - var inputKeys = Object.keys(stringInputs); - for (var index = 0; index < inputKeys.length; index++) { - var key = inputKeys[index]; - if (!stringValidator.validate(stringInputs[key])) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, MODULE_NAME, key)); - } - } - if (userAttributes) { - validate(userAttributes); - } - if (eventTags) { - eventTagsValidator.validate(eventTags); - } - return true; - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return false; - } -}; - -/** - * Shows failed activation log message and returns null when user is not activated in experiment - * @param experimentKey - * @param userId - * @return {null} - */ -Optimizely.prototype.__notActivatingExperiment = function(experimentKey, userId) { - var failedActivationLogMessage = sprintf( - LOG_MESSAGES.NOT_ACTIVATING_USER, - MODULE_NAME, - userId, - experimentKey - ); - this.logger.log(LOG_LEVEL.INFO, failedActivationLogMessage); - return null; -}; - -/** - * Filters out attributes/eventTags with null or undefined values - * @param map - * @returns {Object} map - */ -Optimizely.prototype.__filterEmptyValues = function(map) { - for (var key in map) { - if (map.hasOwnProperty(key) && (map[key] === null || map[key] === undefined)) { - delete map[key]; - } - } - return map; -}; - -/** - * Returns true if the feature is enabled for the given user. - * @param {string} featureKey Key of feature which will be checked - * @param {string} userId ID of user which will be checked - * @param {Object} attributes Optional user attributes - * @return {boolean} True if the feature is enabled for the user, false otherwise - */ -Optimizely.prototype.isFeatureEnabled = function(featureKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log( - LOG_LEVEL.ERROR, - sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'isFeatureEnabled') - ); - return false; - } - - if (!this.__validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { - return false; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return false; - } - - var feature = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); - if (!feature) { - return false; - } - - var featureEnabled = false; - var decision = this.decisionService.getVariationForFeature(configObj, feature, userId, attributes); - var variation = decision.variation; - var sourceInfo = {}; - - if (variation) { - featureEnabled = variation.featureEnabled; - if (decision.decisionSource === DECISION_SOURCES.FEATURE_TEST) { - sourceInfo = { - experimentKey: decision.experiment.key, - variationKey: decision.variation.key, - }; - // got a variation from the exp, so we track the impression - this._sendImpressionEvent(decision.experiment.key, decision.variation.key, userId, attributes); - } - } - - if (featureEnabled === true) { - this.logger.log( - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.FEATURE_ENABLED_FOR_USER, MODULE_NAME, featureKey, userId) - ); - } else { - this.logger.log( - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.FEATURE_NOT_ENABLED_FOR_USER, MODULE_NAME, featureKey, userId) - ); - featureEnabled = false; - } - - var featureInfo = { - featureKey: featureKey, - featureEnabled: featureEnabled, - source: decision.decisionSource, - sourceInfo: sourceInfo, - }; - - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { - type: DECISION_NOTIFICATION_TYPES.FEATURE, - userId: userId, - attributes: attributes || {}, - decisionInfo: featureInfo, - }); - - return featureEnabled; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return false; - } -}; - -/** - * Returns an Array containing the keys of all features in the project that are - * enabled for the given user. - * @param {string} userId - * @param {Object} attributes - * @return {Array} Array of feature keys (strings) - */ -Optimizely.prototype.getEnabledFeatures = function(userId, attributes) { - try { - var enabledFeatures = []; - if (!this.__isValidInstance()) { - this.logger.log( - LOG_LEVEL.ERROR, - sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getEnabledFeatures') - ); - return enabledFeatures; - } - - if (!this.__validateInputs({ user_id: userId })) { - return enabledFeatures; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return enabledFeatures; - } - - objectValues(configObj.featureKeyMap).forEach( - function(feature) { - if (this.isFeatureEnabled(feature.key, userId, attributes)) { - enabledFeatures.push(feature.key); - } - }.bind(this) - ); - - return enabledFeatures; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return []; - } -}; - -/** - * Returns dynamically-typed value of the variable attached to the given - * feature flag. Returns null if the feature key or variable key is invalid. - * - * @param {string} featureKey Key of the feature whose variable's - * value is being accessed - * @param {string} variableKey Key of the variable whose value is - * being accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {string|boolean|number|null} Value of the variable cast to the appropriate - * type, or null if the feature key is invalid or - * the variable key is invalid - */ - -Optimizely.prototype.getFeatureVariable = function(featureKey, variableKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariable')); - return null; - } - return this._getFeatureVariableForType(featureKey, variableKey, null, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Helper method to get the value for a variable of a certain type attached to a - * feature flag. Returns null if the feature key is invalid, the variable key is - * invalid, the given variable type does not match the variable's actual type, - * or the variable value cannot be cast to the required type. If the given variable - * type is null, the value of the variable cast to the appropriate type is returned. - * - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string|null} variableType Type of the variable whose value is being - * accessed (must be one of FEATURE_VARIABLE_TYPES - * in lib/utils/enums/index.js), or null to return the - * value of the variable cast to the appropriate type - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {string|boolean|number|null} Value of the variable cast to the appropriate - * type, or null if the feature key is invalid, the - * variable key is invalid, or there is a mismatch - * with the type of the variable - */ -Optimizely.prototype._getFeatureVariableForType = function(featureKey, variableKey, variableType, userId, attributes) { - if (!this.__validateInputs({ feature_key: featureKey, variable_key: variableKey, user_id: userId }, attributes)) { - return null; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - - var featureFlag = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); - if (!featureFlag) { - return null; - } - - var variable = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, this.logger); - if (!variable) { - return null; - } - - if (variableType && variable.type !== variableType) { - this.logger.log( - LOG_LEVEL.WARNING, - sprintf(LOG_MESSAGES.VARIABLE_REQUESTED_WITH_WRONG_TYPE, MODULE_NAME, variableType, variable.type) - ); - return null; - } - - var decision = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); - var featureEnabled = decision.variation !== null ? decision.variation.featureEnabled : false; - var variableValue = this._getFeatureVariableValueFromVariation(featureKey, featureEnabled, decision.variation, variable, userId); - - var sourceInfo = {}; - if (decision.decisionSource === DECISION_SOURCES.FEATURE_TEST) { - sourceInfo = { - experimentKey: decision.experiment.key, - variationKey: decision.variation.key, - }; - } - - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { - type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, - userId: userId, - attributes: attributes || {}, - decisionInfo: { - featureKey: featureKey, - featureEnabled: featureEnabled, - source: decision.decisionSource, - variableKey: variableKey, - variableValue: variableValue, - variableType: variable.type, - sourceInfo: sourceInfo, - }, - }); - return variableValue; -}; - -/** - * Helper method to get the non type-casted value for a variable attached to a - * feature flag. Returns appropriate variable value depending on whether there - * was a matching variation, feature was enabled or not or varible was part of the - * available variation or not. Also logs the appropriate message explaining how it - * evaluated the value of the variable. - * - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {boolean} featureEnabled Boolean indicating if feature is enabled or not - * @param {object} variation variation returned by decision service - * @param {object} variable varible whose value is being evaluated - * @param {string} userId ID for the user - * @return {string|null} String value of the variable or null if the config Obj - * is null - */ -Optimizely.prototype._getFeatureVariableValueFromVariation = function(featureKey, featureEnabled, variation, variable, userId) { - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - - var variableValue = variable.defaultValue; - if (variation !== null) { - var value = projectConfig.getVariableValueForVariation(configObj, variable, variation, this.logger); - if (value !== null) { - if (featureEnabled) { - variableValue = value; - this.logger.log( - LOG_LEVEL.INFO, - sprintf( - LOG_MESSAGES.USER_RECEIVED_VARIABLE_VALUE, - MODULE_NAME, - variableValue, - variable.key, - featureKey - ) - ); - } else { - this.logger.log( - LOG_LEVEL.INFO, - sprintf( - LOG_MESSAGES.FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE, - MODULE_NAME, - featureKey, - userId, - variableValue - ) - ); - } - } else { - this.logger.log( - LOG_LEVEL.INFO, - sprintf( - LOG_MESSAGES.VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE, - MODULE_NAME, - variable.key, - variation.key - ) - ); - } - } else { - this.logger.log( - LOG_LEVEL.INFO, - sprintf( - LOG_MESSAGES.USER_RECEIVED_DEFAULT_VARIABLE_VALUE, - MODULE_NAME, - userId, - variable.key, - featureKey - ) - ); - } - - return projectConfig.getTypeCastValue(variableValue, variable.type, this.logger); -} - -/** - * Returns value for the given boolean variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {boolean|null} Boolean value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableBoolean = function(featureKey, variableKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableBoolean')); - return null; - } - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.BOOLEAN, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given double variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {number|null} Number value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableDouble = function(featureKey, variableKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableDouble')); - return null; - } - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.DOUBLE, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given integer variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {number|null} Number value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableInteger = function(featureKey, variableKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableInteger')); - return null; - } - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.INTEGER, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given string variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {string|null} String value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableString = function(featureKey, variableKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableString')); - return null; - } - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.STRING, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given json variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {object|null} Object value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableJSON = function(featureKey, variableKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableJSON')); - return null; - } - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.JSON, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns values for all the variables attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variables are being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {object|null} Object containing all the variables, or null if the - * feature key is invalid - */ -Optimizely.prototype.getAllFeatureVariables = function(featureKey, userId, attributes) { - try { - if (!this.__isValidInstance()) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getAllFeatureVariables')); - return null; - } - - if (!this.__validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { - return null; - } - - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - - var featureFlag = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); - if (!featureFlag) { - return null; - } - - var decision = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); - var featureEnabled = decision.variation !== null ? decision.variation.featureEnabled : false; - var allVariables = {}; - - featureFlag.variables.forEach(function (variable) { - allVariables[variable.key] = this._getFeatureVariableValueFromVariation(featureKey, featureEnabled, decision.variation, variable, userId); - }.bind(this)); - - var sourceInfo = {}; - if (decision.decisionSource === DECISION_SOURCES.FEATURE_TEST) { - sourceInfo = { - experimentKey: decision.experiment.key, - variationKey: decision.variation.key, - }; - } - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { - type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, - userId: userId, - attributes: attributes || {}, - decisionInfo: { - featureKey: featureKey, - featureEnabled: featureEnabled, - source: decision.decisionSource, - variableValues: allVariables, - sourceInfo: sourceInfo, - }, - }); - - return allVariables; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns OptimizelyConfig object containing experiments and features data - * @return {Object} - * - * OptimizelyConfig Object Schema - * { - * 'experimentsMap': { - * 'my-fist-experiment': { - * 'id': '111111', - * 'key': 'my-fist-experiment' - * 'variationsMap': { - * 'variation_1': { - * 'id': '121212', - * 'key': 'variation_1', - * 'variablesMap': { - * 'age': { - * 'id': '222222', - * 'key': 'age', - * 'type': 'integer', - * 'value': '0', - * } - * } - * } - * } - * } - * }, - * 'featuresMap': { - * 'awesome-feature': { - * 'id': '333333', - * 'key': 'awesome-feature', - * 'experimentsMap': Object, - * 'variationsMap': Object, - * } - * } - * } - */ -Optimizely.prototype.getOptimizelyConfig = function() { - try { - var configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return null; - } - return this.projectConfigManager.getOptimizelyConfig(); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Stop background processes belonging to this instance, including: - * - * - Active datafile requests - * - Pending datafile requests - * - Pending event queue flushes - * - * In-flight datafile requests will be aborted. Any events waiting to be sent - * as part of a batched event request will be immediately flushed to the event - * dispatcher. - * - * Returns a Promise that fulfills after all in-flight event dispatcher requests - * (including any final request resulting from flushing the queue as described - * above) are complete. If there are no in-flight event dispatcher requests and - * no queued events waiting to be sent, returns an immediately-fulfilled Promise. - * - * Returned Promises are fulfilled with result objects containing these - * properties: - * - success (boolean): true if the event dispatcher signaled completion of - * all in-flight and final requests, or if there were no - * queued events and no in-flight requests. false if an - * unexpected error was encountered during the close - * process. - * - reason (string=): If success is false, this is a string property with - * an explanatory message. - * - * NOTE: After close is called, this instance is no longer usable - any events - * generated will no longer be sent to the event dispatcher. - * - * @return {Promise} - */ -Optimizely.prototype.close = function() { - try { - var eventProcessorStoppedPromise = this.eventProcessor.stop(); - if (this.__disposeOnUpdate) { - this.__disposeOnUpdate(); - this.__disposeOnUpdate = null; - } - if (this.projectConfigManager) { - this.projectConfigManager.stop(); - } - Object.keys(this.__readyTimeouts).forEach( - function(readyTimeoutId) { - var readyTimeoutRecord = this.__readyTimeouts[readyTimeoutId]; - clearTimeout(readyTimeoutRecord.readyTimeout); - readyTimeoutRecord.onClose(); - }.bind(this) - ); - this.__readyTimeouts = {}; - return eventProcessorStoppedPromise.then( - function() { - return { - success: true, - }; - }, - function(err) { - return { - success: false, - reason: String(err), - }; - } - ); - } catch (err) { - this.logger.log(LOG_LEVEL.ERROR, err.message); - this.errorHandler.handleError(err); - return Promise.resolve({ - success: false, - reason: String(err), - }); - } -}; - -/** - * Returns a Promise that fulfills when this instance is ready to use (meaning - * it has a valid datafile), or has failed to become ready within a period of - * time (configurable by the timeout property of the options argument), or when - * this instance is closed via the close method. - * - * If a valid datafile was provided in the constructor, the returned Promise is - * immediately fulfilled. If an sdkKey was provided, a manager will be used to - * fetch a datafile, and the returned promise will fulfill if that fetch - * succeeds or fails before the timeout. The default timeout is 30 seconds, - * which will be used if no timeout is provided in the argument options object. - * - * The returned Promise is fulfilled with a result object containing these - * properties: - * - success (boolean): True if this instance is ready to use with a valid - * datafile, or false if this instance failed to become - * ready or was closed prior to becoming ready. - * - reason (string=): If success is false, this is a string property with - * an explanatory message. Failure could be due to - * expiration of the timeout, network errors, - * unsuccessful responses, datafile parse errors, - * datafile validation errors, or the instance being - * closed - * @param {Object=} options - * @param {number|undefined} options.timeout - * @return {Promise} - */ -Optimizely.prototype.onReady = function(options) { - var timeout; - if (typeof options === 'object' && options !== null) { - timeout = options.timeout; - } - if (!isSafeInteger(timeout)) { - timeout = DEFAULT_ONREADY_TIMEOUT; - } - - var resolveTimeoutPromise; - var timeoutPromise = new Promise(function(resolve) { - resolveTimeoutPromise = resolve; - }); - - var timeoutId = this.__nextReadyTimeoutId; - this.__nextReadyTimeoutId++; - - var onReadyTimeout = function() { - delete this.__readyTimeouts[timeoutId]; - resolveTimeoutPromise({ - success: false, - reason: sprintf('onReady timeout expired after %s ms', timeout), - }); - }.bind(this); - var readyTimeout = setTimeout(onReadyTimeout, timeout); - var onClose = function() { - resolveTimeoutPromise({ - success: false, - reason: 'Instance closed', - }); - }; - - this.__readyTimeouts[timeoutId] = { - readyTimeout: readyTimeout, - onClose: onClose, - }; - - this.__readyPromise.then( - function() { - clearTimeout(readyTimeout); - delete this.__readyTimeouts[timeoutId]; - resolveTimeoutPromise({ - success: true, - }); - }.bind(this) - ); - - return Promise.race([this.__readyPromise, timeoutPromise]); -}; - -export default Optimizely; diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index c96747596..d4f0f0962 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -23,16 +23,16 @@ import Optimizely from './'; import AudienceEvaluator from '../core/audience_evaluator'; import bluebird from 'bluebird'; import bucketer from '../core/bucketer'; -import projectConfigManager from '../core/project_config/project_config_manager'; +import * as projectConfigManager from '../core/project_config/project_config_manager'; import * as enums from '../utils/enums'; -import eventBuilder from '../core/event_builder/index.js'; +import * as eventBuilder from '../core/event_builder'; import eventDispatcher from '../plugins/event_dispatcher/index.node'; import errorHandler from '../plugins/error_handler'; import * as fns from '../utils/fns'; import logger from '../plugins/logger'; -import decisionService from '../core/decision_service'; +import * as decisionService from '../core/decision_service'; import * as jsonSchemaValidator from '../utils/json_schema_validator'; -import projectConfig from '../core/project_config'; +import * as projectConfig from '../core/project_config'; import testData from '../tests/test_data'; var ERROR_MESSAGES = enums.ERROR_MESSAGES; @@ -57,7 +57,7 @@ describe('lib/optimizely', function() { handleError: sinon.stub(), }; logging.setErrorHandler(globalStubErrorHandler); - ProjectConfigManagerStub = sinon.stub(projectConfigManager, 'ProjectConfigManager').callsFake(function(config) { + ProjectConfigManagerStub = sinon.stub(projectConfigManager, 'createProjectConfigManager').callsFake(function(config) { var currentConfig = config.datafile ? projectConfig.createProjectConfig(config.datafile) : null; return { stop: sinon.stub(), @@ -237,8 +237,8 @@ describe('lib/optimizely', function() { logger: createdLogger, sdkKey: '12345', }); - sinon.assert.calledOnce(projectConfigManager.ProjectConfigManager); - sinon.assert.calledWithExactly(projectConfigManager.ProjectConfigManager, { + sinon.assert.calledOnce(projectConfigManager.createProjectConfigManager); + sinon.assert.calledWithExactly(projectConfigManager.createProjectConfigManager, { datafile: config, datafileOptions: { autoUpdate: true, @@ -7717,7 +7717,7 @@ describe('lib/optimizely', function() { isValidInstance: true, }); optlyInstance.close(); - var fakeManager = projectConfigManager.ProjectConfigManager.getCall(0).returnValue; + var fakeManager = projectConfigManager.createProjectConfigManager.getCall(0).returnValue; sinon.assert.calledOnce(fakeManager.stop); }); @@ -7771,7 +7771,7 @@ describe('lib/optimizely', function() { }); it('fulfills the promise with the value from the project config manager ready promise after the project config manager ready promise is fulfilled', function() { - projectConfigManager.ProjectConfigManager.callsFake(function(config) { + projectConfigManager.createProjectConfigManager.callsFake(function(config) { var currentConfig = config.datafile ? projectConfig.createProjectConfig(config.datafile) : null; return { stop: sinon.stub(), @@ -7879,7 +7879,7 @@ describe('lib/optimizely', function() { }); it('clears the timeout when the project config manager ready promise fulfills', function() { - projectConfigManager.ProjectConfigManager.callsFake(function(config) { + projectConfigManager.createProjectConfigManager.callsFake(function(config) { return { stop: sinon.stub(), getConfig: sinon.stub().returns(null), @@ -7914,7 +7914,7 @@ describe('lib/optimizely', function() { onUpdate: sinon.stub().returns(function() {}), onReady: sinon.stub().returns({ then: function() {} }), }; - projectConfigManager.ProjectConfigManager.returns(fakeProjectConfigManager); + projectConfigManager.createProjectConfigManager.returns(fakeProjectConfigManager); optlyInstance = new Optimizely({ clientEngine: 'node-sdk', diff --git a/packages/optimizely-sdk/lib/optimizely/index.ts b/packages/optimizely-sdk/lib/optimizely/index.ts new file mode 100644 index 000000000..e7292a6e6 --- /dev/null +++ b/packages/optimizely-sdk/lib/optimizely/index.ts @@ -0,0 +1,1359 @@ +/**************************************************************************** + * Copyright 2016-2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import { sprintf, objectValues } from '@optimizely/js-sdk-utils'; +import { LogHandler, ErrorHandler } from '@optimizely/js-sdk-logging'; +import * as eventProcessor from '@optimizely/js-sdk-event-processor'; +import { FeatureFlag, FeatureVariable } from '../core/project_config/entities'; +import { EventDispatcher } from '@optimizely/js-sdk-event-processor'; +import { + UserAttributes, + Variation, + EventTags, + OptimizelyConfig, + LogTierV1EventProcessorConfig +} from '../shared_types'; +import { createProjectConfigManager, ProjectConfigManager } from '../core/project_config/project_config_manager'; +import { createNotificationCenter, NotificationCenter } from '../core/notification_center'; +import { createDecisionService, DecisionService } from '../core/decision_service'; +import { getImpressionEvent, getConversionEvent } from '../core/event_builder'; +import { buildImpressionEvent, buildConversionEvent } from '../core/event_builder/event_helpers'; +import { isSafeInteger } from '../utils/fns' +import { validate } from '../utils/attributes_validator'; +import * as enums from '../utils/enums'; +import * as eventTagsValidator from '../utils/event_tags_validator'; +import * as projectConfig from '../core/project_config'; +import * as userProfileServiceValidator from '../utils/user_profile_service_validator'; +import * as stringValidator from '../utils/string_value_validator'; + +const ERROR_MESSAGES = enums.ERROR_MESSAGES; +const LOG_LEVEL = enums.LOG_LEVEL; +const LOG_MESSAGES = enums.LOG_MESSAGES; +const MODULE_NAME = 'OPTIMIZELY'; +const DECISION_SOURCES = enums.DECISION_SOURCES; +const FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; +const DECISION_NOTIFICATION_TYPES = enums.DECISION_NOTIFICATION_TYPES; +const NOTIFICATION_TYPES = enums.NOTIFICATION_TYPES; + +const DEFAULT_ONREADY_TIMEOUT = 30000; + +/** + * The Optimizely class + * @param {Object} config + * @param {string} config.clientEngine + * @param {string} config.clientVersion + * @param {Object|string} config.datafile + * @param {Object} config.errorHandler + * @param {Object} config.eventDispatcher + * @param {Object} config.logger + * @param {Object} config.userProfileService + * @param {Object} config.eventBatchSize + * @param {Object} config.eventFlushInterval + * @param {string} config.sdkKey + */ +export default class Optimizely { + private __isOptimizelyConfigValid: boolean; + private __disposeOnUpdate: (() => void ) | null; + private __readyPromise: Promise<{ success: boolean; reason?: string }>; + private __readyTimeouts: { [key: string]: {readyTimeout: number; onClose:() => void} }; + private __nextReadyTimeoutId: number; + private clientEngine: string; + private clientVersion: string; + private errorHandler: ErrorHandler; + private eventDispatcher: EventDispatcher; + private logger: LogHandler; + private projectConfigManager: ProjectConfigManager; + private notificationCenter: NotificationCenter; + private decisionService: DecisionService; + private eventProcessor: eventProcessor.EventProcessor; + + + constructor(config: projectConfig.ProjectConfig) { + let clientEngine = config.clientEngine; + if (enums.VALID_CLIENT_ENGINES.indexOf(clientEngine) === -1) { + config.logger.log( + LOG_LEVEL.INFO, + sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, MODULE_NAME, clientEngine) + ); + clientEngine = enums.NODE_CLIENT_ENGINE; + } + + this.clientEngine = clientEngine; + this.clientVersion = config.clientVersion || enums.NODE_CLIENT_VERSION; + this.errorHandler = config.errorHandler; + this.eventDispatcher = config.eventDispatcher; + this.__isOptimizelyConfigValid = config.isValidInstance; + this.logger = config.logger; + + this.projectConfigManager = createProjectConfigManager({ + datafile: config.datafile, + datafileOptions: config.datafileOptions, + jsonSchemaValidator: config.jsonSchemaValidator, + sdkKey: config.sdkKey, + }); + + this.__disposeOnUpdate = this.projectConfigManager.onUpdate( + function(this: Optimizely, configObj: projectConfig.ProjectConfig) { + this.logger.log( + LOG_LEVEL.INFO, + sprintf(LOG_MESSAGES.UPDATED_OPTIMIZELY_CONFIG, MODULE_NAME, configObj.revision, configObj.projectId) + ); + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + }.bind(this) + ); + + const projectConfigManagerReadyPromise = this.projectConfigManager.onReady(); + + let userProfileService = null; + if (config.userProfileService) { + try { + if (userProfileServiceValidator.validate(config.userProfileService)) { + userProfileService = config.userProfileService; + this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.VALID_USER_PROFILE_SERVICE, MODULE_NAME)); + } + } catch (ex) { + this.logger.log(LOG_LEVEL.WARNING, ex.message); + } + } + + this.decisionService = createDecisionService({ + userProfileService: userProfileService, + logger: this.logger, + UNSTABLE_conditionEvaluators: config.UNSTABLE_conditionEvaluators, + }); + + this.notificationCenter = createNotificationCenter({ + logger: this.logger, + errorHandler: this.errorHandler, + }); + + this.eventProcessor = new eventProcessor.LogTierV1EventProcessor({ + dispatcher: this.eventDispatcher, + flushInterval: config.eventFlushInterval, + batchSize: config.eventBatchSize, + maxQueueSize: config.eventMaxQueueSize, // TODO: update event-processor to include maxQueueSize + notificationCenter: this.notificationCenter, + } as LogTierV1EventProcessorConfig); + + const eventProcessorStartedPromise = this.eventProcessor.start(); + + this.__readyPromise = Promise.all([projectConfigManagerReadyPromise, eventProcessorStartedPromise]).then(function(promiseResults) { + // Only return status from project config promise because event processor promise does not return any status. + return promiseResults[0]; + }) + + this.__readyTimeouts = {}; + this.__nextReadyTimeoutId = 0; + } + + /** + * Returns a truthy value if this instance currently has a valid project config + * object, and the initial configuration object that was passed into the + * constructor was also valid. + * @return {boolean} + */ + __isValidInstance(): boolean { + return this.__isOptimizelyConfigValid && !!this.projectConfigManager.getConfig(); + } + + /** + * Buckets visitor and sends impression event to Optimizely. + * @param {string} experimentKey + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string|null} variation key + */ + activate(experimentKey: string, userId: string, attributes: UserAttributes): string | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'activate')); + return null; + } + + if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { + return this.__notActivatingExperiment(experimentKey, userId); + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + + try { + const variationKey = this.getVariation(experimentKey, userId, attributes); + if (variationKey === null) { + return this.__notActivatingExperiment(experimentKey, userId); + } + + // If experiment is not set to 'Running' status, log accordingly and return variation key + if (!projectConfig.isRunning(configObj, experimentKey)) { + const shouldNotDispatchActivateLogMessage = sprintf( + LOG_MESSAGES.SHOULD_NOT_DISPATCH_ACTIVATE, + MODULE_NAME, + experimentKey + ); + this.logger.log(LOG_LEVEL.DEBUG, shouldNotDispatchActivateLogMessage); + return variationKey; + } + + this._sendImpressionEvent(experimentKey, variationKey, userId, attributes); + + return variationKey; + } catch (ex) { + this.logger.log(LOG_LEVEL.ERROR, ex.message); + const failedActivationLogMessage = sprintf( + LOG_MESSAGES.NOT_ACTIVATING_USER, + MODULE_NAME, + userId, + experimentKey + ); + this.logger.log(LOG_LEVEL.INFO, failedActivationLogMessage); + this.errorHandler.handleError(ex); + return null; + } + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Create an impression event and call the event dispatcher's dispatch method to + * send this event to Optimizely. Then use the notification center to trigger + * any notification listeners for the ACTIVATE notification type. + * @param {string} experimentKey Key of experiment that was activated + * @param {string} variationKey Key of variation shown in experiment that was activated + * @param {string} userId ID of user to whom the variation was shown + * @param {UserAttributes} attributes Optional user attributes + */ + _sendImpressionEvent(experimentKey: string, variationKey: string, userId: string, attributes?: UserAttributes): void { + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return; + } + + const impressionEvent = buildImpressionEvent({ + experimentKey: experimentKey, + variationKey: variationKey, + userId: userId, + userAttributes: attributes, + clientEngine: this.clientEngine, + clientVersion: this.clientVersion, + configObj: configObj, + }); + // TODO is it okay to not pass a projectConfig as second argument + this.eventProcessor.process(impressionEvent as eventProcessor.ProcessableEvent); + this.__emitNotificationCenterActivate(experimentKey, variationKey, userId, attributes); + } + + /** + * Emit the ACTIVATE notification on the notificationCenter + * @param {string} experimentKey Key of experiment that was activated + * @param {string} variationKey Key of variation shown in experiment that was activated + * @param {string} userId ID of user to whom the variation was shown + * @param {UserAttributes} attributes Optional user attributes + */ + __emitNotificationCenterActivate(experimentKey: string, variationKey: string, userId: string, attributes?: UserAttributes): void { + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return; + } + + const variationId = projectConfig.getVariationIdFromExperimentAndVariationKey(configObj, experimentKey, variationKey); + const experimentId = projectConfig.getExperimentId(configObj, experimentKey); + const impressionEventOptions = { + attributes: attributes, + clientEngine: this.clientEngine, + clientVersion: this.clientVersion, + configObj: configObj, + experimentId: experimentId, + userId: userId, + variationId: variationId, + logger: this.logger, + }; + const impressionEvent = getImpressionEvent(impressionEventOptions); + const experiment = configObj.experimentKeyMap[experimentKey]; + let variation; + if (experiment && experiment.variationKeyMap) { + variation = experiment.variationKeyMap[variationKey]; + } + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, { + experiment: experiment, + userId: userId, + attributes: attributes, + variation: variation, + logEvent: impressionEvent, + }); + } + + /** + * Sends conversion event to Optimizely. + * @param {string} eventKey + * @param {string} userId + * @param {UserAttributes} attributes + * @param {EventTags} eventTags Values associated with the event. + */ + track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'track')); + return; + } + + if (!this.__validateInputs({ user_id: userId, event_key: eventKey }, attributes, eventTags)) { + return; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return; + } + + if (!projectConfig.eventWithKeyExists(configObj, eventKey)) { + this.logger.log( + LOG_LEVEL.WARNING, + sprintf(enums.LOG_MESSAGES.EVENT_KEY_NOT_FOUND, MODULE_NAME, eventKey) + ); + this.logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.NOT_TRACKING_USER, MODULE_NAME, userId)); + return; + } + + // remove null values from eventTags + eventTags = this.__filterEmptyValues(eventTags as EventTags); + const conversionEvent = buildConversionEvent({ + eventKey: eventKey, + eventTags: eventTags, + userId: userId, + userAttributes: attributes, + clientEngine: this.clientEngine, + clientVersion: this.clientVersion, + configObj: configObj, + }); + this.logger.log(LOG_LEVEL.INFO, sprintf(enums.LOG_MESSAGES.TRACK_EVENT, MODULE_NAME, eventKey, userId)); + // TODO is it okay to not pass a projectConfig as second argument + this.eventProcessor.process(conversionEvent as eventProcessor.ProcessableEvent); + this.__emitNotificationCenterTrack(eventKey, userId, attributes as UserAttributes, eventTags); + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + const failedTrackLogMessage = sprintf(LOG_MESSAGES.NOT_TRACKING_USER, MODULE_NAME, userId); + this.logger.log(LOG_LEVEL.ERROR, failedTrackLogMessage); + } + } + /** + * Send TRACK event to notificationCenter + * @param {string} eventKey + * @param {string} userId + * @param {UserAttributes} attributes + * @param {EventTags} eventTags Values associated with the event. + */ + __emitNotificationCenterTrack(eventKey: string, userId: string, attributes: UserAttributes, eventTags: EventTags): void { + try { + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return; + } + + const conversionEventOptions = { + attributes: attributes, + clientEngine: this.clientEngine, + clientVersion: this.clientVersion, + configObj: configObj, + eventKey: eventKey, + eventTags: eventTags, + logger: this.logger, + userId: userId, + }; + const conversionEvent = getConversionEvent(conversionEventOptions); + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.TRACK, { + eventKey: eventKey, + userId: userId, + attributes: attributes, + eventTags: eventTags, + logEvent: conversionEvent, + }); + } catch (ex) { + this.logger.log(LOG_LEVEL.ERROR, ex.message); + this.errorHandler.handleError(ex); + } + } + + /** + * Gets variation where visitor will be bucketed. + * @param {string} experimentKey + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string|null} variation key + */ + getVariation(experimentKey: string, userId: string, attributes: UserAttributes): string | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getVariation')); + return null; + } + + try { + if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { + return null; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + + const experiment = configObj.experimentKeyMap[experimentKey]; + if (!experiment) { + this.logger.log( + LOG_LEVEL.DEBUG, + sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey) + ); + return null; + } + + const variationKey = this.decisionService.getVariation(configObj, experimentKey, userId, attributes); + const decisionNotificationType = projectConfig.isFeatureExperiment(configObj, experiment.id) + ? DECISION_NOTIFICATION_TYPES.FEATURE_TEST + : DECISION_NOTIFICATION_TYPES.AB_TEST; + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: decisionNotificationType, + userId: userId, + attributes: attributes || {}, + decisionInfo: { + experimentKey: experimentKey, + variationKey: variationKey, + }, + }); + + return variationKey; + } catch (ex) { + this.logger.log(LOG_LEVEL.ERROR, ex.message); + this.errorHandler.handleError(ex); + return null; + } + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Force a user into a variation for a given experiment. + * @param {string} experimentKey + * @param {string} userId + * @param {string|null} variationKey user will be forced into. If null, + * then clear the existing experiment-to-variation mapping. + * @return {boolean} A boolean value that indicates if the set completed successfully. + */ + setForcedVariation(experimentKey: string, userId: string, variationKey: string | null): boolean { + if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId })) { + return false; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return false; + } + + try { + return this.decisionService.setForcedVariation(configObj, experimentKey, userId, variationKey); + } catch (ex) { + this.logger.log(LOG_LEVEL.ERROR, ex.message); + this.errorHandler.handleError(ex); + return false; + } + } + + /** + * Gets the forced variation for a given user and experiment. + * @param {string} experimentKey + * @param {string} userId + * @return {string|null} The forced variation key. + */ + getForcedVariation(experimentKey: string, userId: string): string | null { + if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId })) { + return null; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + + try { + return this.decisionService.getForcedVariation(configObj, experimentKey, userId); + } catch (ex) { + this.logger.log(LOG_LEVEL.ERROR, ex.message); + this.errorHandler.handleError(ex); + return null; + } + } + + /** + * Validate string inputs, user attributes and event tags. + * @param {unknown} stringInputs Map of string keys and associated values + * @param {unknown} userAttributes Optional parameter for user's attributes + * @param {unknown} eventTags Optional parameter for event tags + * @return {boolean} True if inputs are valid + * + */ + __validateInputs( + stringInputs?: unknown, + userAttributes?: unknown, + eventTags?: unknown + ): boolean { + try { + // Null, undefined or non-string user Id is invalid. + if (typeof stringInputs === 'object' && stringInputs !== null) { + if (stringInputs.hasOwnProperty('user_id')) { + const userId = stringInputs['user_id']; + if (typeof userId !== 'string' || userId === null || userId === 'undefined') { + throw new Error(sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, MODULE_NAME, 'user_id')); + } + + delete stringInputs['user_id']; + } + const inputKeys = Object.keys(stringInputs); + for (let index = 0; index < inputKeys.length; index++) { + const key = inputKeys[index]; + if (!stringValidator.validate(stringInputs[key])) { + throw new Error(sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, MODULE_NAME, key)); + } + } + } + if (userAttributes) { + validate(userAttributes); + } + if (eventTags) { + eventTagsValidator.validate(eventTags); + } + return true; + + } catch (ex) { + this.logger.log(LOG_LEVEL.ERROR, ex.message); + this.errorHandler.handleError(ex); + return false; + } + + } + + /** + * Shows failed activation log message and returns null when user is not activated in experiment + * @param {string} experimentKey + * @param {string} userId + * @return {null} + */ + __notActivatingExperiment(experimentKey: string, userId: string): null { + const failedActivationLogMessage = sprintf( + LOG_MESSAGES.NOT_ACTIVATING_USER, + MODULE_NAME, + userId, + experimentKey + ); + this.logger.log(LOG_LEVEL.INFO, failedActivationLogMessage); + return null; + } + + /** + * Filters out attributes/eventTags with null or undefined values + * @param {EventTags} map + * @returns {EventTags} + */ + __filterEmptyValues(map: EventTags): EventTags { + for (const key in map) { + if (map.hasOwnProperty(key) && (map[key] === null || map[key] === undefined)) { + delete map[key]; + } + } + return map; + } + + /** + * Returns true if the feature is enabled for the given user. + * @param {string} featureKey Key of feature which will be checked + * @param {string} userId ID of user which will be checked + * @param {UserAttributes} attributes Optional user attributes + * @return {boolean} true if the feature is enabled for the user, false otherwise + */ + isFeatureEnabled(featureKey: string, userId: string, attributes?: UserAttributes): boolean { + try { + if (!this.__isValidInstance()) { + this.logger.log( + LOG_LEVEL.ERROR, + sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'isFeatureEnabled') + ); + return false; + } + + if (!this.__validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { + return false; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return false; + } + + const feature = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); + if (!feature) { + return false; + } + + let sourceInfo = {}; + let featureEnabled = false; + const decision = this.decisionService.getVariationForFeature(configObj, feature, userId, attributes); + const variation = decision.variation; + + if (variation) { + featureEnabled = variation.featureEnabled; + if ( + decision.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decision.experiment !== null && + decision.variation !== null + ) { + sourceInfo = { + experimentKey: decision.experiment.key, + variationKey: decision.variation.key, + }; + // got a variation from the exp, so we track the impression + this._sendImpressionEvent(decision.experiment.key, decision.variation.key, userId, attributes); + } + } + + if (featureEnabled === true) { + this.logger.log( + LOG_LEVEL.INFO, + sprintf(LOG_MESSAGES.FEATURE_ENABLED_FOR_USER, MODULE_NAME, featureKey, userId) + ); + } else { + this.logger.log( + LOG_LEVEL.INFO, + sprintf(LOG_MESSAGES.FEATURE_NOT_ENABLED_FOR_USER, MODULE_NAME, featureKey, userId) + ); + featureEnabled = false; + } + + const featureInfo = { + featureKey: featureKey, + featureEnabled: featureEnabled, + source: decision.decisionSource, + sourceInfo: sourceInfo, + }; + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: userId, + attributes: attributes || {}, + decisionInfo: featureInfo, + }); + + return featureEnabled; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return false; + } + } + + /** + * Returns an Array containing the keys of all features in the project that are + * enabled for the given user. + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string[]} Array of feature keys (strings) + */ + getEnabledFeatures(userId: string, attributes: UserAttributes): string[] { + try { + const enabledFeatures: string[] = []; + if (!this.__isValidInstance()) { + this.logger.log( + LOG_LEVEL.ERROR, + sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getEnabledFeatures') + ); + return enabledFeatures; + } + + if (!this.__validateInputs({ user_id: userId })) { + return enabledFeatures; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return enabledFeatures; + } + if (configObj.featureKeyMap) { + objectValues(configObj.featureKeyMap).forEach( + function(this: Optimizely, feature: FeatureFlag): void { + if (this.isFeatureEnabled(feature.key, userId, attributes)) { + enabledFeatures.push(feature.key); + } + }.bind(this) + ); + } + + return enabledFeatures; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return []; + } + } + + /** + * Returns dynamically-typed value of the variable attached to the given + * feature flag. Returns null if the feature key or variable key is invalid. + * + * @param {string} featureKey Key of the feature whose variable's + * value is being accessed + * @param {string} variableKey Key of the variable whose value is + * being accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {unknown} Value of the variable cast to the appropriate + * type, or null if the feature key is invalid or + * the variable key is invalid + */ + + getFeatureVariable( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): unknown { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariable')); + return null; + } + return this._getFeatureVariableForType(featureKey, variableKey, null, userId, attributes); + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Helper method to get the value for a variable of a certain type attached to a + * feature flag. Returns null if the feature key is invalid, the variable key is + * invalid, the given variable type does not match the variable's actual type, + * or the variable value cannot be cast to the required type. If the given variable + * type is null, the value of the variable cast to the appropriate type is returned. + * + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string|null} variableType Type of the variable whose value is being + * accessed (must be one of FEATURE_VARIABLE_TYPES + * in lib/utils/enums/index.js), or null to return the + * value of the variable cast to the appropriate type + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {unknown} Value of the variable cast to the appropriate + * type, or null if the feature key is invalid, thevariable key is invalid, or there is + * a mismatch with the type of the variable + */ + _getFeatureVariableForType( + featureKey: string, + variableKey: string, + variableType: string | null, + userId: string, + attributes?: UserAttributes): unknown { + if (!this.__validateInputs({ feature_key: featureKey, variable_key: variableKey, user_id: userId }, attributes)) { + return null; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + + const featureFlag = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); + if (!featureFlag) { + return null; + } + + const variable = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, this.logger); + if (!variable) { + return null; + } + + if (variableType && variable.type !== variableType) { + this.logger.log( + LOG_LEVEL.WARNING, + sprintf(LOG_MESSAGES.VARIABLE_REQUESTED_WITH_WRONG_TYPE, MODULE_NAME, variableType, variable.type) + ); + return null; + } + + const decision = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); + const featureEnabled = decision.variation !== null ? decision.variation.featureEnabled : false; + const variableValue = this._getFeatureVariableValueFromVariation(featureKey, featureEnabled, decision.variation, variable, userId); + let sourceInfo = {}; + if ( + decision.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decision.experiment !== null && + decision.variation !== null + ) { + sourceInfo = { + experimentKey: decision.experiment.key, + variationKey: decision.variation.key, + }; + } + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: userId, + attributes: attributes || {}, + decisionInfo: { + featureKey: featureKey, + featureEnabled: featureEnabled, + source: decision.decisionSource, + variableKey: variableKey, + variableValue: variableValue, + variableType: variable.type, + sourceInfo: sourceInfo, + }, + }); + return variableValue; + } + + /** + * Helper method to get the non type-casted value for a variable attached to a + * feature flag. Returns appropriate variable value depending on whether there + * was a matching variation, feature was enabled or not or varible was part of the + * available variation or not. Also logs the appropriate message explaining how it + * evaluated the value of the variable. + * + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {boolean} featureEnabled Boolean indicating if feature is enabled or not + * @param {Variation} variation variation returned by decision service + * @param {FeatureVariable} variable varible whose value is being evaluated + * @param {string} userId ID for the user + * @return {string|null} String value of the variable or null if the + * config Obj is null + */ + _getFeatureVariableValueFromVariation( + featureKey: string, + featureEnabled: boolean, + variation: Variation | null, + variable: FeatureVariable, + userId: string + ): string | null { + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + + let variableValue = variable.defaultValue; + if (variation !== null) { + const value = projectConfig.getVariableValueForVariation(configObj, variable, variation, this.logger); + if (value !== null) { + if (featureEnabled) { + variableValue = value; + this.logger.log( + LOG_LEVEL.INFO, + sprintf( + LOG_MESSAGES.USER_RECEIVED_VARIABLE_VALUE, + MODULE_NAME, + variableValue, + variable.key, + featureKey + ) + ); + } else { + this.logger.log( + LOG_LEVEL.INFO, + sprintf( + LOG_MESSAGES.FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE, + MODULE_NAME, + featureKey, + userId, + variableValue + ) + ); + } + } else { + this.logger.log( + LOG_LEVEL.INFO, + sprintf( + LOG_MESSAGES.VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE, + MODULE_NAME, + variable.key, + variation.key + ) + ); + } + } else { + this.logger.log( + LOG_LEVEL.INFO, + sprintf( + LOG_MESSAGES.USER_RECEIVED_DEFAULT_VARIABLE_VALUE, + MODULE_NAME, + userId, + variable.key, + featureKey + ) + ); + } + + return projectConfig.getTypeCastValue(variableValue, variable.type, this.logger); + } + + /** + * Returns value for the given boolean variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {boolean|null} Boolean value of the variable, or null if the + * feature key is invalid, the variable key is invalid, + * or there is a mismatch with the type of the variable. + */ + getFeatureVariableBoolean( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): boolean | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableBoolean')); + return null; + } + return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.BOOLEAN, userId, attributes) as boolean | null; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Returns value for the given double variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {number|null} Number value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableDouble( + featureKey:string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): number | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableDouble')); + return null; + } + return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.DOUBLE, userId, attributes) as number | null; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Returns value for the given integer variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {number|null} Number value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableInteger( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): number | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableInteger')); + return null; + } + return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.INTEGER, userId, attributes) as number | null; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Returns value for the given string variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {string|null} String value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableString( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): string | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableString')); + return null; + } + return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.STRING, userId, attributes) as string | null; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Returns value for the given json variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {unknown} Object value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableJSON( + featureKey: string, + variableKey: string, + userId: string, + attributes: UserAttributes + ): unknown { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getFeatureVariableJSON')); + return null; + } + return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.JSON, userId, attributes); + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Returns values for all the variables attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variables are being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {object|null} Object containing all the variables, or null if the + * feature key is invalid + */ + getAllFeatureVariables( + featureKey: string, + userId: string, + attributes?: UserAttributes + ): { [variableKey: string]: unknown } | null { + try { + if (!this.__isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getAllFeatureVariables')); + return null; + } + + if (!this.__validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { + return null; + } + + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + + const featureFlag = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); + if (!featureFlag) { + return null; + } + + const decision = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); + const featureEnabled = decision.variation !== null ? decision.variation.featureEnabled : false; + const allVariables = {}; + + featureFlag.variables.forEach(function (this: Optimizely, variable: FeatureVariable) { + allVariables[variable.key] = this._getFeatureVariableValueFromVariation(featureKey, featureEnabled, decision.variation, variable, userId); + }.bind(this)); + + let sourceInfo = {}; + if (decision.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decision.experiment !== null && + decision.variation !== null + ) { + sourceInfo = { + experimentKey: decision.experiment.key, + variationKey: decision.variation.key, + }; + } + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: userId, + attributes: attributes || {}, + decisionInfo: { + featureKey: featureKey, + featureEnabled: featureEnabled, + source: decision.decisionSource, + variableValues: allVariables, + sourceInfo: sourceInfo, + }, + }); + + return allVariables; + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Returns OptimizelyConfig object containing experiments and features data + * @return {OptimizelyConfig|null} + * + * OptimizelyConfig Object Schema + * { + * 'experimentsMap': { + * 'my-fist-experiment': { + * 'id': '111111', + * 'key': 'my-fist-experiment' + * 'variationsMap': { + * 'variation_1': { + * 'id': '121212', + * 'key': 'variation_1', + * 'variablesMap': { + * 'age': { + * 'id': '222222', + * 'key': 'age', + * 'type': 'integer', + * 'value': '0', + * } + * } + * } + * } + * } + * }, + * 'featuresMap': { + * 'awesome-feature': { + * 'id': '333333', + * 'key': 'awesome-feature', + * 'experimentsMap': Object, + * 'variationsMap': Object, + * } + * } + * } + */ + getOptimizelyConfig(): OptimizelyConfig | null { + try { + const configObj = this.projectConfigManager.getConfig(); + if (!configObj) { + return null; + } + return this.projectConfigManager.getOptimizelyConfig(); + } catch (e) { + this.logger.log(LOG_LEVEL.ERROR, e.message); + this.errorHandler.handleError(e); + return null; + } + } + + /** + * Stop background processes belonging to this instance, including: + * + * - Active datafile requests + * - Pending datafile requests + * - Pending event queue flushes + * + * In-flight datafile requests will be aborted. Any events waiting to be sent + * as part of a batched event request will be immediately flushed to the event + * dispatcher. + * + * Returns a Promise that fulfills after all in-flight event dispatcher requests + * (including any final request resulting from flushing the queue as described + * above) are complete. If there are no in-flight event dispatcher requests and + * no queued events waiting to be sent, returns an immediately-fulfilled Promise. + * + * Returned Promises are fulfilled with result objects containing these + * properties: + * - success (boolean): true if the event dispatcher signaled completion of + * all in-flight and final requests, or if there were no + * queued events and no in-flight requests. false if an + * unexpected error was encountered during the close + * process. + * - reason (string=): If success is false, this is a string property with + * an explanatory message. + * + * NOTE: After close is called, this instance is no longer usable - any events + * generated will no longer be sent to the event dispatcher. + * + * @return {Promise} + */ + close(): Promise<{ success: boolean; reason?: string }> { + try { + const eventProcessorStoppedPromise = this.eventProcessor.stop(); + if (this.__disposeOnUpdate) { + this.__disposeOnUpdate(); + this.__disposeOnUpdate = null; + } + if (this.projectConfigManager) { + this.projectConfigManager.stop(); + } + Object.keys(this.__readyTimeouts).forEach( + function(this: Optimizely, readyTimeoutId: string ) { + const readyTimeoutRecord = this.__readyTimeouts[readyTimeoutId]; + clearTimeout(readyTimeoutRecord.readyTimeout); + readyTimeoutRecord.onClose(); + }.bind(this) + ); + this.__readyTimeouts = {}; + return eventProcessorStoppedPromise.then( + function() { + return { + success: true, + }; + }, + function(err) { + return { + success: false, + reason: String(err), + }; + } + ); + } catch (err) { + this.logger.log(LOG_LEVEL.ERROR, err.message); + this.errorHandler.handleError(err); + return Promise.resolve({ + success: false, + reason: String(err), + }); + } + } + + /** + * Returns a Promise that fulfills when this instance is ready to use (meaning + * it has a valid datafile), or has failed to become ready within a period of + * time (configurable by the timeout property of the options argument), or when + * this instance is closed via the close method. + * + * If a valid datafile was provided in the constructor, the returned Promise is + * immediately fulfilled. If an sdkKey was provided, a manager will be used to + * fetch a datafile, and the returned promise will fulfill if that fetch + * succeeds or fails before the timeout. The default timeout is 30 seconds, + * which will be used if no timeout is provided in the argument options object. + * + * The returned Promise is fulfilled with a result object containing these + * properties: + * - success (boolean): True if this instance is ready to use with a valid + * datafile, or false if this instance failed to become + * ready or was closed prior to becoming ready. + * - reason (string=): If success is false, this is a string property with + * an explanatory message. Failure could be due to + * expiration of the timeout, network errors, + * unsuccessful responses, datafile parse errors, + * datafile validation errors, or the instance being + * closed + * @param {Object=} options + * @param {number|undefined} options.timeout + * @return {Promise} + */ + onReady(options?: { timeout?: number }): Promise<{ success: boolean; reason?: string }> { + let timeoutValue: number | undefined; + if (typeof options === 'object' && options !== null) { + if (options.timeout !== undefined) { + timeoutValue = options.timeout; + } + } + if (!isSafeInteger(timeoutValue)) { + timeoutValue = DEFAULT_ONREADY_TIMEOUT; + } + + let resolveTimeoutPromise: (value?: unknown) => void; + const timeoutPromise = new Promise(function(resolve: (value?: unknown) => void) { + resolveTimeoutPromise = resolve; + }); + + const timeoutId = this.__nextReadyTimeoutId; + this.__nextReadyTimeoutId++; + + const onReadyTimeout = function(this: Optimizely) { + delete this.__readyTimeouts[timeoutId]; + resolveTimeoutPromise({ + success: false, + reason: sprintf('onReady timeout expired after %s ms', timeoutValue), + }); + }.bind(this); + const readyTimeout = setTimeout(onReadyTimeout, timeoutValue); + const onClose = function() { + resolveTimeoutPromise({ + success: false, + reason: 'Instance closed', + }); + }; + + this.__readyTimeouts[timeoutId] = { + readyTimeout: readyTimeout, + onClose: onClose, + }; + + this.__readyPromise.then( + function(this: Optimizely) { + clearTimeout(readyTimeout); + delete this.__readyTimeouts[timeoutId]; + resolveTimeoutPromise({ + success: true, + }); + }.bind(this) + ); + + return Promise.race([this.__readyPromise, timeoutPromise]) as Promise<{ success: boolean; reason?: string | undefined; }>; + } +} diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index 11ac8732e..b95b50020 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -4,9 +4,16 @@ export type UserAttributes = { [name: string]: any; } +export interface VariationVariable { + id: string; + value: string; +} + export interface Variation { id: string; key: string; + featureEnabled: boolean; + variables: VariationVariable[]; } export interface Experiment { @@ -23,6 +30,7 @@ export interface Experiment { // TODO[OASIS-6649]: Don't use object type // eslint-disable-next-line @typescript-eslint/ban-types forcedVariations: object; + variationKeyMap?: {[key: string]: Variation} } // Information about past bucketing decisions for a user. @@ -35,11 +43,38 @@ export interface UserProfile { }; } +export type EventTags = { + [key: string]: string | number | boolean; +}; + +// An event to be submitted to Optimizely, enabling tracking the reach and impact of +// tests and feature rollouts. +export interface Event { + // URL to which to send the HTTP request. + url: string; + // HTTP method with which to send the event. + httpVerb: 'POST'; + // Value to send in the request body, JSON-serialized. + // TODO[OASIS-6649]: Don't use any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params: any; +} + export interface UserProfileService { lookup(userId: string): UserProfile; save(profile: UserProfile): void; } +export interface DatafileOptions { + autoUpdate?: boolean; + updateInterval?: number; + urlTemplate?: string; + datafileAccessToken?: string; +} + +/** + * Optimizely Config Entities + */ export interface OptimizelyExperiment { id: string; key: string; @@ -48,9 +83,6 @@ export interface OptimizelyExperiment { }; } -/** - * Optimizely Config Entities - */ export interface OptimizelyVariable { id: string; key: string; @@ -88,3 +120,14 @@ export interface OptimizelyConfig { revision: string; getDatafile(): string; } + +/** + * Temprorary placement of LogTierV1EventProcessorConfig + */ +export interface LogTierV1EventProcessorConfig { + dispatcher: import ('@optimizely/js-sdk-event-processor').EventDispatcher; + flushInterval?: number; + batchSize?: number; + notificationCenter?: import('./core/notification_center').NotificationCenter; + maxQueueSize?: number; +}