Skip to content

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

Merged
merged 9 commits into from
Oct 5, 2020

Conversation

yavorona
Copy link
Contributor

Summary

  • Convert optimizely module from JS to TS.

Test plan

  • Existing unit tests and local bundle

@yavorona yavorona added the WIP label Sep 25, 2020
@yavorona yavorona requested a review from a team as a code owner September 25, 2020 06:06
@yavorona yavorona self-assigned this Sep 25, 2020
@yavorona yavorona removed the WIP label Sep 26, 2020
@coveralls
Copy link

coveralls commented Sep 26, 2020

Coverage Status

Coverage decreased (-0.008%) to 96.63% when pulling 01a9e5d on pnguen/optimizely-module-to-ts into 405bdd0 on master.

@yavorona yavorona force-pushed the pnguen/optimizely-module-to-ts branch 2 times, most recently from f3c51c4 to 687d620 Compare September 28, 2020 19:46
@yavorona yavorona removed their assignment Sep 28, 2020
@yavorona yavorona force-pushed the pnguen/optimizely-module-to-ts branch from d1ecbb0 to a2cefd7 Compare September 28, 2020 20:39
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any;
/**
* Temprorary placement of LogTierV1EventProcessorConfig
Copy link
Contributor Author

@yavorona yavorona Sep 28, 2020

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

Copy link
Contributor

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;
Copy link
Contributor Author

@yavorona yavorona Sep 28, 2020

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

Copy link
Contributor

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!

Copy link
Contributor

@mjc1283 mjc1283 left a 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;
Copy link
Contributor

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!

Comment on lines -139 to -166
// 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;
}

Copy link
Contributor

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.

Comment on lines 33 to 43
interface DatafileOptions {
autoUpdate?: boolean;
updateInterval?: number;
urlTemplate?: string;
datafileAccessToken?: string;
}

Copy link
Contributor

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) {
Copy link
Contributor

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; }>;
Copy link
Contributor

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);
Copy link
Contributor

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

Copy link
Contributor Author

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 { ... }

Comment on lines 839 to 876
* 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
Copy link
Contributor

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;
Copy link
Contributor

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 argumenttype can be defined as an enum.
  • Not sure the generic T is helpful here. I could see it helping if we wanted to restrict it to the feature variable types like T extends string | number | boolean, but with JSON variables, we're calling JSON.parse, the return type of which is broader. I would go with unknown instead of the generic T.

@yavorona
Copy link
Contributor Author

yavorona commented Sep 30, 2020

  • 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.

The biggest part of the shared_files contains Optimizely Config Entities, which originally lived in our published index.d.ts file. If I keep it there, we would not be able to access it in our optimizely module where is it being used. Same happens if I move Optimizely Config Entities to project_config_manager, where it is also being used. @mjc1283 LMK if you think there is a better place for them to live.

I haven’t found a good way to avoid using as type casts in our feature variable getters, but I will spend more time to think about it!

Copy link
Contributor

@mjc1283 mjc1283 left a 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';
Copy link
Contributor

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';
Copy link
Contributor

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.

Comment on lines 49 to 51
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}
Copy link
Contributor

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,
Copy link
Contributor

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):

Suggested change
datafile: object | string,
datafile?: string,

@@ -1,5 +1,5 @@
/**
* Copyright 2018-2020, Optimizely
* Copyright 2020, Optimizely
Copy link
Contributor

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.

Comment on lines 1343 to 1345
let resolveTimeoutPromise: (value?: { success: boolean; reason?: string | undefined; }) => void;
const timeoutPromise = new Promise(
function(resolve: (value?: { success: boolean; reason?: string | undefined; }) => void) {
Copy link
Contributor

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

Suggested change
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);
Copy link
Contributor

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';
Copy link
Contributor

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.

Copy link
Contributor Author

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,
Copy link
Contributor

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:

Suggested change
stringInputs?: unknown,
// Later we could make these camelCase, snake_case is weird
stringInputs: Record<'feature_key' | 'user_id' | 'variable_key' | 'experiment_key', unknown>

Copy link
Contributor Author

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>>

Copy link
Contributor

@mjc1283 mjc1283 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

Comment on lines 21 to 27
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',
}
Copy link
Contributor

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?

Suggested change
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;

Copy link
Contributor Author

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) {
Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing indentation

uzair-folio3 and others added 6 commits October 5, 2020 12:23
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
@yavorona yavorona force-pushed the pnguen/optimizely-module-to-ts branch from 8da7267 to f09a046 Compare October 5, 2020 19:35
@yavorona yavorona merged commit c79e772 into master Oct 5, 2020
@yavorona yavorona deleted the pnguen/optimizely-module-to-ts branch October 5, 2020 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants