-
Notifications
You must be signed in to change notification settings - Fork 82
feat: Convert optimizely module to TS #576
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
Conversation
f3c51c4
to
687d620
Compare
d1ecbb0
to
a2cefd7
Compare
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
params: any; | ||
/** | ||
* Temprorary placement of LogTierV1EventProcessorConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally saved LogTierV1EventProcessorConfig
interface in event-processor/src/eventProcessor.ts
, which resulted in travis lint failure due to event-processor package not being set up https://travis-ci.org/github/optimizely/javascript-sdk/builds/731087456, so I moved it here for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, well I expect event-processor to be providing its own type definitions for what it accepts. Not sure why we are re-defining it here. But in any case, event-processor is published and consumed separately from optimizely-sdk, so we can't make changes in it and immediately pick them up here.
}; | ||
|
||
/** | ||
* Determine for given experiment if event is running, which determines whether should be dispatched or not | ||
*/ | ||
export var isRunning = function(projectConfig, experimentKey) { | ||
return this.getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; | ||
return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existed previously this....
resulted in browser test failure. I do not think we needed it here in the first place. LMKWYT @mjc1283
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this change!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I recommended some small refactors to modernize the code
shared_types
is getting pretty big, which could be a sign of poor organization. Are there any types in there that should live in the modules that they're used in?- The
as
type casts on the return value in the feature variable getter methods - there might be a way to make this more elegant using user-defined type guard functions. But no need to spend a lot of time on that rabbit hole right now.
}; | ||
|
||
/** | ||
* Determine for given experiment if event is running, which determines whether should be dispatched or not | ||
*/ | ||
export var isRunning = function(projectConfig, experimentKey) { | ||
return this.getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; | ||
return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this change!
// An event to be submitted to Optimizely, enabling tracking the reach and impact of | ||
// tests and feature rollouts. | ||
export interface Event { | ||
// URL to which to send the HTTP request. | ||
url: string; | ||
// HTTP method with which to send the event. | ||
httpVerb: 'POST'; | ||
// Value to send in the request body, JSON-serialized. | ||
// TODO[OASIS-6649]: Don't use any type | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
params: any; | ||
} | ||
|
||
export interface EventDispatcher { | ||
/** | ||
* @param event | ||
* Event being submitted for eventual dispatch. | ||
* @param callback | ||
* After the event has at least been queued for dispatch, call this function to return | ||
* control back to the Client. | ||
*/ | ||
dispatchEvent: (event: Event, callback: () => void) => void; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't remove these things from our published interface.
interface DatafileOptions { | ||
autoUpdate?: boolean; | ||
updateInterval?: number; | ||
urlTemplate?: string; | ||
datafileAccessToken?: string; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't remove this from our published interface.
const timeoutId = this.__nextReadyTimeoutId; | ||
this.__nextReadyTimeoutId++; | ||
|
||
const onReadyTimeout = function(this: Optimizely) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend arrow function
}.bind(this) | ||
); | ||
|
||
return Promise.race([this.__readyPromise, timeoutPromise]) as Promise<{ success: boolean; reason?: string | undefined; }>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend removing as
type cast
} | ||
|
||
// remove null values from eventTags | ||
eventTags = this.__filterEmptyValues(eventTags as EventTags); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend removing as
type cast
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed it by updating the signature of the __filterEmptyValues
to
__filterEmptyValues(map: EventTags | undefined): EventTags | undefined { ... }
* Helper method to get the non type-casted value for a variable attached to a | ||
* feature flag. Returns appropriate variable value depending on whether there | ||
* was a matching variation, feature was enabled or not or varible was part of the | ||
* available variation or not. Also logs the appropriate message explaining how it | ||
* evaluated the value of the variable. | ||
* | ||
* @param {string} featureKey Key of the feature whose variable's value is | ||
* being accessed | ||
* @param {boolean} featureEnabled Boolean indicating if feature is enabled or not | ||
* @param {Variation} variation variation returned by decision service | ||
* @param {FeatureVariable} variable varible whose value is being evaluated | ||
* @param {string} userId ID for the user | ||
* @return {string|null} String value of the variable or null if the | ||
* config Obj is null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting - looks like at some point, this function was changed, but they didn't update the comment. It is not returning string | null
, it is actually doing the type cast (calling projectConfig.getTypeCastValue
). Probably the right return type is unknown
.
* @returns {T} Variable value of the appropriate type, or | ||
* null if the type cast failed | ||
*/ | ||
export function getTypeCastValue<T>(variableValue: string, type: string, logger: LogHandler): T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking a little closer at getTypeCastValue
, I think this could be improved (not necessary to do in this PR):
- The second argument
type
can be defined as anenum
. - Not sure the generic
T
is helpful here. I could see it helping if we wanted to restrict it to the feature variable types likeT extends string | number | boolean
, but with JSON variables, we're callingJSON.parse
, the return type of which is broader. I would go withunknown
instead of the genericT
.
The biggest part of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good!
@@ -16,7 +16,7 @@ | |||
import { ProjectConfig } from '../project_config'; | |||
import { LogHandler } from '@optimizely/js-sdk-logging'; | |||
import { EventTags, UserAttributes } from '../../shared_types'; | |||
import { Event as EventLoggingEndpoint } from '../../shared_types'; | |||
import { Event as EventLoggingEndpoint } from '@optimizely/optimizely-sdk'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Event
should stay in shared_types I think. We were trying to avoid importing from @optimizely/optimizely-sdk
within the lower-level modules.
Experiment, | ||
Variation | ||
} from '../../shared_types'; | ||
import { Event } from '@optimizely/optimizely-sdk'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Event
should stay in shared_types I think. We were trying to avoid importing from @optimizely/optimizely-sdk
within the lower-level modules.
id: string; | ||
key: string; | ||
status: string; | ||
layerId: string; | ||
variations: Variation[]; | ||
trafficAllocation: Array<{ | ||
entityId: string; | ||
endOfRange: number; | ||
}>; | ||
audienceIds: string[]; | ||
// TODO[OASIS-6649]: Don't use object type | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
forcedVariations: object; | ||
variationKeyMap: {[key: string]: Variation} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove all these properties except id
, key
, and variationKeyMap
. We could add them later when we need them, but it appears only these 3 are needed at the moment (used by the code in the optimizely module).
interface ProjectConfigManagerConfig { | ||
// TODO[OASIS-6649]: Don't use object type | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
datafile: object | string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is an optional string (the doc comment is wrong):
datafile: object | string, | |
datafile?: string, |
@@ -1,5 +1,5 @@ | |||
/** | |||
* Copyright 2018-2020, Optimizely | |||
* Copyright 2020, Optimizely |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to stay 2018-2020. 2020 alone should be used for files created during 2020.
let resolveTimeoutPromise: (value?: { success: boolean; reason?: string | undefined; }) => void; | ||
const timeoutPromise = new Promise( | ||
function(resolve: (value?: { success: boolean; reason?: string | undefined; }) => void) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea for making this less verbose: the eventual value of Promises returned by onReady
can be defined as an interface in shared_types
:
interface OnReadyResult {
success: boolean;
reason?: string;
}
This can be used here, and also in lib/index.d.ts
let resolveTimeoutPromise: (value?: { success: boolean; reason?: string | undefined; }) => void; | |
const timeoutPromise = new Promise( | |
function(resolve: (value?: { success: boolean; reason?: string | undefined; }) => void) { | |
let resolveTimeoutPromise: (value: OnReadyResult) => void; | |
const timeoutPromise = new Promise<OnReadyResult>( | |
(resolve) => { |
resolveTimeoutPromise({ | ||
success: false, | ||
reason: sprintf('onReady timeout expired after %s ms', timeoutValue), | ||
}); | ||
}.bind(this); | ||
}).bind(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling .bind
is not necessary here.
import { sprintf, objectValues } from '@optimizely/js-sdk-utils'; | ||
import { LogHandler, ErrorHandler } from '@optimizely/js-sdk-logging'; | ||
import { FeatureFlag, FeatureVariable } from '../core/project_config/entities'; | ||
import { EventDispatcher } from '@optimizely/js-sdk-event-processor'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EventDispatcher
is already defined in lib/index.d.ts
, let's move it to shared_types
and import it from there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found incompatibility in our EventDispatcher interface with the one defined in event processor.
The callback in dispatchEvent
method is updated from callback: () => void
to callback: (response: { statusCode: number; }) => void
. I noted this change in the log.
* | ||
*/ | ||
private validateInputs( | ||
stringInputs?: unknown, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the callers, it appears we always pass an object. This will simplify the implementation:
stringInputs?: unknown, | |
// Later we could make these camelCase, snake_case is weird | |
stringInputs: Record<'feature_key' | 'user_id' | 'variable_key' | 'experiment_key', unknown> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made some modification to avoid missing properties compiler error and added event_key
:
Partial<Record<'feature_key' | 'user_id' | 'variable_key' | 'experiment_key' | 'event_key', unknown>>
3ccb52f
to
e1ac24a
Compare
e1ac24a
to
7de389e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work!
export enum NOTIFICATION_TYPES { | ||
ACTIVATE = 'ACTIVATE:experiment, user_id,attributes, variation, event', | ||
DECISION = 'DECISION:type, userId, attributes, decisionInfo', | ||
LOG_EVENT = 'LOG_EVENT:logEvent', | ||
OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE', | ||
TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are defined in the utils package - does this work?
export enum NOTIFICATION_TYPES { | |
ACTIVATE = 'ACTIVATE:experiment, user_id,attributes, variation, event', | |
DECISION = 'DECISION:type, userId, attributes, decisionInfo', | |
LOG_EVENT = 'LOG_EVENT:logEvent', | |
OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE', | |
TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', | |
} | |
export type NOTIFICATION_TYPES = import('@optimizely/js-sdk-utils').NOTIFICATION_TYPES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works great!
): boolean { | ||
try { | ||
// Null, undefined or non-string user Id is invalid. | ||
if (typeof stringInputs === 'object' && stringInputs !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this check is not needed anymore - stringInputs
has to be a non-null object.
let resolveTimeoutPromise: (value: OnReadyResult) => void; | ||
const timeoutPromise = new Promise<OnReadyResult>( | ||
(resolve) => { | ||
resolveTimeoutPromise = resolve; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing indentation
Fix comments Update formatting and add optional attribute sign Clean up Address Matt's comments part 1 Address comment part2 Incorporating commenta part 3 Incorporate comments part 4 Updating names of private methods Update private methods Create configObj interface Clean up Add back DatafileOptions Create ProjectConfigManagerConfig interface Fix LogTierV1EventProcessor compiler error without re-defining LogTierV1EventProcessorConfig Fix EventTags to not except boolean incporporating comments part 1 Incoprorate comments part 2 Returned UserAttributes type back go index.d.ts Return all types back to index.d.ts
8da7267
to
f09a046
Compare
Summary
optimizely
module from JS to TS.Test plan