diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 8ddc736eb..975653611 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -20,7 +20,7 @@ import OptimizelyUserContext from '../../optimizely_user_context'; import { bucket } from '../bucketer'; import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data'; import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; -import { BucketerParams, Experiment, OptimizelyDecideOption, UserProfile } from '../../shared_types'; +import { BucketerParams, Experiment, OptimizelyDecideOption, UserAttributes, UserProfile } from '../../shared_types'; import { CONTROL_ATTRIBUTES, DECISION_SOURCES } from '../../utils/enums'; import { getDecisionTestDatafile } from '../../tests/decision_test_datafile'; import { Value } from '../../utils/promise/operation_value'; @@ -344,7 +344,7 @@ describe('DecisionService', () => { const config = createProjectConfig(cloneDeep(testData)); const experiment = config.experimentIdMap['111127']; - const attributes: any = { + const attributes: UserAttributes = { $opt_experiment_bucket_map: { '111127': { variation_id: '111129', // ID of the 'variation' variation @@ -682,7 +682,7 @@ describe('DecisionService', () => { const config = createProjectConfig(cloneDeep(testData)); const experiment = config.experimentIdMap['111127']; - const attributes: any = { + const attributes: UserAttributes = { $opt_experiment_bucket_map: { '111127': { variation_id: '111129', // ID of the 'variation' variation @@ -715,7 +715,7 @@ describe('DecisionService', () => { const config = createProjectConfig(cloneDeep(testData)); const experiment = config.experimentIdMap['111127']; - const attributes: any = { + const attributes: UserAttributes = { $opt_experiment_bucket_map: { '122227': { variation_id: '111129', // ID of the 'variation' variation @@ -748,7 +748,7 @@ describe('DecisionService', () => { const config = createProjectConfig(cloneDeep(testData)); const experiment = config.experimentIdMap['111127']; - const attributes: any = { + const attributes: UserAttributes = { $opt_experiment_bucket_map: { '111127': { variation_id: '111129', // ID of the 'variation' variation @@ -774,7 +774,7 @@ describe('DecisionService', () => { const config = createProjectConfig(cloneDeep(testData)); const experiment = config.experimentIdMap['111127']; - const attributes: any = { + const attributes: UserAttributes = { $opt_experiment_bucket_map: { '111127': { variation_id: '111129', // ID of the 'variation' variation diff --git a/lib/event_processor/event_builder/log_event.ts b/lib/event_processor/event_builder/log_event.ts index c4132567e..8e65d6ba1 100644 --- a/lib/event_processor/event_builder/log_event.ts +++ b/lib/event_processor/event_builder/log_event.ts @@ -15,12 +15,13 @@ */ import { ConversionEvent, ImpressionEvent, UserEvent } from './user_event'; +import { CONTROL_ATTRIBUTES } from '../../utils/enums'; + import { LogEvent } from '../event_dispatcher/event_dispatcher'; import { EventTags } from '../../shared_types'; const ACTIVATE_EVENT_KEY = 'campaign_activated' const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' -const BOT_FILTERING_KEY = '$opt_bot_filtering' export type EventBatch = { account_id: string @@ -204,8 +205,8 @@ function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { if (typeof data.context.botFiltering === 'boolean') { visitor.attributes.push({ - entity_id: BOT_FILTERING_KEY, - key: BOT_FILTERING_KEY, + entity_id: CONTROL_ATTRIBUTES.BOT_FILTERING, + key: CONTROL_ATTRIBUTES.BOT_FILTERING, type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, value: data.context.botFiltering, }) diff --git a/lib/event_processor/event_builder/user_event.ts b/lib/event_processor/event_builder/user_event.ts index e0a91b5ae..c6c6c5446 100644 --- a/lib/event_processor/event_builder/user_event.ts +++ b/lib/event_processor/event_builder/user_event.ts @@ -254,23 +254,30 @@ const buildVisitorAttributes = ( attributes?: UserAttributes, logger?: LoggerFacade ): VisitorAttribute[] => { - const builtAttributes: VisitorAttribute[] = []; + if (!attributes) { + return []; + } + // Omit attribute values that are not supported by the log endpoint. - if (attributes) { - Object.keys(attributes || {}).forEach(function(attributeKey) { - const attributeValue = attributes[attributeKey]; - if (isAttributeValid(attributeKey, attributeValue)) { - const attributeId = getAttributeId(configObj, attributeKey, logger); - if (attributeId) { - builtAttributes.push({ - entityId: attributeId, - key: attributeKey, - value: attributeValue!, - }); - } + const builtAttributes: VisitorAttribute[] = []; + Object.keys(attributes).forEach(function(attributeKey) { + const attributeValue = attributes[attributeKey]; + + if (typeof attributeValue === 'object' || typeof attributeValue === 'undefined') { + return; + } + + if (isAttributeValid(attributeKey, attributeValue)) { + const attributeId = getAttributeId(configObj, attributeKey, logger); + if (attributeId) { + builtAttributes.push({ + entityId: attributeId, + key: attributeKey, + value: attributeValue, + }); } - }); - } + } + }); return builtAttributes; } diff --git a/lib/optimizely_user_context/index.tests.js b/lib/optimizely_user_context/index.tests.js index d8f4cdf09..1ca29ef1a 100644 --- a/lib/optimizely_user_context/index.tests.js +++ b/lib/optimizely_user_context/index.tests.js @@ -20,12 +20,13 @@ import { NOTIFICATION_TYPES } from '../notification_center/type'; import OptimizelyUserContext from './'; import { createNotificationCenter } from '../notification_center'; import Optimizely from '../optimizely'; -import { CONTROL_ATTRIBUTES, LOG_LEVEL } from '../utils/enums'; +import { LOG_LEVEL } from '../utils/enums'; import testData from '../tests/test_data'; import { OptimizelyDecideOption } from '../shared_types'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; import { createProjectConfig } from '../project_config/project_config'; import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; +import { FORCED_DECISION_NULL_RULE_KEY } from './index' import { USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, @@ -449,7 +450,7 @@ describe('lib/optimizely_user_context', function() { assert.deepEqual(decision.userContext.getAttributes(), {}); assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); assert.deepEqual( - decision.userContext.forcedDecisionsMap[featureKey][CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY], + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], { variationKey } ); assert.equal( @@ -475,7 +476,7 @@ describe('lib/optimizely_user_context', function() { assert.deepEqual(decision.userContext.getAttributes(), {}); assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); assert.deepEqual( - decision.userContext.forcedDecisionsMap[featureKey][CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY], + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], { variationKey } ); assert.equal( @@ -509,7 +510,7 @@ describe('lib/optimizely_user_context', function() { assert.deepEqual(decision.userContext.getAttributes(), {}); assert.deepEqual(Object.values(decision.userContext.forcedDecisionsMap).length, 1); assert.deepEqual( - decision.userContext.forcedDecisionsMap[featureKey][CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY], + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], { variationKey } ); assert.equal( @@ -776,7 +777,7 @@ describe('lib/optimizely_user_context', function() { assert.equal(decision.ruleKey, '18322080788'); assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); assert.deepEqual( - decision.userContext.forcedDecisionsMap[featureKey][CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY], + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], { variationKey } ); assert.equal( diff --git a/lib/optimizely_user_context/index.ts b/lib/optimizely_user_context/index.ts index 46fa103f4..75259feb8 100644 --- a/lib/optimizely_user_context/index.ts +++ b/lib/optimizely_user_context/index.ts @@ -23,9 +23,10 @@ import { UserAttributeValue, UserAttributes, } from '../shared_types'; -import { CONTROL_ATTRIBUTES } from '../utils/enums'; import { OptimizelySegmentOption } from '../odp/segment_manager/optimizely_segment_option'; +export const FORCED_DECISION_NULL_RULE_KEY = '$opt_null_rule_key'; + interface OptimizelyUserContextConfig { optimizely: Optimizely; userId: string; @@ -142,7 +143,7 @@ export default class OptimizelyUserContext implements IOptimizelyUserContext { setForcedDecision(context: OptimizelyDecisionContext, decision: OptimizelyForcedDecision): boolean { const flagKey = context.flagKey; - const ruleKey = context.ruleKey ?? CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY; + const ruleKey = context.ruleKey ?? FORCED_DECISION_NULL_RULE_KEY; const variationKey = decision.variationKey; const forcedDecision = { variationKey }; @@ -169,7 +170,7 @@ export default class OptimizelyUserContext implements IOptimizelyUserContext { * @return {boolean} true if the forced decision has been removed successfully */ removeForcedDecision(context: OptimizelyDecisionContext): boolean { - const ruleKey = context.ruleKey ?? CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY; + const ruleKey = context.ruleKey ?? FORCED_DECISION_NULL_RULE_KEY; const flagKey = context.flagKey; let isForcedDecisionRemoved = false; @@ -204,7 +205,7 @@ export default class OptimizelyUserContext implements IOptimizelyUserContext { */ private findForcedDecision(context: OptimizelyDecisionContext): OptimizelyForcedDecision | null { let variationKey; - const validRuleKey = context.ruleKey ?? CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY; + const validRuleKey = context.ruleKey ?? FORCED_DECISION_NULL_RULE_KEY; const flagKey = context.flagKey; if (this.forcedDecisionsMap.hasOwnProperty(context.flagKey)) { diff --git a/lib/shared_types.ts b/lib/shared_types.ts index 4a727af74..0a1582e4a 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -72,9 +72,11 @@ export interface DecisionResponse { readonly reasons: [string, ...any[]][]; } -export type UserAttributeValue = string | number | boolean | null; +export type UserAttributeValue = string | number | boolean | null | undefined | ExperimentBucketMap; export type UserAttributes = { + $opt_bucketing_id?: string; + $opt_experiment_bucket_map?: ExperimentBucketMap; [name: string]: UserAttributeValue; }; diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts index fe4fe9fbe..9d1fea0d3 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -36,7 +36,6 @@ export const CONTROL_ATTRIBUTES = { BUCKETING_ID: '$opt_bucketing_id', STICKY_BUCKETING_KEY: '$opt_experiment_bucket_map', USER_AGENT: '$opt_user_agent', - FORCED_DECISION_NULL_RULE_KEY: '$opt_null_rule_key', }; export const JAVASCRIPT_CLIENT_ENGINE = 'javascript-sdk';