diff --git a/packages/optimizely-sdk/CHANGELOG.MD b/packages/optimizely-sdk/CHANGELOG.MD index 2fd49e3fb..40b8d7ba3 100644 --- a/packages/optimizely-sdk/CHANGELOG.MD +++ b/packages/optimizely-sdk/CHANGELOG.MD @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Added support sending impression events every time a decision is made ([#599](https://github.com/optimizely/javascript-sdk/pull/599)) ## [4.3.4] - October 8, 2020 diff --git a/packages/optimizely-sdk/lib/core/decision/index.tests.js b/packages/optimizely-sdk/lib/core/decision/index.tests.js new file mode 100644 index 000000000..99edae894 --- /dev/null +++ b/packages/optimizely-sdk/lib/core/decision/index.tests.js @@ -0,0 +1,71 @@ +/** + * 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 { assert } from 'chai'; +import { rolloutDecisionObj, featureTestDecisionObj } from '../../tests/test_data'; +import * as decision from './'; + +describe('lib/core/decision', function() { + describe('getExperimentKey method', function() { + it('should return empty string when experiment is null', function() { + var experimentKey = decision.getExperimentKey(rolloutDecisionObj); + assert.strictEqual(experimentKey, ''); + }); + + it('should return empty string when experiment is not defined', function() { + var experimentKey = decision.getExperimentKey({}); + assert.strictEqual(experimentKey, ''); + }); + + it('should return experiment key when experiment is defined', function() { + var experimentKey = decision.getExperimentKey(featureTestDecisionObj); + assert.strictEqual(experimentKey, 'testing_my_feature'); + }); + }); + + describe('getVariationKey method', function() { + it('should return empty string when variation is null', function() { + var variationKey = decision.getVariationKey(rolloutDecisionObj); + assert.strictEqual(variationKey, ''); + }); + + it('should return empty string when variation is not defined', function() { + var variationKey = decision.getVariationKey({}); + assert.strictEqual(variationKey, ''); + }); + + it('should return variation key when variation is defined', function() { + var variationKey = decision.getVariationKey(featureTestDecisionObj); + assert.strictEqual(variationKey, 'variation'); + }); + }); + + describe('getFeatureEnabledFromVariation method', function() { + it('should return false when variation is null', function() { + var featureEnabled = decision.getFeatureEnabledFromVariation(rolloutDecisionObj); + assert.strictEqual(featureEnabled, false); + }); + + it('should return false when variation is not defined', function() { + var featureEnabled = decision.getFeatureEnabledFromVariation({}); + assert.strictEqual(featureEnabled, false); + }); + + it('should return featureEnabled boolean when variation is defined', function() { + var featureEnabled = decision.getFeatureEnabledFromVariation(featureTestDecisionObj); + assert.strictEqual(featureEnabled, true); + }); + }); +}) diff --git a/packages/optimizely-sdk/lib/core/decision/index.ts b/packages/optimizely-sdk/lib/core/decision/index.ts new file mode 100644 index 000000000..c895eb8ad --- /dev/null +++ b/packages/optimizely-sdk/lib/core/decision/index.ts @@ -0,0 +1,44 @@ +/** + * 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 { DecisionObj } from '../decision_service'; + +/** + * Get experiment key from the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {string} Experiment key or empty string if experiment is null + */ +export function getExperimentKey(decisionObj: DecisionObj): string { + return decisionObj.experiment?.key ?? ''; +} + +/** + * Get variation key from the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {string} Variation key or empty string if variation is null + */ +export function getVariationKey(decisionObj: DecisionObj): string { + return decisionObj.variation?.key ?? ''; +} + +/** + * Get featureEnabled from variation in the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {boolean} featureEnabled boolean or false if variation is null + */ +export function getFeatureEnabledFromVariation(decisionObj: DecisionObj): boolean { + return decisionObj.variation?.featureEnabled ?? false; +} 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 8be719a75..396d75653 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts @@ -53,7 +53,7 @@ export interface DecisionService { * @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 + * @return {DecisionObj} An object with experiment, variation, and decisionSource * properties. If the user was not bucketed into a variation, the variation * property is null. */ @@ -62,7 +62,7 @@ export interface DecisionService { feature: FeatureFlag, userId: string, attributes: unknown - ): Decision; + ): DecisionObj; /** * Removes forced variation for given userId and experimentKey @@ -99,7 +99,7 @@ interface Options { UNSTABLE_conditionEvaluators: unknown; } -interface Decision { +export interface DecisionObj { experiment: Experiment | null; variation: Variation | null; decisionSource: string; diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js index 456c1986d..9ff5dce49 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.js @@ -663,11 +663,11 @@ DecisionService.prototype.getForcedVariation = function(configObj, experimentKey /** * Sets the forced variation for a user in a given experiment - * @param {Object} configObj Object representing project configuration - * @param {string} experimentKey Key for experiment. - * @param {string} userId The user Id. - * @param {string} 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. + * @param {Object} configObj Object representing project configuration + * @param {string} experimentKey Key for experiment. + * @param {string} userId The user Id. + * @param {string|null} 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. */ DecisionService.prototype.setForcedVariation = function(configObj, experimentKey, userId, variationKey) { if (variationKey != null && !stringValidator.validate(variationKey)) { 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 index 3ef0097bf..334dcea13 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/event_helpers.d.ts +++ b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.d.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { ProjectConfig } from '../project_config'; +import { DecisionObj } from '../decision_service'; import { EventTags, UserAttributes } from '../../shared_types'; interface ImpressionConfig { - experimentKey: string; - variationKey: string; + decisionObj: DecisionObj; userId: string; + flagKey: string; userAttributes?: UserAttributes; clientEngine: string; clientVersion: string; @@ -60,6 +61,10 @@ interface ImpressionEvent { id: string; key: string; } | null; + + ruleKey: string, + flagKey: string, + ruleType: string, } interface ConversionConfig { diff --git a/packages/optimizely-sdk/lib/core/event_builder/event_helpers.js b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.js index 65aaf2e1f..6d85191f4 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/event_helpers.js +++ b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.js @@ -19,15 +19,14 @@ import fns from '../../utils/fns'; import projectConfig from '../project_config'; import * as eventTagUtils from '../../utils/event_tag_utils'; import * as attributesValidator from'../../utils/attributes_validator'; +import * as decision from '../decision'; var logger = getLogger('EVENT_BUILDER'); /** * Creates an ImpressionEvent object from decision data * @param {Object} config - * @param {Object} config.configObj - * @param {String} config.experimentKey - * @param {String} config.variationKey + * @param {Object} config.decisionObj * @param {String} config.userId * @param {Object} config.userAttributes * @param {String} config.clientEngine @@ -36,17 +35,29 @@ var logger = getLogger('EVENT_BUILDER'); */ export var buildImpressionEvent = function(config) { var configObj = config.configObj; - var experimentKey = config.experimentKey; - var variationKey = config.variationKey; + var decisionObj = config.decisionObj; var userId = config.userId; + var flagKey = config.flagKey; var userAttributes = config.userAttributes; var clientEngine = config.clientEngine; var clientVersion = config.clientVersion; + var ruleType = decisionObj.decisionSource; + var experimentKey = decision.getExperimentKey(decisionObj); + var variationKey = decision.getVariationKey(decisionObj); - var variationId = projectConfig.getVariationIdFromExperimentAndVariationKey(configObj, experimentKey, variationKey); - var experimentId = projectConfig.getExperimentId(configObj, experimentKey); - var layerId = projectConfig.getLayerId(configObj, experimentId); + let experimentId = null; + let variationId = null; + if (experimentKey !== '' && variationKey !== '') { + variationId = projectConfig.getVariationIdFromExperimentAndVariationKey(configObj, experimentKey, variationKey); + } + if (experimentKey !== '') { + experimentId = projectConfig.getExperimentId(configObj, experimentKey); + } + let layerId = null; + if (experimentId !== null) { + layerId = projectConfig.getLayerId(configObj, experimentId); + } return { type: 'impression', timestamp: fns.currentTimestamp(), @@ -80,6 +91,10 @@ export var buildImpressionEvent = function(config) { id: variationId, key: variationKey, }, + + ruleKey: experimentKey, + flagKey: flagKey, + ruleType: ruleType, }; }; diff --git a/packages/optimizely-sdk/lib/core/event_builder/event_helpers.tests.js b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.tests.js index 613118eeb..fd168a1b0 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/event_helpers.tests.js +++ b/packages/optimizely-sdk/lib/core/event_builder/event_helpers.tests.js @@ -56,6 +56,29 @@ describe('lib/event_builder/event_helpers', function() { describe('buildImpressionEvent', function() { describe('when botFiltering and anonymizeIP are true', function() { it('should build an ImpressionEvent with the correct attributes', function() { + var decision = { + experiment: { + key: 'exp1', + status: 'Running', + forcedVariations: {}, + audienceIds: [], + layerId: 'layer-id', + trafficAllocation: [], + variationKeyMap: { + 'variation': { + key: 'var1', + id: 'var1-id', + } + }, + id: 'exp1-id', + variations: [{ key: 'var1', id: 'var1-id' }], + }, + variation: { + key: 'var1', + id: 'var1-id', + }, + decisionSource: 'experiment', + } projectConfig.getVariationIdFromExperimentAndVariationKey .withArgs(configObj, 'exp1', 'var1') .returns('var1-id'); @@ -66,8 +89,8 @@ describe('lib/event_builder/event_helpers', function() { var result = buildImpressionEvent({ configObj: configObj, - experimentKey: 'exp1', - variationKey: 'var1', + decisionObj: decision, + flagKey: 'flagkey1', userId: 'user1', userAttributes: { plan_type: 'bronze', @@ -113,12 +136,39 @@ describe('lib/event_builder/event_helpers', function() { id: 'var1-id', key: 'var1', }, + + ruleKey: "exp1", + flagKey: 'flagkey1', + ruleType: 'experiment', }); }); }); describe('when botFiltering and anonymizeIP are undefined', function() { it('should create an ImpressionEvent with the correct attributes', function() { + var decision = { + experiment: { + key: 'exp1', + status: 'Running', + forcedVariations: {}, + audienceIds: [], + layerId: '253442', + trafficAllocation: [], + variationKeyMap: { + 'variation': { + key: 'var1', + id: 'var1-id', + } + }, + id: '1237847778', + variations: [{ key: 'var1', id: 'var1-id' }], + }, + variation: { + key: 'var1', + id: 'var1-id', + }, + decisionSource: 'experiment', + } projectConfig.getVariationIdFromExperimentAndVariationKey .withArgs(configObj, 'exp1', 'var1') .returns('var1-id'); @@ -132,8 +182,8 @@ describe('lib/event_builder/event_helpers', function() { var result = buildImpressionEvent({ configObj: configObj, - experimentKey: 'exp1', - variationKey: 'var1', + decisionObj: decision, + flagKey: 'flagkey1', userId: 'user1', userAttributes: { plan_type: 'bronze', @@ -179,6 +229,10 @@ describe('lib/event_builder/event_helpers', function() { id: 'var1-id', key: 'var1', }, + + ruleKey: "exp1", + flagKey: 'flagkey1', + ruleType: 'experiment', }); }); }); diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.d.ts b/packages/optimizely-sdk/lib/core/event_builder/index.d.ts index 9bd23ffd0..b6d06345a 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/index.d.ts +++ b/packages/optimizely-sdk/lib/core/event_builder/index.d.ts @@ -22,9 +22,12 @@ interface ImpressionOptions { clientEngine: string; clientVersion: string; configObj: ProjectConfig; - experimentId: string; + experimentId: string | null; + ruleKey: string; + flagKey: string; + ruleType: string; eventKey?: string; - variationId: string; + variationId: string | null; logger?: LogHandler; userId: string; } diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.js b/packages/optimizely-sdk/lib/core/event_builder/index.js index 6fd90808d..cc66c6840 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/index.js +++ b/packages/optimizely-sdk/lib/core/event_builder/index.js @@ -39,6 +39,7 @@ function getCommonEventParams(options) { var configObj = options.configObj; var anonymize_ip = configObj.anonymizeIP; var botFiltering = configObj.botFiltering; + if (anonymize_ip === null || anonymize_ip === undefined) { anonymize_ip = false; } @@ -92,26 +93,46 @@ function getCommonEventParams(options) { * @param {Object} configObj Object representing project configuration * @param {string} experimentId ID of experiment for which impression needs to be recorded * @param {string} variationId ID for variation which would be presented to user + * @param {string} ruleKey Key of experiment for which impression needs to be recorded + * @param {string} ruleType Type for the decision source + * @param {string} flagKey Key for a feature flag * @return {Object} Impression event params */ -function getImpressionEventParams(configObj, experimentId, variationId) { +function getImpressionEventParams(configObj, experimentId, variationId, ruleKey, ruleType, flagKey) { + let campaignId = null; + if (experimentId !== null) { + campaignId = projectConfig.getLayerId(configObj, experimentId); + } + + let variationKey = projectConfig.getVariationKeyFromId(configObj, variationId); + if (variationKey === null) { + variationKey = ''; + } + var impressionEventParams = { decisions: [ { - campaign_id: projectConfig.getLayerId(configObj, experimentId), + campaign_id: campaignId, experiment_id: experimentId, variation_id: variationId, + metadata: { + flag_key: flagKey, + rule_key: ruleKey, + rule_type: ruleType, + variation_key: variationKey, + } }, ], events: [ { - entity_id: projectConfig.getLayerId(configObj, experimentId), + entity_id: campaignId, timestamp: fns.currentTimestamp(), key: ACTIVATE_EVENT_KEY, uuid: fns.uuid(), }, ], }; + return impressionEventParams; } @@ -163,6 +184,9 @@ function getVisitorSnapshot(configObj, eventKey, eventTags, logger) { * @param {string} options.experimentId Experiment for which impression needs to be recorded * @param {string} options.userId ID for user * @param {string} options.variationId ID for variation which would be presented to user + * @param {string} options.ruleKey Key of an experiment for which impression needs to be recorded + * @param {string} options.ruleType Type for the decision source + * @param {string} options.flagKey Key for a feature flag * @return {Object} Params to be used in impression event logging endpoint call */ export var getImpressionEvent = function(options) { @@ -173,7 +197,14 @@ export var getImpressionEvent = function(options) { var commonParams = getCommonEventParams(options); impressionEvent.url = ENDPOINT; - var impressionEventParams = getImpressionEventParams(options.configObj, options.experimentId, options.variationId); + var impressionEventParams = getImpressionEventParams( + options.configObj, + options.experimentId, + options.variationId, + options.ruleKey, + options.ruleType, + options.flagKey + ); // combine Event params into visitor obj commonParams.visitors[0].snapshots.push(impressionEventParams); diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.tests.js b/packages/optimizely-sdk/lib/core/event_builder/index.tests.js index 37a12013a..2cd4c268f 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/index.tests.js +++ b/packages/optimizely-sdk/lib/core/event_builder/index.tests.js @@ -61,6 +61,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -88,6 +94,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: configObj, experimentId: '111127', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', variationId: '111128', userId: 'testUser', }; @@ -122,6 +131,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -150,6 +165,9 @@ describe('lib/core/event_builder', function() { configObj: configObj, experimentId: '111127', variationId: '111128', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', userId: 'testUser', }; @@ -183,6 +201,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -211,6 +235,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: configObj, experimentId: '111127', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', variationId: '111128', userId: 'testUser', }; @@ -245,6 +272,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -273,6 +306,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: configObj, experimentId: '111127', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', variationId: '111128', userId: 'testUser', }; @@ -300,6 +336,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -328,6 +370,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: configObj, experimentId: '111127', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', variationId: '111128', userId: 'testUser', logger: mockLogger, @@ -370,6 +415,12 @@ describe('lib/core/event_builder', function() { variation_id: '595008', experiment_id: '595010', campaign_id: '595005', + metadata: { + flag_key: 'flagKey2', + rule_key: 'exp2', + rule_type: 'experiment', + variation_key: 'var', + }, }, ], events: [ @@ -398,6 +449,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: v4ConfigObj, experimentId: '595010', + ruleKey: 'exp2', + flagKey: 'flagKey2', + ruleType: 'experiment', variationId: '595008', userId: 'testUser', }; @@ -440,6 +494,12 @@ describe('lib/core/event_builder', function() { variation_id: '595008', experiment_id: '595010', campaign_id: '595005', + metadata: { + flag_key: 'flagKey2', + rule_key: 'exp2', + rule_type: 'experiment', + variation_key: 'var', + }, }, ], events: [ @@ -468,6 +528,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: v4ConfigObj, experimentId: '595010', + ruleKey: 'exp2', + flagKey: 'flagKey2', + ruleType: 'experiment', variationId: '595008', userId: 'testUser', }; @@ -520,6 +583,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -553,6 +622,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: configObj, experimentId: '111127', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', variationId: '111128', userId: 'testUser', }; @@ -599,6 +671,12 @@ describe('lib/core/event_builder', function() { variation_id: '111128', experiment_id: '111127', campaign_id: '4', + metadata: { + flag_key: 'flagKey1', + rule_key: 'exp1', + rule_type: 'experiment', + variation_key: 'control', + }, }, ], events: [ @@ -633,6 +711,9 @@ describe('lib/core/event_builder', function() { clientVersion: packageJSON.version, configObj: configObj, experimentId: '111127', + ruleKey: 'exp1', + flagKey: 'flagKey1', + ruleType: 'experiment', variationId: '111128', userId: 'testUser', }; diff --git a/packages/optimizely-sdk/lib/core/event_processor/index.ts b/packages/optimizely-sdk/lib/core/event_processor/index.ts new file mode 100644 index 000000000..0d2acd2fc --- /dev/null +++ b/packages/optimizely-sdk/lib/core/event_processor/index.ts @@ -0,0 +1,27 @@ +/** + * 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 { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '@optimizely/js-sdk-event-processor'; + +export function createEventProcessor( + ...args: ConstructorParameters +): LogTierV1EventProcessor { + return new LogTierV1EventProcessor(...args); +} + +export { EventProcessor, LocalStoragePendingEventsDispatcher } from '@optimizely/js-sdk-event-processor'; + +export default { createEventProcessor, LocalStoragePendingEventsDispatcher }; 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 585e054f0..78436ed8b 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.d.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.d.ts @@ -19,6 +19,7 @@ import { FeatureFlag, FeatureVariable, Experiment, Variation } from './entities export interface ProjectConfig { revision: string; projectId: string; + sendFlagDecisions?: boolean; experimentKeyMap:{[key: string]: Experiment}; featureKeyMap: { [key: string]: FeatureFlag @@ -41,7 +42,11 @@ export function isRunning(configObj: ProjectConfig, experimentKey: string): bool * @param {string} variationKey The variation key * @return {string} the variation ID */ -export function getVariationIdFromExperimentAndVariationKey(configObj: ProjectConfig, experimentKey: string, variationKey: string): string; +export function getVariationIdFromExperimentAndVariationKey( + configObj: ProjectConfig, + experimentKey: string, + variationKey: string +): string; /** * Get experiment ID for the provided experiment key @@ -131,3 +136,19 @@ export function getVariableValueForVariation( variation: Variation, logger: LogHandler ): string | null; + +/** + * Get the send flag decisions value + * @param {ProjectConfig} projectConfig + * @return {boolean} A boolean value that indicates if we should send flag decisions + */ +export function getSendFlagDecisionsValue(configObj: ProjectConfig): boolean; + +/** + * Get experiment from provided experiment key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentKey Event key for which experiment IDs are to be retrieved + * @return {Experiment} experiment + * @throws If experiment key is not in datafile + */ +export function getExperimentFromKey(configObj: ProjectConfig, experimentKey: string): Experiment; diff --git a/packages/optimizely-sdk/lib/core/project_config/index.js b/packages/optimizely-sdk/lib/core/project_config/index.js index 4442b6e7d..03fd92f0f 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.js +++ b/packages/optimizely-sdk/lib/core/project_config/index.js @@ -279,7 +279,7 @@ export var getExperimentAudienceConditions = function(projectConfig, experimentK * Get variation key given experiment key and variation ID * @param {Object} projectConfig Object representing project configuration * @param {string} variationId ID of the variation - * @return {string} Variation key or null if the variation ID is not found + * @return {string|null} Variation key or null if the variation ID is not found */ export var getVariationKeyFromId = function(projectConfig, variationId) { if (projectConfig.variationIdMap.hasOwnProperty(variationId)) { @@ -603,6 +603,15 @@ export var tryCreatingProjectConfig = function(config) { }; }; +/** + * Get the send flag decisions value + * @param {ProjectConfig} projectConfig + * @return {boolean} A boolean value that indicates if we should send flag decisions + */ +export var getSendFlagDecisionsValue = function(projectConfig) { + return !!projectConfig.sendFlagDecisions; +} + export default { createProjectConfig: createProjectConfig, getExperimentId: getExperimentId, @@ -622,6 +631,7 @@ export default { getVariableForFeature: getVariableForFeature, getVariableValueForVariation: getVariableValueForVariation, getTypeCastValue: getTypeCastValue, + getSendFlagDecisionsValue: getSendFlagDecisionsValue, getAudiencesById: getAudiencesById, eventWithKeyExists: eventWithKeyExists, isFeatureExperiment: isFeatureExperiment, diff --git a/packages/optimizely-sdk/lib/core/project_config/index.tests.js b/packages/optimizely-sdk/lib/core/project_config/index.tests.js index 63a70e1bb..4c90a5fd2 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/index.tests.js @@ -392,6 +392,32 @@ describe('lib/core/project_config', function() { }); }); + describe('#getSendFlagDecisionsValue', function() { + it('should return false when sendFlagDecisions is undefined', function() { + configObj.sendFlagDecisions = undefined; + assert.deepEqual( + projectConfig.getSendFlagDecisionsValue(configObj), + false + ); + }); + + it('should return false when sendFlagDecisions is set to false', function() { + configObj.sendFlagDecisions = false; + assert.deepEqual( + projectConfig.getSendFlagDecisionsValue(configObj), + false + ); + }); + + it('should return true when sendFlagDecisions is set to true', function() { + configObj.sendFlagDecisions = true; + assert.deepEqual( + projectConfig.getSendFlagDecisionsValue(configObj), + true + ); + }); + }); + describe('feature management', function() { var featureManagementLogger = loggerPlugin.createLogger({ logLevel: LOG_LEVEL.INFO }); beforeEach(function() { diff --git a/packages/optimizely-sdk/lib/index.browser.tests.js b/packages/optimizely-sdk/lib/index.browser.tests.js index 5e9e44811..07052a3f6 100644 --- a/packages/optimizely-sdk/lib/index.browser.tests.js +++ b/packages/optimizely-sdk/lib/index.browser.tests.js @@ -16,7 +16,7 @@ import { assert } from 'chai'; import sinon from 'sinon'; import * as logging from '@optimizely/js-sdk-logging'; -import * as eventProcessor from '@optimizely/js-sdk-event-processor'; +import eventProcessor from './core/event_processor'; import Optimizely from './optimizely'; import testData from './tests/test_data'; @@ -391,11 +391,11 @@ describe('javascript-sdk', function() { describe('event processor configuration', function() { var eventProcessorSpy; beforeEach(function() { - eventProcessorSpy = sinon.spy(eventProcessor, 'LogTierV1EventProcessor'); + eventProcessorSpy = sinon.spy(eventProcessor, 'createEventProcessor'); }); afterEach(function() { - eventProcessor.LogTierV1EventProcessor.restore(); + eventProcessor.createEventProcessor.restore(); }); it('should use default event flush interval when none is provided', function() { diff --git a/packages/optimizely-sdk/lib/index.node.tests.js b/packages/optimizely-sdk/lib/index.node.tests.js index 9700c7957..6e3e04242 100644 --- a/packages/optimizely-sdk/lib/index.node.tests.js +++ b/packages/optimizely-sdk/lib/index.node.tests.js @@ -15,7 +15,7 @@ */ import { assert } from 'chai'; import sinon from 'sinon'; -import * as eventProcessor from '@optimizely/js-sdk-event-processor'; +import eventProcessor from './core/event_processor'; import * as enums from './utils/enums'; import Optimizely from './optimizely'; @@ -96,11 +96,11 @@ describe('optimizelyFactory', function() { describe('event processor configuration', function() { var eventProcessorSpy; beforeEach(function() { - eventProcessorSpy = sinon.stub(eventProcessor, 'LogTierV1EventProcessor').callThrough(); + eventProcessorSpy = sinon.stub(eventProcessor, 'createEventProcessor').callThrough(); }); afterEach(function() { - eventProcessor.LogTierV1EventProcessor.restore(); + eventProcessor.createEventProcessor.restore(); }); it('should ignore invalid event flush interval and use default instead', function() { diff --git a/packages/optimizely-sdk/lib/index.react_native.tests.js b/packages/optimizely-sdk/lib/index.react_native.tests.js index a0d87adb1..2d187969a 100644 --- a/packages/optimizely-sdk/lib/index.react_native.tests.js +++ b/packages/optimizely-sdk/lib/index.react_native.tests.js @@ -16,7 +16,7 @@ import { assert } from 'chai'; import sinon from 'sinon'; import * as logging from '@optimizely/js-sdk-logging'; -import * as eventProcessor from '@optimizely/js-sdk-event-processor'; +import eventProcessor from './core/event_processor'; import Optimizely from './optimizely'; import testData from './tests/test_data'; @@ -184,11 +184,11 @@ describe('javascript-sdk/react-native', function() { describe('event processor configuration', function() { var eventProcessorSpy; beforeEach(function() { - eventProcessorSpy = sinon.spy(eventProcessor, 'LogTierV1EventProcessor'); + eventProcessorSpy = sinon.spy(eventProcessor, 'createEventProcessor'); }); afterEach(function() { - eventProcessor.LogTierV1EventProcessor.restore(); + eventProcessor.createEventProcessor.restore(); }); it('should use default event flush interval when none is provided', function() { diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index 404c26834..48c8e5390 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -16,7 +16,7 @@ import { assert } from 'chai'; import sinon from 'sinon'; import { sprintf } from '@optimizely/js-sdk-utils'; -import * as eventProcessor from '@optimizely/js-sdk-event-processor'; +import eventProcessor from '../core/event_processor'; import * as logging from '@optimizely/js-sdk-logging'; import Optimizely from './'; @@ -330,6 +330,12 @@ describe('lib/optimizely', function() { campaign_id: '4', experiment_id: '111127', variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + }, }, ], events: [ @@ -383,6 +389,13 @@ describe('lib/optimizely', function() { campaign_id: '5', experiment_id: '122227', variation_id: '122229', + metadata: { + flag_key: '', + rule_key: 'testExperimentWithAudiences', + rule_type: 'experiment', + variation_key: 'variationWithAudience', + }, + }, ], events: [ @@ -441,6 +454,12 @@ describe('lib/optimizely', function() { campaign_id: '5', experiment_id: '122227', variation_id: '122229', + metadata: { + flag_key: '', + rule_key: 'testExperimentWithAudiences', + rule_type: 'experiment', + variation_key: 'variationWithAudience', + }, }, ], events: [ @@ -504,6 +523,12 @@ describe('lib/optimizely', function() { campaign_id: '5', experiment_id: '122227', variation_id: '122229', + metadata: { + flag_key: '', + rule_key: 'testExperimentWithAudiences', + rule_type: 'experiment', + variation_key: 'variationWithAudience', + }, }, ], events: [ @@ -596,6 +621,12 @@ describe('lib/optimizely', function() { campaign_id: '2', experiment_id: '443', variation_id: '662', + metadata: { + flag_key: '', + rule_key: 'groupExperiment2', + rule_type: 'experiment', + variation_key: 'var2exp2', + }, }, ], events: [ @@ -647,6 +678,12 @@ describe('lib/optimizely', function() { campaign_id: '1', experiment_id: '442', variation_id: '552', + metadata: { + flag_key: '', + rule_key: 'groupExperiment1', + rule_type: 'experiment', + variation_key: 'var2exp1', + }, }, ], events: [ @@ -2298,6 +2335,12 @@ describe('lib/optimizely', function() { campaign_id: '4', experiment_id: '111127', variation_id: '111129', + metadata: { + flag_key: '', + rule_key: "testExperiment", + rule_type: "experiment", + variation_key: "variation", + }, }, ], events: [ @@ -2353,6 +2396,12 @@ describe('lib/optimizely', function() { campaign_id: '4', experiment_id: '111127', variation_id: '111129', + metadata: { + flag_key: '', + rule_key: "testExperiment", + rule_type: "experiment", + variation_key: "variation", + }, }, ], events: [ @@ -4413,6 +4462,12 @@ describe('lib/optimizely', function() { campaign_id: '594093', experiment_id: '594098', variation_id: '594096', + metadata: { + flag_key: 'test_feature_for_experiment', + rule_key: 'testing_my_feature', + rule_type: 'feature-test', + variation_key: 'variation', + }, }, ], events: [ @@ -4626,6 +4681,12 @@ describe('lib/optimizely', function() { campaign_id: '599023', experiment_id: '599028', variation_id: '599027', + metadata: { + flag_key: 'shared_feature', + rule_key: 'test_shared_feature', + rule_type: 'feature-test', + variation_key: 'control', + }, }, ], events: [ @@ -4758,7 +4819,10 @@ describe('lib/optimizely', function() { }); }); - it('returns false and does not dispatch an event', function() { + it('returns false and does not dispatch an event when sendFlagDecisions is not defined', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = undefined; + optlyInstance.projectConfigManager.getConfig.returns(newConfig); var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); assert.strictEqual(result, false); sinon.assert.notCalled(eventDispatcher.dispatchEvent); @@ -4768,6 +4832,82 @@ describe('lib/optimizely', function() { 'OPTIMIZELY: Feature test_feature is not enabled for user user1.' ); }); + + it('returns false and does not dispatch an event when sendFlagDecisions is set to false', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = false; + optlyInstance.projectConfigManager.getConfig.returns(newConfig); + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledWith( + createdLogger.log, + LOG_LEVEL.INFO, + 'OPTIMIZELY: Feature test_feature is not enabled for user user1.' + ); + }); + + it('returns false and dispatch an event when sendFlagDecisions is set to true', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = true; + optlyInstance.projectConfigManager.getConfig.returns(newConfig); + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); + assert.strictEqual(result, false); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '572018', + project_id: '594001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: null, + experiment_id: null, + variation_id: null, + metadata: { + flag_key: 'test_feature', + rule_key: '', + rule_type: 'rollout', + variation_key: '', + }, + }, + ], + events: [ + { + entity_id: null, + timestamp: 1509489766569, + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'user1', + attributes: [ + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + revision: '35', + client_name: 'node-sdk', + client_version: enums.NODE_CLIENT_VERSION, + anonymize_ip: true, + enrich_decisions: true, + }, + }; + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + assert.deepEqual(callArgs[0], expectedImpressionEvent); + }); }); }); @@ -7359,6 +7499,12 @@ describe('lib/optimizely', function() { campaign_id: '4', experiment_id: '111127', variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + }, }, ], events: [ @@ -7447,6 +7593,12 @@ describe('lib/optimizely', function() { campaign_id: '4', experiment_id: '111127', variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + }, }, ], events: [ @@ -7518,6 +7670,12 @@ describe('lib/optimizely', function() { campaign_id: '4', experiment_id: '111127', variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + }, }, ], events: [ @@ -7572,11 +7730,11 @@ describe('lib/optimizely', function() { start: sinon.stub(), stop: sinon.stub(), }; - sinon.stub(eventProcessor, 'LogTierV1EventProcessor').returns(mockEventProcessor); + sinon.stub(eventProcessor, 'createEventProcessor').returns(mockEventProcessor); }); afterEach(function() { - eventProcessor.LogTierV1EventProcessor.restore(); + eventProcessor.createEventProcessor.restore(); }); describe('when the event processor stop method returns a promise that fulfills', function() { @@ -7655,13 +7813,13 @@ describe('lib/optimizely', function() { beforeEach(function() { sinon.stub(errorHandler, 'handleError'); sinon.stub(createdLogger, 'log'); - sinon.spy(eventProcessor, 'LogTierV1EventProcessor'); + sinon.spy(eventProcessor, 'createEventProcessor'); }); afterEach(function() { errorHandler.handleError.restore(); createdLogger.log.restore(); - eventProcessor.LogTierV1EventProcessor.restore(); + eventProcessor.createEventProcessor.restore(); }); it('should instantiate the eventProcessor with the provided event flush interval and event batch size', function() { @@ -7678,7 +7836,7 @@ describe('lib/optimizely', function() { }); sinon.assert.calledWithExactly( - eventProcessor.LogTierV1EventProcessor, + eventProcessor.createEventProcessor, sinon.match({ dispatcher: eventDispatcher, flushInterval: 20000, diff --git a/packages/optimizely-sdk/lib/optimizely/index.ts b/packages/optimizely-sdk/lib/optimizely/index.ts index b7120d32f..5ef887e9e 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.ts +++ b/packages/optimizely-sdk/lib/optimizely/index.ts @@ -28,17 +28,18 @@ import { import { Variation } from '../core/project_config/entities'; 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 { createDecisionService, DecisionService, DecisionObj } from '../core/decision_service'; import { getImpressionEvent, getConversionEvent } from '../core/event_builder'; import { buildImpressionEvent, buildConversionEvent } from '../core/event_builder/event_helpers'; import fns from '../utils/fns' import { validate } from '../utils/attributes_validator'; -import { EventProcessor, LogTierV1EventProcessor } from '@optimizely/js-sdk-event-processor'; +import { EventProcessor, default as eventProcessor } from '../core/event_processor'; 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'; +import * as decision from '../core/decision'; import { ERROR_MESSAGES, LOG_LEVEL, @@ -170,7 +171,7 @@ export default class Optimizely { notificationCenter: this.notificationCenter, } - this.eventProcessor = new LogTierV1EventProcessor(eventProcessorConfig); + this.eventProcessor = eventProcessor.createEventProcessor(eventProcessorConfig); const eventProcessorStartedPromise = this.eventProcessor.start(); @@ -233,7 +234,20 @@ export default class Optimizely { return variationKey; } - this.sendImpressionEvent(experimentKey, variationKey, userId, attributes); + const experiment = projectConfig.getExperimentFromKey(configObj, experimentKey); + const variation = experiment.variationKeyMap[variationKey]; + const decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: enums.DECISION_SOURCES.EXPERIMENT + } + + this.sendImpressionEvent( + decisionObj, + '', + userId, + attributes + ); return variationKey; } catch (ex) { @@ -259,20 +273,25 @@ export default class Optimizely { * 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 {DecisionObj} decisionObj Decision Object + * @param {string} flagKey Key for a feature flag * @param {string} userId ID of user to whom the variation was shown * @param {UserAttributes} attributes Optional user attributes */ - private sendImpressionEvent(experimentKey: string, variationKey: string, userId: string, attributes?: UserAttributes): void { + private sendImpressionEvent( + decisionObj: DecisionObj, + flagKey: string, + userId: string, + attributes?: UserAttributes + ): void { const configObj = this.projectConfigManager.getConfig(); if (!configObj) { return; } const impressionEvent = buildImpressionEvent({ - experimentKey: experimentKey, - variationKey: variationKey, + decisionObj: decisionObj, + flagKey: flagKey, userId: userId, userAttributes: attributes, clientEngine: this.clientEngine, @@ -281,30 +300,48 @@ export default class Optimizely { }); // TODO is it okay to not pass a projectConfig as second argument this.eventProcessor.process(impressionEvent); - this.emitNotificationCenterActivate(experimentKey, variationKey, userId, attributes); + this.emitNotificationCenterActivate(decisionObj, flagKey, 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 + * @param {DecisionObj} decisionObj Decision object + * @param {string} flagKey Key for a feature flag + * @param {string} userId ID of user to whom the variation was shown + * @param {UserAttributes} attributes Optional user attributes */ - private emitNotificationCenterActivate(experimentKey: string, variationKey: string, userId: string, attributes?: UserAttributes): void { + private emitNotificationCenterActivate( + decisionObj: DecisionObj, + flagKey: 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 ruleType = decisionObj.decisionSource; + const experimentKey = decision.getExperimentKey(decisionObj); + const variationKey = decision.getVariationKey(decisionObj); + + let experimentId = null; + let variationId = null; + + if (experimentKey !=='' && variationKey !== '') { + variationId = projectConfig.getVariationIdFromExperimentAndVariationKey(configObj, experimentKey, variationKey); + experimentId = projectConfig.getExperimentId(configObj, experimentKey); + } + const impressionEventOptions = { attributes: attributes, clientEngine: this.clientEngine, clientVersion: this.clientVersion, configObj: configObj, experimentId: experimentId, + ruleKey: experimentKey, + flagKey: flagKey, + ruleType: ruleType, userId: userId, variationId: variationId, logger: this.logger, @@ -312,7 +349,7 @@ export default class Optimizely { const impressionEvent = getImpressionEvent(impressionEventOptions); const experiment = configObj.experimentKeyMap[experimentKey]; let variation; - if (experiment && experiment.variationKeyMap) { + if (experiment && experiment.variationKeyMap && variationKey !== '') { variation = experiment.variationKeyMap[variationKey]; } this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, { @@ -637,24 +674,30 @@ export default class Optimizely { } 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); - } + const decisionObj = this.decisionService.getVariationForFeature(configObj, feature, userId, attributes); + const decisionSource = decisionObj.decisionSource; + const experimentKey = decision.getExperimentKey(decisionObj); + const variationKey = decision.getVariationKey(decisionObj); + + let featureEnabled = decision.getFeatureEnabledFromVariation(decisionObj); + + if (decisionSource === DECISION_SOURCES.FEATURE_TEST) { + sourceInfo = { + experimentKey: experimentKey, + variationKey: variationKey, + }; + } + + if ( + decisionSource === DECISION_SOURCES.FEATURE_TEST || + decisionSource === DECISION_SOURCES.ROLLOUT && projectConfig.getSendFlagDecisionsValue(configObj) + ) { + this.sendImpressionEvent( + decisionObj, + feature.key, + userId, + attributes + ); } if (featureEnabled === true) { @@ -673,7 +716,7 @@ export default class Optimizely { const featureInfo = { featureKey: featureKey, featureEnabled: featureEnabled, - source: decision.decisionSource, + source: decisionObj.decisionSource, sourceInfo: sourceInfo, }; @@ -824,18 +867,18 @@ export default class Optimizely { 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); + const decisionObj = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); + const featureEnabled = decision.getFeatureEnabledFromVariation(decisionObj); + const variableValue = this.getFeatureVariableValueFromVariation(featureKey, featureEnabled, decisionObj.variation, variable, userId); let sourceInfo = {}; if ( - decision.decisionSource === DECISION_SOURCES.FEATURE_TEST && - decision.experiment !== null && - decision.variation !== null + decisionObj.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decisionObj.experiment !== null && + decisionObj.variation !== null ) { sourceInfo = { - experimentKey: decision.experiment.key, - variationKey: decision.variation.key, + experimentKey: decisionObj.experiment.key, + variationKey: decisionObj.variation.key, }; } @@ -846,7 +889,7 @@ export default class Optimizely { decisionInfo: { featureKey: featureKey, featureEnabled: featureEnabled, - source: decision.decisionSource, + source: decisionObj.decisionSource, variableKey: variableKey, variableValue: variableValue, variableType: variable.type, @@ -1138,22 +1181,22 @@ export default class Optimizely { return null; } - const decision = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); - const featureEnabled = decision.variation !== null ? decision.variation.featureEnabled : false; + const decisionObj = this.decisionService.getVariationForFeature(configObj, featureFlag, userId, attributes); + const featureEnabled = decision.getFeatureEnabledFromVariation(decisionObj); const allVariables = {}; featureFlag.variables.forEach((variable: FeatureVariable) => { - allVariables[variable.key] = this.getFeatureVariableValueFromVariation(featureKey, featureEnabled, decision.variation, variable, userId); + allVariables[variable.key] = this.getFeatureVariableValueFromVariation(featureKey, featureEnabled, decisionObj.variation, variable, userId); }); let sourceInfo = {}; - if (decision.decisionSource === DECISION_SOURCES.FEATURE_TEST && - decision.experiment !== null && - decision.variation !== null + if (decisionObj.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decisionObj.experiment !== null && + decisionObj.variation !== null ) { sourceInfo = { - experimentKey: decision.experiment.key, - variationKey: decision.variation.key, + experimentKey: decisionObj.experiment.key, + variationKey: decisionObj.variation.key, }; } this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { @@ -1163,7 +1206,7 @@ export default class Optimizely { decisionInfo: { featureKey: featureKey, featureEnabled: featureEnabled, - source: decision.decisionSource, + source: decisionObj.decisionSource, variableValues: allVariables, sourceInfo: sourceInfo, }, diff --git a/packages/optimizely-sdk/lib/tests/test_data.js b/packages/optimizely-sdk/lib/tests/test_data.js index eeb0f1215..b5633f194 100644 --- a/packages/optimizely-sdk/lib/tests/test_data.js +++ b/packages/optimizely-sdk/lib/tests/test_data.js @@ -2671,6 +2671,62 @@ export var getMutexFeatureTestsConfig = function() { return cloneDeep(mutexFeatureTestsConfig); }; +export var rolloutDecisionObj = { + experiment: null, + variation: null, + decisionSource: 'rollout', +} + +export var featureTestDecisionObj = { + experiment: { + trafficAllocation: [ + { endOfRange: 5000, entityId: '594096' }, + { endOfRange: 10000, entityId: '594097' } + ], + layerId: '594093', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [], + }, + { + key: 'control', + id: '594097', + featureEnabled: true, + variables: [], + }, + ], + status: 'Running', + key: 'testing_my_feature', + id: '594098', + variationKeyMap: { + variation: { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [], + }, + control: { + key: 'control', + id: '594097', + featureEnabled: true, + variables: [], + }, + }, + }, + variation: { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [], + }, + decisionSource: 'feature-test', +} + export default { getTestProjectConfig: getTestProjectConfig, getParsedAudiences: getParsedAudiences, diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index f7f24507a..8eb37926d 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -200,6 +200,7 @@ export const DECISION_NOTIFICATION_TYPES = { export const DECISION_SOURCES = { FEATURE_TEST: 'feature-test', ROLLOUT: 'rollout', + EXPERIMENT: 'experiment', }; export const AUDIENCE_EVALUATION_TYPES = { diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index b26e33cc6..b866afea9 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -291,9 +291,9 @@ } }, "@optimizely/js-sdk-event-processor": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-event-processor/-/js-sdk-event-processor-0.6.0.tgz", - "integrity": "sha512-wNwuyUb563MDxVCHTlDCAGu6lVqHfv3K3ig4QZiR2HPpDo0bT0+zRFuqe4gbor6yfcOe3LDsq4xIxW2TxY2x4g==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-event-processor/-/js-sdk-event-processor-0.7.0.tgz", + "integrity": "sha512-HFXPeM5xCCy2qRrVcYeYcy6JIwaGRVb8GnoDQjEsyB8M5AgNzoOTBFWPstCMFHE5jyjrE+QbhXpRcTBLBvRnEA==", "requires": { "@optimizely/js-sdk-logging": "^0.1.0", "@optimizely/js-sdk-utils": "^0.4.0" diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 7543d3f81..e2f43a4af 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -42,7 +42,7 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.8.0", - "@optimizely/js-sdk-event-processor": "^0.6.0", + "@optimizely/js-sdk-event-processor": "^0.7.0", "@optimizely/js-sdk-logging": "^0.1.0", "@optimizely/js-sdk-utils": "^0.4.0", "json-schema": "^0.2.3",