Skip to content

feat: Implement createUserContext for decide API #632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import Optimizely from '../../optimizely';
import projectConfig from '../project_config';
import AudienceEvaluator from '../audience_evaluator';
import errorHandler from '../../plugins/error_handler';
import eventBuilder from '../../core/event_builder/index.js';
import eventDispatcher from '../../plugins/event_dispatcher/index.node';
import * as jsonSchemaValidator from '../../utils/json_schema_validator';
import {
Expand Down Expand Up @@ -951,7 +950,6 @@ describe('lib/core/decision_service', function() {
jsonSchemaValidator: jsonSchemaValidator,
isValidInstance: true,
logger: createdLogger,
eventBuilder: eventBuilder,
eventDispatcher: eventDispatcher,
errorHandler: errorHandler,
});
Expand Down
106 changes: 90 additions & 16 deletions packages/optimizely-sdk/lib/optimizely/index.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import eventProcessor from '../core/event_processor';
import * as logging from '@optimizely/js-sdk-logging';

import Optimizely from './';
import OptimizelyUserContext from '../optimizely_user_context';
import AudienceEvaluator from '../core/audience_evaluator';
import bluebird from 'bluebird';
import bucketer from '../core/bucketer';
import * as projectConfigManager from '../core/project_config/project_config_manager';
import * as enums from '../utils/enums';
import * as eventBuilder from '../core/event_builder';
import eventDispatcher from '../plugins/event_dispatcher/index.node';
import errorHandler from '../plugins/error_handler';
import fns from '../utils/fns';
Expand Down Expand Up @@ -210,7 +210,6 @@ describe('lib/optimizely', function() {
it('should not log an error when sdkKey is provided and datafile is not provided', function() {
new Optimizely({
clientEngine: 'node-sdk',
eventBuilder: eventBuilder,
errorHandler: stubErrorHandler,
eventDispatcher: eventDispatcher,
isValidInstance: true,
Expand Down Expand Up @@ -2626,7 +2625,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -2676,7 +2674,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -2723,7 +2720,6 @@ describe('lib/optimizely', function() {
var optly = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfigWithFeatures(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -4313,6 +4309,95 @@ describe('lib/optimizely', function() {
});
});

describe('decide APIs', function() {
var optlyInstance;
var bucketStub;
var createdLogger = logger.createLogger({
logLevel: LOG_LEVEL.INFO,
logToConsole: false,
});
beforeEach(function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestDecideProjectConfig(),
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
isValidInstance: true,
eventBatchSize: 1,
});

bucketStub = sinon.stub(bucketer, 'bucket');
sinon.stub(errorHandler, 'handleError');
sinon.stub(createdLogger, 'log');
sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c');
});

afterEach(function() {
bucketer.bucket.restore();
errorHandler.handleError.restore();
createdLogger.log.restore();
fns.uuid.restore();
});
describe('#createUserContext', function() {
it('should create OptimizelyUserContext with provided attributes and userId', function() {
var userId = 'testUser1';
var attributes = { test_attribute: 'test_value' };
var user = optlyInstance.createUserContext(userId, attributes);
assert.instanceOf(user, OptimizelyUserContext);
assert.deepEqual(optlyInstance, user.getOptimizely());
assert.deepEqual(attributes, user.getAttributes());
assert.deepEqual(userId, user.getUserId());
});

it('should create OptimizelyUserContext when no attributes provided', function() {
var userId = 'testUser2';
var user = optlyInstance.createUserContext(userId);
assert.instanceOf(user, OptimizelyUserContext);
assert.deepEqual(optlyInstance, user.getOptimizely());
assert.deepEqual({}, user.getAttributes());
assert.deepEqual(userId, user.getUserId());
});

it('should create multiple instances of OptimizelyUserContext', function() {
var userId1 = 'testUser1'
var userId2 = 'testUser2';
var attributes1 = { test_attribute: 'test_value' };
var user1 = optlyInstance.createUserContext(userId1, attributes1);
var user2 = optlyInstance.createUserContext(userId2);
assert.instanceOf(user1, OptimizelyUserContext);
assert.deepEqual(user1.getOptimizely(), optlyInstance);
assert.deepEqual(user1.getAttributes(), attributes1);
assert.deepEqual(user1.getUserId(), userId1);
assert.instanceOf(user2, OptimizelyUserContext);
assert.deepEqual(user2.getOptimizely(), optlyInstance);
assert.deepEqual(user2.getAttributes(), {});
assert.deepEqual(user2.getUserId(), userId2);
});

it('should call the error handler for invalid user ID and return null', function() {
assert.isNull(optlyInstance.createUserContext(null));
sinon.assert.calledOnce(errorHandler.handleError);
var errorMessage = errorHandler.handleError.lastCall.args[0].message;
assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id'));
sinon.assert.calledOnce(createdLogger.log);
var logMessage = createdLogger.log.args[0][1];
assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id'));
});

it('should call the error handler for invalid attributes and return null', function() {
assert.isNull(optlyInstance.createUserContext('user1', 'invalid_attributes'));
sinon.assert.calledOnce(errorHandler.handleError);
var errorMessage = errorHandler.handleError.lastCall.args[0].message;
assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR'));
sinon.assert.calledOnce(createdLogger.log);
var logMessage = createdLogger.log.args[0][1];
assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR'));
});
});
});

//tests separated out from APIs because of mock bucketing
describe('getVariationBucketingIdAttribute', function() {
var optlyInstance;
Expand All @@ -4324,7 +4409,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -4379,7 +4463,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfigWithFeatures(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -4412,7 +4495,6 @@ describe('lib/optimizely', function() {
lasers: 300,
message: 'this is not a valid datafile',
},
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -4936,7 +5018,6 @@ describe('lib/optimizely', function() {
lasers: 300,
message: 'this is not a valid datafile',
},
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -4976,7 +5057,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfigWithFeatures(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -7147,7 +7227,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTypedAudiencesConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -7279,7 +7358,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTypedAudiencesConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -7470,7 +7548,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -7758,7 +7835,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -7789,7 +7865,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
Expand Down Expand Up @@ -8169,7 +8244,6 @@ describe('lib/optimizely', function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
eventBuilder: eventBuilder,
errorHandler: {
handleError: function() {},
},
Expand Down
31 changes: 28 additions & 3 deletions packages/optimizely-sdk/lib/optimizely/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
FeatureVariable,
OptimizelyOptions
} from '../shared_types';
import OptimizelyUserContext from '../optimizely_user_context';
import { createProjectConfigManager, ProjectConfigManager } from '../core/project_config/project_config_manager';
import { createNotificationCenter, NotificationCenter } from '../core/notification_center';
import { createDecisionService, DecisionService, DecisionObj } from '../core/decision_service';
Expand Down Expand Up @@ -55,11 +56,10 @@ const MODULE_NAME = 'OPTIMIZELY';

const DEFAULT_ONREADY_TIMEOUT = 30000;


// TODO: Make feature_key, user_id, variable_key, experiment_key, event_key camelCase
export type InputKey = 'feature_key' | 'user_id' | 'variable_key' | 'experiment_key' | 'event_key' | 'variation_id';
type InputKey = 'feature_key' | 'user_id' | 'variable_key' | 'experiment_key' | 'event_key' | 'variation_id';

export type StringInputs = Partial<Record<InputKey, unknown>>;
type StringInputs = Partial<Record<InputKey, unknown>>;

/**
* The Optimizely class
Expand Down Expand Up @@ -1415,4 +1415,29 @@ export default class Optimizely {

return Promise.race([this.readyPromise, timeoutPromise]);
}

//============ decide ============//

/**
* Creates a context of the user for which decision APIs will be called.
*
* A user context will be created successfully even when the SDK is not fully configured yet, so no
* this.isValidInstance() check is performed here.
*
* @param {string} userId The user ID to be used for bucketing.
* @param {UserAttributes} attributes Optional user attributes.
* @return {OptimizelyUserContext|null} An OptimizelyUserContext associated with this OptimizelyClient or
* null if provided inputs are invalid
*/
createUserContext(userId: string, attributes?: UserAttributes): OptimizelyUserContext | null {
if (!this.validateInputs({ user_id: userId }, attributes)) {
return null;
}

return new OptimizelyUserContext({
optimizely: this,
userId,
attributes
});
}
}
Loading