Skip to content
Open
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
5 changes: 3 additions & 2 deletions clientlibs/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@
"docs:markdown": "typedoc --options typedoc.json",
"test": "jest",
"test:coverage": "jest --coverage",
"quicktest": "([ -d dist ] || yarn build) && ts-node quickTest.ts",
"quicktest:adaptive": "([ -d dist ] || yarn build) && ts-node quickTestAdaptive.ts"
"quicktest": "([ -f dist/node/index.js ] || yarn build) && ts-node quickTest.ts",
"quicktest:lite": "([ -f dist/lite/index.js ] || yarn build) && ts-node quickTest.ts lite",
"quicktest:adaptive": "([ -f dist/node/index.js ] || yarn build) && ts-node quickTestAdaptive.ts"
},
"keywords": [],
"author": "",
Expand Down
97 changes: 58 additions & 39 deletions clientlibs/js/quickTest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
// to run: npx ts-node clientlibs/js/quickTest.ts
// to run against the full (axios-bundled) build: npx ts-node clientlibs/js/quickTest.ts
// to run against the "lite" build (BYO http client): npx ts-node clientlibs/js/quickTest.ts lite

import { AxiosError } from 'axios';
import UpgradeClient, { MARKED_DECISION_POINT_STATUS, UpGradeClientInterfaces } from './dist/node';
import type { UpGradeClientInterfaces } from './dist/node';
import { FetchHttpClient } from './quickTestLiteHttpClient';

const variant = process.argv[2] === 'lite' ? 'lite' : 'node';
console.log(`\n[quickTest] running against the "${variant}" build\n`);

// dynamic require so the unused variant's bundle (and, for "node", its bundled axios) is never
// loaded -- that would defeat the point of smoke-testing "lite" in isolation.
// webpack's `libraryExport: 'default'` UMD setting makes the required module *be* the default
// export (the UpgradeClient class) directly, with MARKED_DECISION_POINT_STATUS/etc. reachable
// only as static properties on it -- there is no `.default` to unwrap.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const UpgradeClient = require(`./dist/${variant}`) as typeof import('./dist/node').default;
const { MARKED_DECISION_POINT_STATUS } = UpgradeClient;
type UpgradeClientInstance = InstanceType<typeof UpgradeClient>;

const URL = {
LOCAL: 'http://localhost:3030',
Expand Down Expand Up @@ -39,6 +53,9 @@ const options: UpGradeClientInterfaces.IConfigOptions = {
includeStoredUserGroups,
}
: null,
// the "lite" build ships with no bundled http client and throws unless one is provided;
// the "node"/"browser" builds throw if one *is* provided, since they own the default (axios) client
httpClient: variant === 'lite' ? new FetchHttpClient() : undefined,
};

const logRequest = [
Expand Down Expand Up @@ -100,65 +117,65 @@ async function quickTest() {

/** test functions *******************************************************************************/

async function doInit(client: UpgradeClient) {
async function doInit(client: UpgradeClientInstance) {
try {
const response = await client.init();
console.log('\n[Init response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Init', error);
logRequestError('Init', error);
}
}

async function doGroupMembership(client: UpgradeClient) {
async function doGroupMembership(client: UpgradeClientInstance) {
const groupRequest: UpGradeClientInterfaces.IExperimentUserGroup = group;

try {
const response = await client.setGroupMembership(groupRequest);
console.log('\n[Group response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Group', error);
logRequestError('Group', error);
}
}

async function doWorkingGroupMembership(client: UpgradeClient) {
async function doWorkingGroupMembership(client: UpgradeClientInstance) {
const workingGroupRequest: UpGradeClientInterfaces.IExperimentUserWorkingGroup = { workingGroup };
try {
const response = await client.setWorkingGroup(workingGroupRequest);
console.log('\n[Working Group response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Working Group', error);
logRequestError('Working Group', error);
}
}

async function doAliases(client: UpgradeClient) {
async function doAliases(client: UpgradeClientInstance) {
const aliasRequest = [alias];
try {
const response = await client.setAltUserIds(aliasRequest);
console.log('\n[Aliases response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Aliases', error);
logRequestError('Aliases', error);
}
}

async function doAssign(client: UpgradeClient) {
async function doAssign(client: UpgradeClientInstance) {
try {
const response = await client.getAllExperimentConditions();
console.log('\n[Assign response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Assign', error);
logRequestError('Assign', error);
}
}

async function doAssignIgnoreCache(client: UpgradeClient) {
async function doAssignIgnoreCache(client: UpgradeClientInstance) {
try {
const response = await client.getAllExperimentConditions({ ignoreCache: true });
console.log('\n[Assign response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Assign', error);
logRequestError('Assign', error);
}
}

async function doGetDecisionPointAssignment(client: UpgradeClient): Promise<string | null> {
async function doGetDecisionPointAssignment(client: UpgradeClientInstance): Promise<string | null> {
try {
const response = await client.getDecisionPointAssignment(site, target);
console.log('\n[Decision Point Assignment response]:', JSON.stringify(response));
Expand All @@ -176,77 +193,77 @@ async function doGetDecisionPointAssignment(client: UpgradeClient): Promise<stri
console.log('\n[payloadValue]:', JSON.stringify(payloadValue));
return condition;
} catch (error) {
logAxiosError('Decision Point Assignment', error);
logRequestError('Decision Point Assignment', error);
return null;
}
}

// to test this function, omit passing options to constructor
function doSetFeatureFlagUserGroupsForSession(
client: UpgradeClient,
client: UpgradeClientInstance,
options: UpGradeClientInterfaces.IConfigOptions | null | undefined
) {
client.setFeatureFlagUserGroupsForSession(options?.featureFlagUserGroupsForSession);
}

async function doFeatureFlags(client: UpgradeClient) {
async function doFeatureFlags(client: UpgradeClientInstance) {
try {
const response = await client.getAllFeatureFlags();
console.log('\n[Feature Flag response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Feature Flag', error);
logRequestError('Feature Flag', error);
}
}

async function doFeatureFlagsIgnoreCache(client: UpgradeClient) {
async function doFeatureFlagsIgnoreCache(client: UpgradeClientInstance) {
try {
const response = await client.getAllFeatureFlags({ ignoreCache: true });
console.log('\n[Feature Flag response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Feature Flag', error);
logRequestError('Feature Flag', error);
}
}

async function doHasFeatureFlag(client: UpgradeClient) {
async function doHasFeatureFlag(client: UpgradeClientInstance) {
try {
const response = await client.hasFeatureFlag(featureFlagKey);
console.log('\n[Has Feature Flag response]:', response);
} catch (error) {
logAxiosError('Has Feature Flag', error);
logRequestError('Has Feature Flag', error);
}
}

async function doMark(client: UpgradeClient, condition: string | null) {
async function doMark(client: UpgradeClientInstance, condition: string | null) {
try {
const response = await client.markDecisionPoint(site, target, condition, status);
console.log('\n[Mark response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Mark', error);
logRequestError('Mark', error);
}
}

async function doLog(client: UpgradeClient) {
async function doLog(client: UpgradeClientInstance) {
try {
const response = await client.log(logRequest);
console.log('\n[Log response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Log', error);
logRequestError('Log', error);
}
}

async function doSendRewardByExperimentId(client: UpgradeClient) {
async function doSendRewardByExperimentId(client: UpgradeClientInstance) {
try {
const response = await client.sendReward({
rewardValue,
experimentId,
});
console.log('\n[Send Reward by ExperimentId response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Send Reward by ExperimentId', error);
logRequestError('Send Reward by ExperimentId', error);
}
}

async function doSendRewardByDecisionPoint(client: UpgradeClient) {
async function doSendRewardByDecisionPoint(client: UpgradeClientInstance) {
try {
const response = await client.sendReward({
rewardValue,
Expand All @@ -258,22 +275,24 @@ async function doSendRewardByDecisionPoint(client: UpgradeClient) {
});
console.log('\n[Send Reward by Decision Point response]:', JSON.stringify(response));
} catch (error) {
logAxiosError('Send Reward by Decision Point', error);
logRequestError('Send Reward by Decision Point', error);
}
}

/** utility functions *******************************************************************************/

function logAxiosError(functionContext: string, error: unknown) {
const axiosError = error as AxiosError;
// handles error shapes from both the default (axios-backed) http client and the fetch-based
// one used to smoke-test the "lite" build in quickTestLiteHttpClient.ts
function logRequestError(functionContext: string, error: unknown) {
const err = error as Error;
try {
const parsedError = JSON.parse(axiosError.message);
const parsedError = JSON.parse(err.message);

console.error(`\n[${functionContext} error]:`, {
status: parsedError.status,
request: parsedError.config.data,
message: parsedError.message,
stack: parsedError.stack,
status: parsedError.status ?? parsedError.statusCode,
request: parsedError.config?.data ?? parsedError.response,
message: parsedError.message ?? err.message,
stack: err.stack,
});
} catch {
console.error(`\n[${functionContext} error]:`, error);
Expand Down
59 changes: 59 additions & 0 deletions clientlibs/js/quickTestLiteHttpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// A minimal IHttpClientWrapper implementation used only by quickTest.ts to smoke-test the
// "lite" build, which ships with no bundled HTTP client and requires consumers to bring their own.
// Deliberately uses the global `fetch` (Node 18+) instead of axios, so running against the lite
// build never pulls axios back in.
import type { UpGradeClientInterfaces } from './dist/node';

export class FetchHttpClient implements UpGradeClientInterfaces.IHttpClientWrapper {
public config: UpGradeClientInterfaces.IHttpClientWrapperRequestConfig = undefined;

public async doGet<ResponseType>(
url: string,
options: UpGradeClientInterfaces.IHttpClientWrapperRequestConfig
): Promise<ResponseType> {
return this.request<ResponseType>('GET', url, undefined, options);
}

public async doPost<ResponseType, RequestBodyType>(
url: string,
body: RequestBodyType,
options: UpGradeClientInterfaces.IHttpClientWrapperRequestConfig
): Promise<ResponseType> {
return this.request<ResponseType>('POST', url, body, options);
}

public async doPatch<ResponseType, RequestBodyType>(
url: string,
body: RequestBodyType,
options: UpGradeClientInterfaces.IHttpClientWrapperRequestConfig
): Promise<ResponseType> {
return this.request<ResponseType>('PATCH', url, body, options);
}

private async request<ResponseType>(
method: 'GET' | 'POST' | 'PATCH',
url: string,
body: unknown,
options: UpGradeClientInterfaces.IHttpClientWrapperRequestConfig
): Promise<ResponseType> {
const response = await fetch(url, {
method,
headers: options.headers as Record<string, string>,
credentials: options.withCredentials ? 'include' : 'same-origin',
body: body === undefined ? undefined : JSON.stringify(body),
});
Comment thread
danoswaltCL marked this conversation as resolved.

// matches DefaultHttpClient's shape so the existing quickTest error logging works unchanged
let responseBody: unknown;
try {
responseBody = await response.json();
} catch {
responseBody = undefined;
}
if (!response.ok) {
throw new Error(JSON.stringify({ statusCode: response.status, response: responseBody }));
}

return responseBody as ResponseType;
}
}
8 changes: 7 additions & 1 deletion clientlibs/js/src/ApiService/ApiService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { UpGradeClientEnums, UpGradeClientInterfaces, UpGradeClientRequests } from '../types';
import { CaliperEnvelope, IExperimentAssignment, ILogInput, IUserAliases, ILogRequestBody } from 'upgrade_types';
import {
CaliperEnvelope,
IExperimentAssignment,
ILogInput,
IUserAliases,
ILogRequestBody,
} from 'upgrade_types/Experiment/interfaces';
import { DataService } from 'DataService/DataService';
import { IApiServiceRequestParams, IEndpoints } from './ApiService.types';

Expand Down
9 changes: 2 additions & 7 deletions clientlibs/js/src/Assignment/Assignment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
IExperimentAssignment,
PAYLOAD_TYPE,
EXPERIMENT_TYPE,
IPayload,
MARKED_DECISION_POINT_STATUS,
} from 'upgrade_types';
import { IExperimentAssignment, IPayload } from 'upgrade_types/Experiment/interfaces';
import { PAYLOAD_TYPE, EXPERIMENT_TYPE, MARKED_DECISION_POINT_STATUS } from 'upgrade_types/Experiment/enums';
import { UpGradeClientInterfaces } from '../types';
import ApiService from '../ApiService/ApiService';

Expand Down
2 changes: 1 addition & 1 deletion clientlibs/js/src/DataService/DataService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UpGradeClientInterfaces } from '../types';
import { IExperimentAssignment } from 'upgrade_types';
import { IExperimentAssignment } from 'upgrade_types/Experiment/interfaces';

/**
* Synchronous data store
Expand Down
14 changes: 4 additions & 10 deletions clientlibs/js/src/UpGradeClient/UpgradeClient.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { UpGradeClientInterfaces } from '../types';
import {
ILogInput,
CaliperEnvelope,
IExperimentAssignment,
MARKED_DECISION_POINT_STATUS,
IUserAliases,
BinaryRewardAllowedValue,
} from 'upgrade_types';
import { UpGradeClientEnums, UpGradeClientInterfaces } from '../types';
import { ILogInput, CaliperEnvelope, IExperimentAssignment, IUserAliases } from 'upgrade_types/Experiment/interfaces';
import { MARKED_DECISION_POINT_STATUS } from 'upgrade_types/Experiment/enums';
import Assignment from '../Assignment/Assignment';
import ApiService from '../ApiService/ApiService';
import { DataService } from '../DataService/DataService';
Expand Down Expand Up @@ -75,7 +69,7 @@ export default class UpgradeClient {

// allow BINARY_REWARD_VALUE to be exposed on the client a la UpgradeClient.BINARY_REWARD_VALUE
// this will allow js users who are not using the upgrade types package to use this enum for sendReward()
public static BINARY_REWARD_VALUE = BinaryRewardAllowedValue;
public static BINARY_REWARD_VALUE = UpGradeClientEnums.BINARY_REWARD_VALUE;

/**
* When constructing UpgradeClient, the user id, api host url, and "context" identifier are required.
Expand Down
4 changes: 2 additions & 2 deletions clientlibs/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import UpgradeClient from './UpGradeClient/UpgradeClient';
import Assignment from './Assignment/Assignment';
import { UpGradeClientEnums, UpGradeClientInterfaces, UpGradeClientRequests } from './types';
import { MARKED_DECISION_POINT_STATUS } from 'upgrade_types';
import type { IExperimentAssignment, IExperimentAssignmentv5 } from 'upgrade_types';
import { MARKED_DECISION_POINT_STATUS } from 'upgrade_types/Experiment/enums';
import type { IExperimentAssignment, IExperimentAssignmentv5 } from 'upgrade_types/Experiment/interfaces';

export default UpgradeClient;

Expand Down
2 changes: 1 addition & 1 deletion clientlibs/js/src/types/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { IMetricMetaData, MARKED_DECISION_POINT_STATUS } from 'upgrade_types';
import { IMetricMetaData, MARKED_DECISION_POINT_STATUS } from 'upgrade_types/Experiment/enums';

export namespace UpGradeClientInterfaces {
// this namespace should be for consumer facing interface
Expand Down
5 changes: 5 additions & 0 deletions clientlibs/js/src/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ export namespace UpGradeClientEnums {
POST = 'POST',
PATCH = 'PATCH',
}

export enum BINARY_REWARD_VALUE {
SUCCESS = 'SUCCESS',
FAILURE = 'FAILURE',
}
}
Loading