Skip to content

Commit a994bd2

Browse files
committed
Merge branch 'master' into uzair/sdk_options_refactor
2 parents 147b72c + 2387061 commit a994bd2

File tree

17 files changed

+208
-97
lines changed

17 files changed

+208
-97
lines changed

packages/optimizely-sdk/CHANGELOG.MD

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
## [4.5.1] - March 2, 2021
11+
12+
### Bug fixes
13+
- Refactored TypeScript type definitions to have `OptimizelyUserContext` and `OptimizelyDecision` imported from `shared_types` to provide isolation from internal modules ([#655](https://github.com/optimizely/javascript-sdk/pull/655))
14+
15+
## [4.5.0] - February 17, 2021
16+
17+
### New Features
18+
19+
- Introducing a new primary interface for retrieving feature flag status, configuration and associated experiment decisions for users ([#632](https://github.com/optimizely/javascript-sdk/pull/632), [#634](https://github.com/optimizely/javascript-sdk/pull/634), [#635](https://github.com/optimizely/javascript-sdk/pull/635), [#636](https://github.com/optimizely/javascript-sdk/pull/636), [#640](https://github.com/optimizely/javascript-sdk/pull/640), [#642](https://github.com/optimizely/javascript-sdk/pull/642), [#643](https://github.com/optimizely/javascript-sdk/pull/643), [#644](https://github.com/optimizely/javascript-sdk/pull/644), [#647](https://github.com/optimizely/javascript-sdk/pull/647), [#648](https://github.com/optimizely/javascript-sdk/pull/648)). The new `OptimizelyUserContext` class is instantiated with `createUserContext` and exposes the following APIs to get `OptimizelyDecision`:
20+
21+
- setAttribute
22+
- decide
23+
- decideAll
24+
- decideForKeys
25+
- trackEvent
26+
27+
- For details, refer to our documentation page:
28+
- browser version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk).
29+
- Node version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk).
30+
31+
## [4.5.0-beta] - February 10, 2021
32+
33+
### New Features
34+
35+
- Introducing a new primary interface for retrieving feature flag status, configuration and associated experiment decisions for users ([#632](https://github.com/optimizely/javascript-sdk/pull/632), [#634](https://github.com/optimizely/javascript-sdk/pull/634), [#635](https://github.com/optimizely/javascript-sdk/pull/635), [#636](https://github.com/optimizely/javascript-sdk/pull/636), [#640](https://github.com/optimizely/javascript-sdk/pull/640), [#642](https://github.com/optimizely/javascript-sdk/pull/642), [#643](https://github.com/optimizely/javascript-sdk/pull/643), [#644](https://github.com/optimizely/javascript-sdk/pull/644), [#647](https://github.com/optimizely/javascript-sdk/pull/647), [#648](https://github.com/optimizely/javascript-sdk/pull/648)). The new `OptimizelyUserContext` class is instantiated with `createUserContext` and exposes the following APIs to get `OptimizelyDecision`:
36+
37+
- setAttribute
38+
- decide
39+
- decideAll
40+
- decideForKeys
41+
- trackEvent
42+
43+
- For details, refer to our documentation page:
44+
- browser version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk).
45+
- Node version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk).
46+
1047
## [4.4.3] - November 23, 2020
1148

1249
### Bug fixes

packages/optimizely-sdk/lib/core/bucketer/index.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { assert, expect } from 'chai';
1818
import { cloneDeep } from 'lodash';
1919
import { sprintf } from '@optimizely/js-sdk-utils';
2020

21-
import bucketer from './';
21+
import * as bucketer from './';
2222
import {
2323
ERROR_MESSAGES,
2424
LOG_MESSAGES,

packages/optimizely-sdk/lib/core/bucketer/index.js renamed to packages/optimizely-sdk/lib/core/bucketer/index.ts

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,48 @@
1919
*/
2020
import { sprintf } from '@optimizely/js-sdk-utils';
2121
import murmurhash from 'murmurhash';
22+
import { LogHandler } from '@optimizely/js-sdk-logging';
23+
import {
24+
DecisionResponse,
25+
Experiment,
26+
Variation
27+
} from '../../shared_types';
2228

2329
import {
2430
ERROR_MESSAGES,
2531
LOG_LEVEL,
2632
LOG_MESSAGES,
2733
} from '../../utils/enums';
2834

29-
var HASH_SEED = 1;
30-
var MAX_HASH_VALUE = Math.pow(2, 32);
31-
var MAX_TRAFFIC_VALUE = 10000;
32-
var MODULE_NAME = 'BUCKETER';
33-
var RANDOM_POLICY = 'random';
35+
const HASH_SEED = 1;
36+
const MAX_HASH_VALUE = Math.pow(2, 32);
37+
const MAX_TRAFFIC_VALUE = 10000;
38+
const MODULE_NAME = 'BUCKETER';
39+
const RANDOM_POLICY = 'random';
40+
41+
interface TrafficAllocation {
42+
entityId: string;
43+
endOfRange: number;
44+
}
45+
46+
interface Group {
47+
id: string;
48+
policy: string;
49+
trafficAllocation: TrafficAllocation[];
50+
}
51+
52+
interface BucketerParams {
53+
experimentId: string;
54+
experimentKey: string;
55+
userId: string;
56+
trafficAllocationConfig: TrafficAllocation[];
57+
experimentKeyMap: { [key: string]: Experiment };
58+
groupIdMap: { [key: string]: Group };
59+
variationIdMap: { [id: string]: Variation } ;
60+
varationIdMapKey: string;
61+
logger: LogHandler;
62+
bucketingId: string;
63+
}
3464

3565
/**
3666
* Determines ID of variation to be shown for the given input params
@@ -48,18 +78,18 @@ var RANDOM_POLICY = 'random';
4878
* @return {Object} DecisionResponse DecisionResponse containing variation ID that user has been bucketed into,
4979
* null if user is not bucketed into any experiment and the decide reasons.
5080
*/
51-
export var bucket = function(bucketerParams) {
52-
var decideReasons = [];
81+
export const bucket = function(bucketerParams: BucketerParams): DecisionResponse<string | null> {
82+
const decideReasons: string[] = [];
5383
// Check if user is in a random group; if so, check if user is bucketed into a specific experiment
54-
var experiment = bucketerParams.experimentKeyMap[bucketerParams.experimentKey];
55-
var groupId = experiment['groupId'];
84+
const experiment = bucketerParams.experimentKeyMap[bucketerParams.experimentKey];
85+
const groupId = experiment['groupId'];
5686
if (groupId) {
57-
var group = bucketerParams.groupIdMap[groupId];
87+
const group = bucketerParams.groupIdMap[groupId];
5888
if (!group) {
5989
throw new Error(sprintf(ERROR_MESSAGES.INVALID_GROUP_ID, MODULE_NAME, groupId));
6090
}
6191
if (group.policy === RANDOM_POLICY) {
62-
var bucketedExperimentId = this.bucketUserIntoExperiment(
92+
const bucketedExperimentId = bucketUserIntoExperiment(
6393
group,
6494
bucketerParams.bucketingId,
6595
bucketerParams.userId,
@@ -68,7 +98,7 @@ export var bucket = function(bucketerParams) {
6898

6999
// Return if user is not bucketed into any experiment
70100
if (bucketedExperimentId === null) {
71-
var notbucketedInAnyExperimentLogMessage = sprintf(
101+
const notbucketedInAnyExperimentLogMessage = sprintf(
72102
LOG_MESSAGES.USER_NOT_IN_ANY_EXPERIMENT,
73103
MODULE_NAME,
74104
bucketerParams.userId,
@@ -84,7 +114,7 @@ export var bucket = function(bucketerParams) {
84114

85115
// Return if user is bucketed into a different experiment than the one specified
86116
if (bucketedExperimentId !== bucketerParams.experimentId) {
87-
var notBucketedIntoExperimentOfGroupLogMessage = sprintf(
117+
const notBucketedIntoExperimentOfGroupLogMessage = sprintf(
88118
LOG_MESSAGES.USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP,
89119
MODULE_NAME,
90120
bucketerParams.userId,
@@ -100,7 +130,7 @@ export var bucket = function(bucketerParams) {
100130
}
101131

102132
// Continue bucketing if user is bucketed into specified experiment
103-
var bucketedIntoExperimentOfGroupLogMessage = sprintf(
133+
const bucketedIntoExperimentOfGroupLogMessage = sprintf(
104134
LOG_MESSAGES.USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP,
105135
MODULE_NAME,
106136
bucketerParams.userId,
@@ -111,10 +141,10 @@ export var bucket = function(bucketerParams) {
111141
decideReasons.push(bucketedIntoExperimentOfGroupLogMessage);
112142
}
113143
}
114-
var bucketingId = sprintf('%s%s', bucketerParams.bucketingId, bucketerParams.experimentId);
115-
var bucketValue = this._generateBucketValue(bucketingId);
144+
const bucketingId = sprintf('%s%s', bucketerParams.bucketingId, bucketerParams.experimentId);
145+
const bucketValue = _generateBucketValue(bucketingId);
116146

117-
var bucketedUserLogMessage = sprintf(
147+
const bucketedUserLogMessage = sprintf(
118148
LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET,
119149
MODULE_NAME,
120150
bucketValue,
@@ -123,18 +153,19 @@ export var bucket = function(bucketerParams) {
123153
bucketerParams.logger.log(LOG_LEVEL.DEBUG, bucketedUserLogMessage);
124154
decideReasons.push(bucketedUserLogMessage);
125155

126-
var entityId = this._findBucket(bucketValue, bucketerParams.trafficAllocationConfig);
127-
128-
if (!bucketerParams.variationIdMap.hasOwnProperty(entityId)) {
129-
if (entityId) {
130-
var invalidVariationIdLogMessage = sprintf(LOG_MESSAGES.INVALID_VARIATION_ID, MODULE_NAME);
131-
bucketerParams.logger.log(LOG_LEVEL.WARNING, invalidVariationIdLogMessage);
132-
decideReasons.push(invalidVariationIdLogMessage);
156+
const entityId = _findBucket(bucketValue, bucketerParams.trafficAllocationConfig);
157+
if (entityId !== null) {
158+
if (!bucketerParams.variationIdMap[entityId]) {
159+
if (entityId) {
160+
const invalidVariationIdLogMessage = sprintf(LOG_MESSAGES.INVALID_VARIATION_ID, MODULE_NAME);
161+
bucketerParams.logger.log(LOG_LEVEL.WARNING, invalidVariationIdLogMessage);
162+
decideReasons.push(invalidVariationIdLogMessage);
163+
}
164+
return {
165+
result: null,
166+
reasons: decideReasons,
167+
};
133168
}
134-
return {
135-
result: null,
136-
reasons: decideReasons,
137-
};
138169
}
139170

140171
return {
@@ -145,54 +176,63 @@ export var bucket = function(bucketerParams) {
145176

146177
/**
147178
* Returns bucketed experiment ID to compare against experiment user is being called into
148-
* @param {Object} group Group that experiment is in
149-
* @param {string} bucketingId Bucketing ID
150-
* @param {string} userId ID of user to be bucketed into experiment
151-
* @param {Object} logger Logger implementation
152-
* @return {string|null} ID of experiment if user is bucketed into experiment within the group, null otherwise
179+
* @param {Group} group Group that experiment is in
180+
* @param {string} bucketingId Bucketing ID
181+
* @param {string} userId ID of user to be bucketed into experiment
182+
* @param {LogHandler} logger Logger implementation
183+
* @return {string|null} ID of experiment if user is bucketed into experiment within the group, null otherwise
153184
*/
154-
export var bucketUserIntoExperiment = function(group, bucketingId, userId, logger) {
155-
var bucketingKey = sprintf('%s%s', bucketingId, group.id);
156-
var bucketValue = this._generateBucketValue(bucketingKey);
185+
export const bucketUserIntoExperiment = function(
186+
group: Group,
187+
bucketingId: string,
188+
userId: string,
189+
logger: LogHandler
190+
): string | null {
191+
const bucketingKey = sprintf('%s%s', bucketingId, group.id);
192+
const bucketValue = _generateBucketValue(bucketingKey);
157193
logger.log(
158194
LOG_LEVEL.DEBUG,
159195
sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, MODULE_NAME, bucketValue, userId)
160196
);
161-
var trafficAllocationConfig = group.trafficAllocation;
162-
var bucketedExperimentId = this._findBucket(bucketValue, trafficAllocationConfig);
197+
const trafficAllocationConfig = group.trafficAllocation;
198+
const bucketedExperimentId = _findBucket(bucketValue, trafficAllocationConfig);
163199
return bucketedExperimentId;
164200
};
165201

166202
/**
167203
* Returns entity ID associated with bucket value
168-
* @param {string} bucketValue
169-
* @param {Object[]} trafficAllocationConfig
170-
* @param {number} trafficAllocationConfig[].endOfRange
171-
* @param {number} trafficAllocationConfig[].entityId
172-
* @return {string|null} Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
204+
* @param {number} bucketValue
205+
* @param {TrafficAllocation[]} trafficAllocationConfig
206+
* @param {number} trafficAllocationConfig[].endOfRange
207+
* @param {string} trafficAllocationConfig[].entityId
208+
* @return {string|null} Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
173209
*/
174-
export var _findBucket = function(bucketValue, trafficAllocationConfig) {
175-
for (var i = 0; i < trafficAllocationConfig.length; i++) {
210+
export const _findBucket = function(
211+
bucketValue: number,
212+
trafficAllocationConfig: TrafficAllocation[]
213+
): string | null {
214+
for (let i = 0; i < trafficAllocationConfig.length; i++) {
176215
if (bucketValue < trafficAllocationConfig[i].endOfRange) {
177216
return trafficAllocationConfig[i].entityId;
178217
}
179218
}
219+
180220
return null;
181221
};
182222

183223
/**
184224
* Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE)
185-
* @param {string} bucketingKey String value for bucketing
186-
* @return {string} the generated bucket value
187-
* @throws If bucketing value is not a valid string
225+
* @param {string} bucketingKey String value for bucketing
226+
* @return {number} The generated bucket value
227+
* @throws If bucketing value is not a valid string
188228
*/
189-
export var _generateBucketValue = function(bucketingKey) {
229+
export const _generateBucketValue = function(bucketingKey: string): number {
190230
try {
191231
// NOTE: the mmh library already does cast the hash value as an unsigned 32bit int
192232
// https://github.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115
193-
var hashValue = murmurhash.v3(bucketingKey, HASH_SEED);
194-
var ratio = hashValue / MAX_HASH_VALUE;
195-
return parseInt(ratio * MAX_TRAFFIC_VALUE, 10);
233+
const hashValue = murmurhash.v3(bucketingKey, HASH_SEED);
234+
const ratio = hashValue / MAX_HASH_VALUE;
235+
return Math.floor(ratio * MAX_TRAFFIC_VALUE);
196236
} catch (ex) {
197237
throw new Error(sprintf(ERROR_MESSAGES.INVALID_BUCKETING_ID, MODULE_NAME, bucketingKey, ex.message));
198238
}
@@ -201,6 +241,4 @@ export var _generateBucketValue = function(bucketingKey) {
201241
export default {
202242
bucket: bucket,
203243
bucketUserIntoExperiment: bucketUserIntoExperiment,
204-
_findBucket: _findBucket,
205-
_generateBucketValue: _generateBucketValue,
206244
};

packages/optimizely-sdk/lib/core/decision_service/index.d.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,10 @@ import {
2020
UserProfileService,
2121
FeatureFlag,
2222
Experiment,
23-
Variation
23+
Variation,
24+
DecisionResponse
2425
} from '../../shared_types';
2526

26-
interface DecisionResponse<T> {
27-
readonly result: T;
28-
readonly reasons: string[];
29-
}
30-
3127
/**
3228
* Creates an instance of the DecisionService.
3329
* @param {Options} options Configuration options

packages/optimizely-sdk/lib/index.browser.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ describe('javascript-sdk', function() {
148148
optlyInstance.onReady().catch(function() {});
149149

150150
assert.instanceOf(optlyInstance, Optimizely);
151-
assert.equal(optlyInstance.clientVersion, '4.4.3');
151+
assert.equal(optlyInstance.clientVersion, '4.5.1');
152152
});
153153

154154
it('should set the JavaScript client engine and version', function() {

packages/optimizely-sdk/lib/index.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,33 @@ declare module '@optimizely/optimizely-sdk' {
4343

4444
export type OptimizelyFeature = import('./shared_types').OptimizelyFeature;
4545

46-
export type EventTags = import ('./shared_types').EventTags;
46+
export type EventTags = import('./shared_types').EventTags;
4747

48-
export type Event = import ('./shared_types').Event;
48+
export type Event = import('./shared_types').Event;
4949

50-
export type EventDispatcher = import ('./shared_types').EventDispatcher;
50+
export type EventDispatcher = import('./shared_types').EventDispatcher;
5151

52-
export type DatafileOptions = import ('./shared_types').DatafileOptions;
52+
export type DatafileOptions = import('./shared_types').DatafileOptions;
5353

5454
export type Config = import ('./shared_types').Config;
5555

5656
export type SDKOptions = Config;
5757

58-
export type OptimizelyOptions = import ('./shared_types').OptimizelyOptions;
58+
export type OptimizelyOptions = import('./shared_types').OptimizelyOptions;
5959

6060
export type UserProfileService = import('./shared_types').UserProfileService;
6161

6262
export type UserProfile = import('./shared_types').UserProfile;
6363

6464
export type ListenerPayload = import('./shared_types').ListenerPayload;
6565

66-
export type OptimizelyUserContext = import('./optimizely_user_context').default;
66+
export type OptimizelyDecision = import('./shared_types').OptimizelyDecision;
6767

68-
export type OptimizelyDecision = import('./optimizely_decision').OptimizelyDecision;
68+
export type OptimizelyUserContext = import('./shared_types').OptimizelyUserContext;
6969

7070
export enum OptimizelyDecideOption {
7171
DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT',
72-
ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY',
72+
ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY',
7373
IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE',
7474
INCLUDE_REASONS = 'INCLUDE_REASONS',
7575
EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES'

packages/optimizely-sdk/lib/index.node.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('optimizelyFactory', function() {
9090
optlyInstance.onReady().catch(function() {});
9191

9292
assert.instanceOf(optlyInstance, Optimizely);
93-
assert.equal(optlyInstance.clientVersion, '4.4.3');
93+
assert.equal(optlyInstance.clientVersion, '4.5.1');
9494
});
9595

9696
describe('event processor configuration', function() {

packages/optimizely-sdk/lib/index.react_native.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('javascript-sdk/react-native', function() {
8989
optlyInstance.onReady().catch(function() {});
9090

9191
assert.instanceOf(optlyInstance, Optimizely);
92-
assert.equal(optlyInstance.clientVersion, '4.4.3');
92+
assert.equal(optlyInstance.clientVersion, '4.5.1');
9393
});
9494

9595
it('should set the Javascript client engine and version', function() {

packages/optimizely-sdk/lib/optimizely/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import {
2626
FeatureFlag,
2727
FeatureVariable,
2828
OptimizelyOptions,
29-
OptimizelyDecideOption
29+
OptimizelyDecideOption,
30+
OptimizelyDecision
3031
} from '../shared_types';
31-
import { OptimizelyDecision, newErrorDecision } from '../optimizely_decision';
32+
import { newErrorDecision } from '../optimizely_decision';
3233
import OptimizelyUserContext from '../optimizely_user_context';
3334
import { createProjectConfigManager, ProjectConfigManager } from '../core/project_config/project_config_manager';
3435
import { createNotificationCenter, NotificationCenter } from '../core/notification_center';

0 commit comments

Comments
 (0)