From 3cb9ec64920b21548d91a427e0c61c068a4f99d9 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 2 Sep 2022 15:28:52 -0400 Subject: [PATCH 01/35] Add ODP event, send event, & refactors --- .../lib/plugins/odp/graphql_manager.ts | 2 +- .../lib/plugins/odp/odp_client.ts | 52 ++++++++++++++++--- .../lib/plugins/odp/odp_event.ts | 32 ++++++++++++ .../lib/plugins/odp/odp_request_parameters.ts | 45 ++++++++++++++++ .../plugins/odp/query_segments_parameters.ts | 22 ++++---- .../lib/plugins/odp/send_events_parameters.ts | 45 ++++++++++++++++ .../browser_request_handler.ts | 6 +-- .../lib/utils/http_request_handler/http.ts | 2 +- .../node_request_handler.ts | 6 +-- .../optimizely-sdk/tests/odpClient.spec.ts | 18 +++---- 10 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_event.ts create mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts create mode 100644 packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index fc0160d05..c301abdb7 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -76,7 +76,7 @@ export class GraphqlManager implements IGraphQLManager { public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { const parameters = new QuerySegmentsParameters({ apiKey, - apiHost, + apiEndpoint: apiHost, userKey, userValue, segmentsToCheck, diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 416f312b5..46f6d40ca 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -18,21 +18,32 @@ import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { RequestHandler, Response } from '../../utils/http_request_handler/http'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; +import { SendEventsParameters } from './send_events_parameters'; /** - * Standard failure message for fetch errors + * Standard message for audience querying fetch errors */ -const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; +const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Failed to query audience segments'; +/** + * Standard message for sending events errors + */ +const EVENT_SENDING_FAILURE_MESSAGE = 'Failed to send ODP events'; /** * Return value for scenarios with no valid JSON */ const EMPTY_JSON_RESPONSE = null; +/** + * Code when no valid HTTP Status Code available; + */ +const EMPTY_RESPONSE_CODE = 0; /** * Interface for sending requests and handling responses to Optimizely Data Platform */ export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; + + sendOdpEvents(parameters: SendEventsParameters): Promise; } /** @@ -64,13 +75,13 @@ export class OdpClient implements IOdpClient { * @returns JSON response string from ODP */ public async querySegments(parameters: QuerySegmentsParameters): Promise { - if (!parameters?.apiHost || !parameters?.apiKey) { + if (!parameters?.apiEndpoint || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); return EMPTY_JSON_RESPONSE; } - const method = 'POST'; - const url = parameters.apiHost; + const method = parameters.httpVerb; + const url = parameters.apiEndpoint; const headers = { 'Content-Type': 'application/json', 'x-api-key': parameters.apiKey, @@ -84,11 +95,40 @@ export class OdpClient implements IOdpClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); + this._logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); return EMPTY_JSON_RESPONSE; } return response.body; } + + public async sendOdpEvents(parameters: SendEventsParameters): Promise { + if (!parameters?.apiEndpoint || !parameters?.apiKey) { + this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); + return EMPTY_RESPONSE_CODE; + } + + const method = parameters.httpVerb; + const url = parameters.apiEndpoint; + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': parameters.apiKey, + }; + const data = parameters.toGraphQLJson(); + + let response: Response; + try { + const request = this._requestHandler.makeRequest(url, headers, method, data); + response = await request.responsePromise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + this._errorHandler.handleError(error); + this._logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); + + return EMPTY_RESPONSE_CODE; + } + + return response.statusCode ?? 0; + } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts new file mode 100644 index 000000000..d84577c01 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class OdpEvent { + private readonly _type: string; + + private readonly _action: string; + + private readonly _identifiers: Map; + + private readonly _data: Map; + + constructor(type: string, action: string, identifiers: Map, data: Map) { + this._type = type; + this._action = action; + this._identifiers = identifiers; + this._data = data; + } +} diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts new file mode 100644 index 000000000..190c6ef54 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface IOdpRequestParameters { + apiKey: string | undefined; + apiEndpoint: string | undefined; + httpVerb: string; + + toGraphQLJson(): string; +} + +export abstract class OdpRequestParameters implements IOdpRequestParameters { + /** + * Optimizely Data Platform API key + */ + public apiKey: string | undefined; + + /** + * Fully-qualified URL to ODP events endpoint + */ + public apiEndpoint: string | undefined; + + /** + * HTTP Verb used to send request + */ + public abstract readonly httpVerb: string; + + /** + * Method to convert object(s) into expected GraphQL JSON string + */ + public abstract toGraphQLJson(): string; +} diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts index aa12df677..ae2ffd3aa 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -14,20 +14,12 @@ * limitations under the License. */ +import { OdpRequestParameters } from './odp_request_parameters'; + /** * Handles parameters used in querying ODP segments */ -export class QuerySegmentsParameters { - /** - * Optimizely Data Platform API key - */ - public apiKey?: string; - - /** - * Fully-qualified URL to ODP endpoint - */ - public apiHost: string | undefined; - +export class QuerySegmentsParameters extends OdpRequestParameters { /** * 'vuid' or 'fs_user_id' (client device id or fullstack id) */ @@ -43,7 +35,13 @@ export class QuerySegmentsParameters { */ public segmentsToCheck: string[] | undefined; - constructor(parameters: { apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[] }) { + /** + * HTTP Verb used to send request + */ + public readonly httpVerb = 'POST'; + + constructor(parameters: { apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[] }) { + super(); Object.assign(this, parameters); } diff --git a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts new file mode 100644 index 000000000..6e3eb4cde --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OdpEvent } from './odp_event'; +import { OdpRequestParameters } from './odp_request_parameters'; + +/** + * Handles parameters used in send ODP events + */ +export class SendEventsParameters extends OdpRequestParameters { + /** + * Collection of ODP events to transmit + */ + public events: OdpEvent[] | undefined; + + /** + * HTTP Verb used to send request + */ + public httpVerb = 'POST'; + + constructor(parameters: { apiKey: string, apiEndpoint: string, events: OdpEvent[] }) { + super(); + Object.assign(this, parameters); + } + + /** + * Convert events to JSON format + */ + public toGraphQLJson(): string { + return JSON.stringify(this.events); + } +} diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index 3103b5322..7785404c7 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -34,17 +34,17 @@ export class BrowserRequestHandler implements RequestHandler { /** * Builds an XMLHttpRequest - * @param reqUrl Fully-qualified URL to which to send the request + * @param requestUrl Fully-qualified URL to which to send the request * @param headers List of headers to include in the request * @param method HTTP method to use * @param data stringified version of data to POST, PUT, etc * @returns AbortableRequest contains both the response Promise and capability to abort() */ - public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { + public makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { const request = new XMLHttpRequest(); const responsePromise: Promise = new Promise((resolve, reject) => { - request.open(method, reqUrl, true); + request.open(method, requestUrl, true); this.setHeadersInXhr(headers, request); diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts index 4eafb07fd..98c1cedc6 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts @@ -43,5 +43,5 @@ export interface AbortableRequest { * Client that handles sending requests and receiving responses */ export interface RequestHandler { - makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest; + makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest; } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index 5a6c647f1..d2d9c0bf9 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -36,14 +36,14 @@ export class NodeRequestHandler implements RequestHandler { /** * Builds an XMLHttpRequest - * @param reqUrl Fully-qualified URL to which to send the request + * @param requestUrl Fully-qualified URL to which to send the request * @param headers List of headers to include in the request * @param method HTTP method to use * @param data stringified version of data to POST, PUT, etc * @returns AbortableRequest contains both the response Promise and capability to abort() */ - public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { - const parsedUrl = url.parse(reqUrl); + public makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { + const parsedUrl = url.parse(requestUrl); if (parsedUrl.protocol !== 'https:') { return { diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index d0a7bbb57..22a4dea94 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -26,7 +26,7 @@ import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_reque describe('OdpClient', () => { const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ apiKey: 'not-real-api-key', - apiHost: 'https://api.example.com/v3/graphql', + apiEndpoint: 'https://api.example.com/v3/graphql', userKey: 'fs_user_id', userValue: 'mock-user-id', segmentsToCheck: [ @@ -80,7 +80,7 @@ describe('OdpClient', () => { it('should handle missing API Host', async () => { const missingApiHost = new QuerySegmentsParameters({ apiKey: 'apiKey', - apiHost: '', + apiEndpoint: '', userKey: 'userKey', userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], @@ -96,7 +96,7 @@ describe('OdpClient', () => { it('should handle missing API Key', async () => { const missingApiHost = new QuerySegmentsParameters({ apiKey: '', - apiHost: 'apiHost', + apiEndpoint: 'apiHost', userKey: 'userKey', userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], @@ -119,7 +119,6 @@ describe('OdpClient', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; @@ -139,7 +138,6 @@ describe('OdpClient', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; @@ -165,7 +163,7 @@ describe('OdpClient', () => { expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); }); it('Node should handle 400 HTTP response', async () => { @@ -184,7 +182,7 @@ describe('OdpClient', () => { expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); }); it('Browser should handle 500 HTTP response', async () => { @@ -203,7 +201,7 @@ describe('OdpClient', () => { expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); }); it('Node should handle 500 HTTP response', async () => { @@ -222,7 +220,7 @@ describe('OdpClient', () => { expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); }); it('should handle a network timeout', async () => { @@ -237,7 +235,7 @@ describe('OdpClient', () => { expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (network error)')).once(); }); }); From 5d6144d897dcc1cfc88b3ef24a9811d01d6351a2 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 2 Sep 2022 17:11:41 -0400 Subject: [PATCH 02/35] WIP testing sendOdpEvents --- .../lib/plugins/odp/odp_event.ts | 3 +- .../optimizely-sdk/tests/odpClient.spec.ts | 437 +++++++++++------- 2 files changed, 277 insertions(+), 163 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index d84577c01..488f1dc0b 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -23,7 +23,8 @@ export class OdpEvent { private readonly _data: Map; - constructor(type: string, action: string, identifiers: Map, data: Map) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(type: string, action: string, identifiers: Map, data: Map) { this._type = type; this._action = action; this._identifiers = identifiers; diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 22a4dea94..9f276fe8a 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -22,42 +22,10 @@ import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; +import { SendEventsParameters } from '../lib/plugins/odp/send_events_parameters'; +import { OdpEvent } from '../lib/plugins/odp/odp_event'; describe('OdpClient', () => { - const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ - apiKey: 'not-real-api-key', - apiEndpoint: 'https://api.example.com/v3/graphql', - userKey: 'fs_user_id', - userValue: 'mock-user-id', - segmentsToCheck: [ - 'has_email', - 'has_email_opted_in', - 'push_on_sale', - ], - }); - const VALID_RESPONSE_JSON = { - 'data': { - 'customer': { - 'audiences': { - 'edges': [ - { - 'node': { - 'name': 'has_email', - 'state': 'qualified', - }, - }, - { - 'node': { - 'name': 'has_email_opted_in', - 'state': 'qualified', - }, - }, - ], - }, - }, - }, - }; - let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockBrowserRequestHandler: BrowserRequestHandler; @@ -77,165 +45,310 @@ describe('OdpClient', () => { resetCalls(mockNodeRequestHandler); }); - it('should handle missing API Host', async () => { - const missingApiHost = new QuerySegmentsParameters({ - apiKey: 'apiKey', - apiEndpoint: '', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], + describe('querySegments', () => { + const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ + apiKey: 'not-real-api-key', + apiEndpoint: 'https://api.example.com/v3/graphql', + userKey: 'fs_user_id', + userValue: 'mock-user-id', + segmentsToCheck: [ + 'has_email', + 'has_email_opted_in', + 'push_on_sale', + ], + }); + const VALID_RESPONSE_JSON = { + 'data': { + 'customer': { + 'audiences': { + 'edges': [ + { + 'node': { + 'name': 'has_email', + 'state': 'qualified', + }, + }, + { + 'node': { + 'name': 'has_email_opted_in', + 'state': 'qualified', + }, + }, + ], + }, + }, + }, + }; + + it('should handle missing API Host', async () => { + const missingApiEndpoint = new QuerySegmentsParameters({ + apiKey: 'apiKey', + apiEndpoint: '', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + await client.querySegments(missingApiEndpoint); + + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - await client.querySegments(missingApiHost); + it('should handle missing API Key', async () => { + const missingApiHost = new QuerySegmentsParameters({ + apiKey: '', + apiEndpoint: 'apiHost', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); - }); + await client.querySegments(missingApiHost); - it('should handle missing API Key', async () => { - const missingApiHost = new QuerySegmentsParameters({ - apiKey: '', - apiEndpoint: 'apiHost', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - await client.querySegments(missingApiHost); + it('Browser: should get mocked segments successfully', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(VALID_RESPONSE_JSON), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + + expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); - }); + it('Node: should get mocked segments successfully', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(VALID_RESPONSE_JSON), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + + expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); - it('Browser: should get mocked segments successfully', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(VALID_RESPONSE_JSON), - headers: {}, - }), + it('Browser: should handle 400 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 400, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + it('Node: should handle 400 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 400, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); + }); - expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - }); + it('Browser: should handle 500 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 500, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); + }); - it('Node: should get mocked segments successfully', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(VALID_RESPONSE_JSON), - headers: {}, - }), + it('Node: should handle 500 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 500, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + it('should handle a network timeout', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - }); + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - it('Browser should handle 400 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject({ - statusCode: 400, - body: '', - headers: {}, - }), + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (network error)')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); }); - it('Node should handle 400 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject({ - statusCode: 400, - body: '', - headers: {}, - }), + describe('sendOdpEvents', () => { + const MOCK_SEND_PARAMETERS = new SendEventsParameters({ + apiKey: 'also-not-real-api-key', + apiEndpoint: 'https://events.example.com/v2/api', + events: [ + new OdpEvent('t1', 'a1', + new Map([['id-key-1', 'id-value-1']]), + new Map(Object.entries({ + key11: 'value-1', + key12: true, + key13: 3.5, + }))), + new OdpEvent('t2', 'a2', + new Map([['id-key-2', 'id-value-2']]), + new Map(Object.entries({ + key2: 'value-2', + }))), + ], }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + const VALID_RESPONSE_CODE = 200; - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + it('should handle missing API Endpoint', async () => { + const missingApiEndpoint = new SendEventsParameters({ + apiKey: 'apiKey', + apiEndpoint: '', + events: [], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); - }); + await client.sendOdpEvents(missingApiEndpoint); - it('Browser should handle 500 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject({ - statusCode: 500, - body: '', - headers: {}, - }), + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + it('should handle missing API Key', async () => { + const missingApiKey = new SendEventsParameters({ + apiKey: '', + apiEndpoint: 'https://some.example.com/endpoint', + events: [], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); - }); + await client.sendOdpEvents(missingApiKey); - it('Node should handle 500 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject({ - statusCode: 500, - body: '', - headers: {}, - }), + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); - }); - - it('should handle a network timeout', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject(new Error('Request timed out')), + it('Browser: should send events successfully', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(MOCK_SEND_PARAMETERS), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const response = await client.sendOdpEvents(MOCK_SEND_PARAMETERS) ?? ''; + + expect(response).toEqual(VALID_RESPONSE_CODE); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + it('Node: should send events successfully', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(MOCK_SEND_PARAMETERS), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const response = await client.sendOdpEvents(MOCK_SEND_PARAMETERS) ?? ''; + + expect(response).toEqual(VALID_RESPONSE_CODE); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (network error)')).once(); + // TODO: Start here + it('Browser: should not retry for 400 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 400, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(400); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to send ODP events (400)')).once(); + }); }); -}); +}) +; From 2890a0d66b9ad53c44cf1bf879a818e95281ef45 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 12 Sep 2022 10:02:59 -0400 Subject: [PATCH 03/35] Unit tests + code ODP Client edits --- .../lib/plugins/odp/odp_client.ts | 39 ++++-- .../browser_request_handler.ts | 4 +- .../optimizely-sdk/tests/odpClient.spec.ts | 129 ++++++++++++++---- 3 files changed, 128 insertions(+), 44 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 46f6d40ca..4eeb92b1f 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -31,11 +31,15 @@ const EVENT_SENDING_FAILURE_MESSAGE = 'Failed to send ODP events'; /** * Return value for scenarios with no valid JSON */ -const EMPTY_JSON_RESPONSE = null; +const NULL_JSON_RESPONSE = null; /** * Code when no valid HTTP Status Code available; */ -const EMPTY_RESPONSE_CODE = 0; +export const RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE = 0; +/** + * Defines when consumer should not retry ODP event + */ +const RETRY_NOT_ADVISED = null; /** * Interface for sending requests and handling responses to Optimizely Data Platform @@ -43,7 +47,7 @@ const EMPTY_RESPONSE_CODE = 0; export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; - sendOdpEvents(parameters: SendEventsParameters): Promise; + sendOdpEvents(parameters: SendEventsParameters): Promise; } /** @@ -71,13 +75,13 @@ export class OdpClient implements IOdpClient { /** * Handler for querying the ODP GraphQL endpoint - * @param parameters - * @returns JSON response string from ODP + * @param parameters Query parameters to send to ODP + * @returns JSON response string from ODP or null */ public async querySegments(parameters: QuerySegmentsParameters): Promise { if (!parameters?.apiEndpoint || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); - return EMPTY_JSON_RESPONSE; + return NULL_JSON_RESPONSE; } const method = parameters.httpVerb; @@ -95,18 +99,27 @@ export class OdpClient implements IOdpClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); + this._logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); - return EMPTY_JSON_RESPONSE; + return NULL_JSON_RESPONSE; } return response.body; } - public async sendOdpEvents(parameters: SendEventsParameters): Promise { + /** + * Handler for sending ODP events + * @param parameters + * @returns + * 1. null, When there was a non-recoverable error and no retry is needed. + * 2. 0 If an unexpected error occurred and retrying can be useful. + * 3. HTTPStatus code if httpclient was able to make the request and was able to receive response. + * It is recommended to retry if status code was 5xx. + */ + public async sendOdpEvents(parameters: SendEventsParameters): Promise { if (!parameters?.apiEndpoint || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); - return EMPTY_RESPONSE_CODE; + return RETRY_NOT_ADVISED; } const method = parameters.httpVerb; @@ -124,11 +137,11 @@ export class OdpClient implements IOdpClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); + this._logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); - return EMPTY_RESPONSE_CODE; + return RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE; } - return response.statusCode ?? 0; + return response.statusCode ?? RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE; } } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index 7785404c7..187408a65 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -18,8 +18,6 @@ import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { REQUEST_TIMEOUT_MS } from './config'; import { LogHandler, LogLevel } from '../../modules/logging'; -const READY_STATE_DONE = 4; - /** * Handles sending requests and receiving responses over HTTP via XMLHttpRequest */ @@ -49,7 +47,7 @@ export class BrowserRequestHandler implements RequestHandler { this.setHeadersInXhr(headers, request); request.onreadystatechange = (): void => { - if (request.readyState === READY_STATE_DONE) { + if (request.readyState === XMLHttpRequest.DONE) { const statusCode = request.status; if (statusCode === 0) { reject(new Error('Request error')); diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 9f276fe8a..8c4554643 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -18,7 +18,7 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpClient } from '../lib/plugins/odp/odp_client'; +import { OdpClient, RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; @@ -79,6 +79,7 @@ describe('OdpClient', () => { }, }, }; + const BODY_FROM_ERROR = ''; it('should handle missing API Host', async () => { const missingApiEndpoint = new QuerySegmentsParameters({ @@ -154,9 +155,9 @@ describe('OdpClient', () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, - responsePromise: Promise.reject({ + responsePromise: Promise.resolve({ statusCode: 400, - body: '', + body: BODY_FROM_ERROR, headers: {}, }), }); @@ -164,18 +165,18 @@ describe('OdpClient', () => { const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); it('Node: should handle 400 HTTP response', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, - responsePromise: Promise.reject({ + responsePromise: Promise.resolve({ statusCode: 400, - body: '', + body: BODY_FROM_ERROR, headers: {}, }), }); @@ -183,18 +184,18 @@ describe('OdpClient', () => { const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (400)')).once(); + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); it('Browser: should handle 500 HTTP response', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, - responsePromise: Promise.reject({ + responsePromise: Promise.resolve({ statusCode: 500, - body: '', + body: BODY_FROM_ERROR, headers: {}, }), }); @@ -202,18 +203,18 @@ describe('OdpClient', () => { const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); it('Node: should handle 500 HTTP response', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, - responsePromise: Promise.reject({ + responsePromise: Promise.resolve({ statusCode: 500, - body: '', + body: BODY_FROM_ERROR, headers: {}, }), }); @@ -221,9 +222,9 @@ describe('OdpClient', () => { const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (500)')).once(); + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); it('should handle a network timeout', async () => { @@ -271,8 +272,9 @@ describe('OdpClient', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - await client.sendOdpEvents(missingApiEndpoint); + const statusReturned = await client.sendOdpEvents(missingApiEndpoint); + expect(statusReturned).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); }); @@ -285,8 +287,9 @@ describe('OdpClient', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - await client.sendOdpEvents(missingApiKey); + const statusReturned = await client.sendOdpEvents(missingApiKey); + expect(statusReturned).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); }); @@ -329,12 +332,11 @@ describe('OdpClient', () => { verify(mockLogger.log(anything(), anyString())).never(); }); - // TODO: Start here - it('Browser: should not retry for 400 HTTP response', async () => { + it('Browser: should handle and return 400 HTTP response', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, - responsePromise: Promise.reject({ + responsePromise: Promise.resolve({ statusCode: 400, body: '', headers: {}, @@ -345,10 +347,81 @@ describe('OdpClient', () => { const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); expect(statusCode).toEqual(400); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Node: should handle and return 400 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 400, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(400); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Browser: should handle and return 500 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 500, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(500); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Node: should handle and return 500 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 500, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(500); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('should handle a network timeout', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); + + const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toBe(RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to send ODP events (400)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Failed to send ODP events (network error)')).once(); }); }); }) -; From a3463b9ac117621e9f8c7e4614877b4014061082 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 12 Sep 2022 10:48:11 -0400 Subject: [PATCH 04/35] Refactor apiHost to apiEndpoint --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index c301abdb7..a81e1cc70 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -68,15 +68,15 @@ export class GraphqlManager implements IGraphQLManager { /** * Retrieves the audience segments from ODP * @param apiKey ODP public key - * @param apiHost Fully-qualified URL of ODP + * @param apiEndpoint Fully-qualified URL of ODP * @param userKey 'vuid' or 'fs_user_id key' * @param userValue Associated value to query for the user key * @param segmentsToCheck Audience segments to check for experiment inclusion */ - public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { + public async fetchSegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { const parameters = new QuerySegmentsParameters({ apiKey, - apiEndpoint: apiHost, + apiEndpoint, userKey, userValue, segmentsToCheck, From 9eba320d8bb2afc2190b399865a40b983ff49592 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 13 Sep 2022 09:45:31 -0400 Subject: [PATCH 05/35] WIP code review changes --- .../lib/plugins/odp/odp_client.ts | 28 ++++++------------- .../lib/plugins/odp/odp_event.ts | 3 +- .../lib/plugins/odp/odp_request_parameters.ts | 7 ----- .../lib/plugins/odp/send_events_parameters.ts | 2 +- .../optimizely-sdk/tests/odpClient.spec.ts | 10 +++---- 5 files changed, 16 insertions(+), 34 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 4eeb92b1f..8441ed9af 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -23,23 +23,11 @@ import { SendEventsParameters } from './send_events_parameters'; /** * Standard message for audience querying fetch errors */ -const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Failed to query audience segments'; +const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; /** * Standard message for sending events errors */ -const EVENT_SENDING_FAILURE_MESSAGE = 'Failed to send ODP events'; -/** - * Return value for scenarios with no valid JSON - */ -const NULL_JSON_RESPONSE = null; -/** - * Code when no valid HTTP Status Code available; - */ -export const RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE = 0; -/** - * Defines when consumer should not retry ODP event - */ -const RETRY_NOT_ADVISED = null; +const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; /** * Interface for sending requests and handling responses to Optimizely Data Platform @@ -81,7 +69,7 @@ export class OdpClient implements IOdpClient { public async querySegments(parameters: QuerySegmentsParameters): Promise { if (!parameters?.apiEndpoint || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); - return NULL_JSON_RESPONSE; + return null; } const method = parameters.httpVerb; @@ -101,7 +89,7 @@ export class OdpClient implements IOdpClient { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); - return NULL_JSON_RESPONSE; + return null; } return response.body; @@ -119,7 +107,7 @@ export class OdpClient implements IOdpClient { public async sendOdpEvents(parameters: SendEventsParameters): Promise { if (!parameters?.apiEndpoint || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); - return RETRY_NOT_ADVISED; + return null; } const method = parameters.httpVerb; @@ -128,7 +116,7 @@ export class OdpClient implements IOdpClient { 'Content-Type': 'application/json', 'x-api-key': parameters.apiKey, }; - const data = parameters.toGraphQLJson(); + const data = parameters.toJson(); let response: Response; try { @@ -139,9 +127,9 @@ export class OdpClient implements IOdpClient { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); - return RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE; + return 0; } - return response.statusCode ?? RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE; + return response.statusCode ?? 0; } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index 488f1dc0b..229685ef6 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -21,7 +21,8 @@ export class OdpEvent { private readonly _identifiers: Map; - private readonly _data: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly _data: Map; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(type: string, action: string, identifiers: Map, data: Map) { diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts index 190c6ef54..73db3b1ac 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts @@ -18,8 +18,6 @@ export interface IOdpRequestParameters { apiKey: string | undefined; apiEndpoint: string | undefined; httpVerb: string; - - toGraphQLJson(): string; } export abstract class OdpRequestParameters implements IOdpRequestParameters { @@ -37,9 +35,4 @@ export abstract class OdpRequestParameters implements IOdpRequestParameters { * HTTP Verb used to send request */ public abstract readonly httpVerb: string; - - /** - * Method to convert object(s) into expected GraphQL JSON string - */ - public abstract toGraphQLJson(): string; } diff --git a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts index 6e3eb4cde..f53ba965c 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts @@ -39,7 +39,7 @@ export class SendEventsParameters extends OdpRequestParameters { /** * Convert events to JSON format */ - public toGraphQLJson(): string { + public toJson(): string { return JSON.stringify(this.events); } } diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 8c4554643..67bca38eb 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -18,7 +18,7 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpClient, RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE } from '../lib/plugins/odp/odp_client'; +import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; @@ -71,7 +71,7 @@ describe('OdpClient', () => { { 'node': { 'name': 'has_email_opted_in', - 'state': 'qualified', + 'state': 'not-ready', }, }, ], @@ -239,7 +239,7 @@ describe('OdpClient', () => { expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to query audience segments (network error)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); @@ -418,9 +418,9 @@ describe('OdpClient', () => { const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); - expect(statusCode).toBe(RETRY_ADVISED_BUT_NO_HTTP_STATUS_AVAILABLE); + expect(statusCode).toBe(0); verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Failed to send ODP events (network error)')).once(); + verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (network error)')).once(); }); }); }) From e34e8420cea6f81f8baf9746304207cead95a4f2 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 13 Sep 2022 10:18:43 -0400 Subject: [PATCH 06/35] Separate/refactor send events and query segments unit tests --- .../lib/plugins/odp/odp_client.ts | 4 +- .../optimizely-sdk/tests/odpClient.spec.ts | 427 ------------------ .../tests/odpQuerySegments.spec.ts | 242 ++++++++++ .../tests/odpSendEvents.spec.ts | 226 +++++++++ 4 files changed, 470 insertions(+), 429 deletions(-) delete mode 100644 packages/optimizely-sdk/tests/odpClient.spec.ts create mode 100644 packages/optimizely-sdk/tests/odpQuerySegments.spec.ts create mode 100644 packages/optimizely-sdk/tests/odpSendEvents.spec.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 8441ed9af..4c5286e29 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -35,7 +35,7 @@ const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; - sendOdpEvents(parameters: SendEventsParameters): Promise; + sendEvents(parameters: SendEventsParameters): Promise; } /** @@ -104,7 +104,7 @@ export class OdpClient implements IOdpClient { * 3. HTTPStatus code if httpclient was able to make the request and was able to receive response. * It is recommended to retry if status code was 5xx. */ - public async sendOdpEvents(parameters: SendEventsParameters): Promise { + public async sendEvents(parameters: SendEventsParameters): Promise { if (!parameters?.apiEndpoint || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); return null; diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts deleted file mode 100644 index 67bca38eb..000000000 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ /dev/null @@ -1,427 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// - -import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpClient } from '../lib/plugins/odp/odp_client'; -import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; -import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; -import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -import { SendEventsParameters } from '../lib/plugins/odp/send_events_parameters'; -import { OdpEvent } from '../lib/plugins/odp/odp_event'; - -describe('OdpClient', () => { - let mockErrorHandler: ErrorHandler; - let mockLogger: LogHandler; - let mockBrowserRequestHandler: BrowserRequestHandler; - let mockNodeRequestHandler: NodeRequestHandler; - - beforeAll(() => { - mockErrorHandler = mock(); - mockLogger = mock(); - mockBrowserRequestHandler = mock(); - mockNodeRequestHandler = mock(); - }); - - beforeEach(() => { - resetCalls(mockErrorHandler); - resetCalls(mockLogger); - resetCalls(mockBrowserRequestHandler); - resetCalls(mockNodeRequestHandler); - }); - - describe('querySegments', () => { - const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ - apiKey: 'not-real-api-key', - apiEndpoint: 'https://api.example.com/v3/graphql', - userKey: 'fs_user_id', - userValue: 'mock-user-id', - segmentsToCheck: [ - 'has_email', - 'has_email_opted_in', - 'push_on_sale', - ], - }); - const VALID_RESPONSE_JSON = { - 'data': { - 'customer': { - 'audiences': { - 'edges': [ - { - 'node': { - 'name': 'has_email', - 'state': 'qualified', - }, - }, - { - 'node': { - 'name': 'has_email_opted_in', - 'state': 'not-ready', - }, - }, - ], - }, - }, - }, - }; - const BODY_FROM_ERROR = ''; - - it('should handle missing API Host', async () => { - const missingApiEndpoint = new QuerySegmentsParameters({ - apiKey: 'apiKey', - apiEndpoint: '', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - await client.querySegments(missingApiEndpoint); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); - }); - - it('should handle missing API Key', async () => { - const missingApiHost = new QuerySegmentsParameters({ - apiKey: '', - apiEndpoint: 'apiHost', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - await client.querySegments(missingApiHost); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); - }); - - it('Browser: should get mocked segments successfully', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(VALID_RESPONSE_JSON), - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; - - expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('Node: should get mocked segments successfully', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(VALID_RESPONSE_JSON), - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; - - expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('Browser: should handle 400 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('Node: should handle 400 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('Browser: should handle 500 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('Node: should handle 500 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('should handle a network timeout', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); - }); - }); - - describe('sendOdpEvents', () => { - const MOCK_SEND_PARAMETERS = new SendEventsParameters({ - apiKey: 'also-not-real-api-key', - apiEndpoint: 'https://events.example.com/v2/api', - events: [ - new OdpEvent('t1', 'a1', - new Map([['id-key-1', 'id-value-1']]), - new Map(Object.entries({ - key11: 'value-1', - key12: true, - key13: 3.5, - }))), - new OdpEvent('t2', 'a2', - new Map([['id-key-2', 'id-value-2']]), - new Map(Object.entries({ - key2: 'value-2', - }))), - ], - }); - const VALID_RESPONSE_CODE = 200; - - it('should handle missing API Endpoint', async () => { - const missingApiEndpoint = new SendEventsParameters({ - apiKey: 'apiKey', - apiEndpoint: '', - events: [], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const statusReturned = await client.sendOdpEvents(missingApiEndpoint); - - expect(statusReturned).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); - }); - - it('should handle missing API Key', async () => { - const missingApiKey = new SendEventsParameters({ - apiKey: '', - apiEndpoint: 'https://some.example.com/endpoint', - events: [], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const statusReturned = await client.sendOdpEvents(missingApiKey); - - expect(statusReturned).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); - }); - - it('Browser: should send events successfully', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(MOCK_SEND_PARAMETERS), - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const response = await client.sendOdpEvents(MOCK_SEND_PARAMETERS) ?? ''; - - expect(response).toEqual(VALID_RESPONSE_CODE); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('Node: should send events successfully', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(MOCK_SEND_PARAMETERS), - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const response = await client.sendOdpEvents(MOCK_SEND_PARAMETERS) ?? ''; - - expect(response).toEqual(VALID_RESPONSE_CODE); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('Browser: should handle and return 400 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: '', - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); - - expect(statusCode).toEqual(400); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('Node: should handle and return 400 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: '', - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); - - expect(statusCode).toEqual(400); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('Browser: should handle and return 500 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: '', - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - - const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); - - expect(statusCode).toEqual(500); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('Node: should handle and return 500 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: '', - headers: {}, - }), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); - - expect(statusCode).toEqual(500); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('should handle a network timeout', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - - const statusCode = await client.sendOdpEvents(MOCK_SEND_PARAMETERS); - - expect(statusCode).toBe(0); - verify(mockErrorHandler.handleError(anything())).once(); - verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (network error)')).once(); - }); - }); -}) - diff --git a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts new file mode 100644 index 000000000..8530ac9d1 --- /dev/null +++ b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts @@ -0,0 +1,242 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; +import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import { OdpClient } from '../lib/plugins/odp/odp_client'; +import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; +import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; +import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; + +const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ + apiKey: 'not-real-api-key', + apiEndpoint: 'https://api.example.com/v3/graphql', + userKey: 'fs_user_id', + userValue: 'mock-user-id', + segmentsToCheck: [ + 'has_email', + 'has_email_opted_in', + 'push_on_sale', + ], +}); +const VALID_RESPONSE_JSON = { + 'data': { + 'customer': { + 'audiences': { + 'edges': [ + { + 'node': { + 'name': 'has_email', + 'state': 'qualified', + }, + }, + { + 'node': { + 'name': 'has_email_opted_in', + 'state': 'not-ready', + }, + }, + ], + }, + }, + }, +}; +const BODY_FROM_ERROR = ''; + +describe('OdpClient Query Segments', () => { + let mockErrorHandler: ErrorHandler; + let mockLogger: LogHandler; + let mockBrowserRequestHandler: BrowserRequestHandler; + let mockNodeRequestHandler: NodeRequestHandler; + + beforeAll(() => { + mockErrorHandler = mock(); + mockLogger = mock(); + mockBrowserRequestHandler = mock(); + mockNodeRequestHandler = mock(); + }); + + beforeEach(() => { + resetCalls(mockErrorHandler); + resetCalls(mockLogger); + resetCalls(mockBrowserRequestHandler); + resetCalls(mockNodeRequestHandler); + }); + + it('should handle missing API Host', async () => { + const missingApiEndpoint = new QuerySegmentsParameters({ + apiKey: 'apiKey', + apiEndpoint: '', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + await client.querySegments(missingApiEndpoint); + + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); + }); + + it('should handle missing API Key', async () => { + const missingApiHost = new QuerySegmentsParameters({ + apiKey: '', + apiEndpoint: 'apiHost', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + await client.querySegments(missingApiHost); + + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); + }); + + it('Browser: should get mocked segments successfully', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(VALID_RESPONSE_JSON), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + + expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('Node: should get mocked segments successfully', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(VALID_RESPONSE_JSON), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + + expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('Browser: should handle 400 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 400, + body: BODY_FROM_ERROR, + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Node: should handle 400 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 400, + body: BODY_FROM_ERROR, + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Browser: should handle 500 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 500, + body: BODY_FROM_ERROR, + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Node: should handle 500 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 500, + body: BODY_FROM_ERROR, + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBe(BODY_FROM_ERROR); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('should handle a network timeout', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); + }); +}); + diff --git a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts new file mode 100644 index 000000000..0a4844236 --- /dev/null +++ b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts @@ -0,0 +1,226 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; +import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import { OdpClient } from '../lib/plugins/odp/odp_client'; +import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; +import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; +import { SendEventsParameters } from '../lib/plugins/odp/send_events_parameters'; +import { OdpEvent } from '../lib/plugins/odp/odp_event'; + +const MOCK_SEND_PARAMETERS = new SendEventsParameters({ + apiKey: 'not-real-api-key', + apiEndpoint: 'https://events.example.com/v2/api', + events: [ + new OdpEvent('t1', 'a1', + new Map([['id-key-1', 'id-value-1']]), + new Map(Object.entries({ + key11: 'value-1', + key12: true, + key13: 3.5, + }))), + new OdpEvent('t2', 'a2', + new Map([['id-key-2', 'id-value-2']]), + new Map(Object.entries({ + key2: 'value-2', + }))), + ], +}); +const VALID_RESPONSE_CODE = 200; + +describe('OdpClient Send Events', () => { + let mockErrorHandler: ErrorHandler; + let mockLogger: LogHandler; + let mockBrowserRequestHandler: BrowserRequestHandler; + let mockNodeRequestHandler: NodeRequestHandler; + + beforeAll(() => { + mockErrorHandler = mock(); + mockLogger = mock(); + mockBrowserRequestHandler = mock(); + mockNodeRequestHandler = mock(); + }); + + beforeEach(() => { + resetCalls(mockErrorHandler); + resetCalls(mockLogger); + resetCalls(mockBrowserRequestHandler); + resetCalls(mockNodeRequestHandler); + }); + + it('should handle missing API Endpoint', async () => { + const missingApiEndpoint = new SendEventsParameters({ + apiKey: 'apiKey', + apiEndpoint: '', + events: [], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const statusReturned = await client.sendEvents(missingApiEndpoint); + + expect(statusReturned).toBeNull(); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); + }); + + it('should handle missing API Key', async () => { + const missingApiKey = new SendEventsParameters({ + apiKey: '', + apiEndpoint: 'https://some.example.com/endpoint', + events: [], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const statusReturned = await client.sendEvents(missingApiKey); + + expect(statusReturned).toBeNull(); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); + }); + + it('Browser: should send events successfully', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(MOCK_SEND_PARAMETERS), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const response = await client.sendEvents(MOCK_SEND_PARAMETERS) ?? ''; + + expect(response).toEqual(VALID_RESPONSE_CODE); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('Node: should send events successfully', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(MOCK_SEND_PARAMETERS), + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const response = await client.sendEvents(MOCK_SEND_PARAMETERS) ?? ''; + + expect(response).toEqual(VALID_RESPONSE_CODE); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('Browser: should handle and return 400 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 400, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(400); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Node: should handle and return 400 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 400, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(400); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Browser: should handle and return 500 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 500, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(500); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('Node: should handle and return 500 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 500, + body: '', + headers: {}, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toEqual(500); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); + }); + + it('should handle a network timeout', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); + + const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + + expect(statusCode).toBe(0); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (network error)')).once(); + }); +}); + From 5d5f643cdd2ba578c1a77b624533b341b079df38 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 15 Sep 2022 17:15:07 -0400 Subject: [PATCH 07/35] Code review requested changes --- .../lib/plugins/odp/graphql_manager.ts | 3 ++ .../lib/plugins/odp/odp_client.ts | 33 +++++++++--- .../lib/plugins/odp/odp_event.ts | 16 +++--- .../lib/plugins/odp/odp_request_parameters.ts | 8 +-- .../plugins/odp/query_segments_parameters.ts | 6 +-- .../lib/plugins/odp/send_events_parameters.ts | 2 +- .../tests/graphQLManager.spec.ts | 10 ++++ .../tests/odpQuerySegments.spec.ts | 53 ++++++++++++++++++- .../tests/odpSendEvents.spec.ts | 9 +++- 9 files changed, 112 insertions(+), 28 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index a81e1cc70..eca44de59 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -74,6 +74,9 @@ export class GraphqlManager implements IGraphQLManager { * @param segmentsToCheck Audience segments to check for experiment inclusion */ public async fetchSegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { + if (segmentsToCheck?.length === 0) { + return EMPTY_SEGMENTS_COLLECTION; + } const parameters = new QuerySegmentsParameters({ apiKey, apiEndpoint, diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 4c5286e29..fba9b5694 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -67,16 +67,27 @@ export class OdpClient implements IOdpClient { * @returns JSON response string from ODP or null */ public async querySegments(parameters: QuerySegmentsParameters): Promise { - if (!parameters?.apiEndpoint || !parameters?.apiKey) { + const { apiEndpoint, apiKey, httpVerb, userKey, userValue, segmentsToCheck } = parameters; + + if (segmentsToCheck?.length === 0) { + return ''; + } + + if (!apiEndpoint || !apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); return null; } - const method = parameters.httpVerb; - const url = parameters.apiEndpoint; + if (!userKey || !userValue) { + this._logger.log(LogLevel.ERROR, 'No UserKey or UserValue set before querying segments'); + return null; + } + + const method = httpVerb; + const url = apiEndpoint; const headers = { 'Content-Type': 'application/json', - 'x-api-key': parameters.apiKey, + 'x-api-key': apiKey, }; const data = parameters.toGraphQLJson(); @@ -105,16 +116,22 @@ export class OdpClient implements IOdpClient { * It is recommended to retry if status code was 5xx. */ public async sendEvents(parameters: SendEventsParameters): Promise { - if (!parameters?.apiEndpoint || !parameters?.apiKey) { + const { apiEndpoint, apiKey, httpVerb, events } = parameters; + + if (events?.length === 0) { + return null; + } + + if (!apiEndpoint || !apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); return null; } - const method = parameters.httpVerb; - const url = parameters.apiEndpoint; + const method = httpVerb; + const url = apiEndpoint; const headers = { 'Content-Type': 'application/json', - 'x-api-key': parameters.apiKey, + 'x-api-key': apiKey, }; const data = parameters.toJson(); diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index 229685ef6..468710fd0 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -15,20 +15,20 @@ */ export class OdpEvent { - private readonly _type: string; + private readonly type: string; - private readonly _action: string; + private readonly action: string; - private readonly _identifiers: Map; + private readonly identifiers: Map; // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly _data: Map; + private readonly data: Map; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(type: string, action: string, identifiers: Map, data: Map) { - this._type = type; - this._action = action; - this._identifiers = identifiers; - this._data = data; + this.type = type; + this.action = action; + this.identifiers = identifiers; + this.data = data; } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts index 73db3b1ac..87ee97466 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts @@ -15,8 +15,8 @@ */ export interface IOdpRequestParameters { - apiKey: string | undefined; - apiEndpoint: string | undefined; + apiKey?: string; + apiEndpoint?: string; httpVerb: string; } @@ -24,12 +24,12 @@ export abstract class OdpRequestParameters implements IOdpRequestParameters { /** * Optimizely Data Platform API key */ - public apiKey: string | undefined; + public apiKey?: string; /** * Fully-qualified URL to ODP events endpoint */ - public apiEndpoint: string | undefined; + public apiEndpoint?: string; /** * HTTP Verb used to send request diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts index ae2ffd3aa..2f7eab785 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -23,17 +23,17 @@ export class QuerySegmentsParameters extends OdpRequestParameters { /** * 'vuid' or 'fs_user_id' (client device id or fullstack id) */ - public userKey: string | undefined; + public userKey?: string; /** * Value for the user key */ - public userValue: string | undefined; + public userValue?: string; /** * Audience segments to check for inclusion in the experiment */ - public segmentsToCheck: string[] | undefined; + public segmentsToCheck?: string[]; /** * HTTP Verb used to send request diff --git a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts index f53ba965c..879356a5e 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts @@ -24,7 +24,7 @@ export class SendEventsParameters extends OdpRequestParameters { /** * Collection of ODP events to transmit */ - public events: OdpEvent[] | undefined; + public events?: OdpEvent[]; /** * HTTP Verb used to send request diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index 4a877c1bc..b9f71780a 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -140,6 +140,16 @@ describe('GraphQLManager', () => { verify(mockLogger.log(anything(), anyString())).never(); }); + it('should handle a request to query no segments', async () => { + const manager = makeManagerInstance(); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, []); + + expect(segments).toHaveLength(0); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + it('should handle empty qualified segments', async () => { const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; diff --git a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts index 8530ac9d1..3d2222874 100644 --- a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts +++ b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts @@ -78,7 +78,7 @@ describe('OdpClient Query Segments', () => { resetCalls(mockNodeRequestHandler); }); - it('should handle missing API Host', async () => { + it('should handle missing API Endpoint', async () => { const missingApiEndpoint = new QuerySegmentsParameters({ apiKey: 'apiKey', apiEndpoint: '', @@ -97,7 +97,7 @@ describe('OdpClient Query Segments', () => { it('should handle missing API Key', async () => { const missingApiHost = new QuerySegmentsParameters({ apiKey: '', - apiEndpoint: 'apiHost', + apiEndpoint: 'apiEndpoint', userKey: 'userKey', userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], @@ -110,6 +110,55 @@ describe('OdpClient Query Segments', () => { verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); + it('should handle missing User Key', async () => { + const missingApiEndpoint = new QuerySegmentsParameters({ + apiKey: 'apiKey', + apiEndpoint: 'apiEndpoint', + userKey: '', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + await client.querySegments(missingApiEndpoint); + + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No UserKey or UserValue set before querying segments')).once(); + }); + + it('should handle missing User Value', async () => { + const missingApiHost = new QuerySegmentsParameters({ + apiKey: 'apiKey', + apiEndpoint: 'apiEndpoint', + userKey: 'userKey', + userValue: '', + segmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + await client.querySegments(missingApiHost); + + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'No UserKey or UserValue set before querying segments')).once(); + }); + + it('should handle no segments being requested', async () => { + const missingApiHost = new QuerySegmentsParameters({ + apiKey: 'apiKey', + apiEndpoint: 'apiEndpoint', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: [], + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const json = await client.querySegments(missingApiHost); + + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + expect(json).toBe(''); + }); + it('Browser: should get mocked segments successfully', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { diff --git a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts index 0a4844236..dd854bfa8 100644 --- a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts +++ b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts @@ -34,6 +34,7 @@ const MOCK_SEND_PARAMETERS = new SendEventsParameters({ key11: 'value-1', key12: true, key13: 3.5, + key14: null, }))), new OdpEvent('t2', 'a2', new Map([['id-key-2', 'id-value-2']]), @@ -49,12 +50,14 @@ describe('OdpClient Send Events', () => { let mockLogger: LogHandler; let mockBrowserRequestHandler: BrowserRequestHandler; let mockNodeRequestHandler: NodeRequestHandler; + let mockOdpEvent: OdpEvent; beforeAll(() => { mockErrorHandler = mock(); mockLogger = mock(); mockBrowserRequestHandler = mock(); mockNodeRequestHandler = mock(); + mockOdpEvent = mock(); }); beforeEach(() => { @@ -62,13 +65,14 @@ describe('OdpClient Send Events', () => { resetCalls(mockLogger); resetCalls(mockBrowserRequestHandler); resetCalls(mockNodeRequestHandler); + resetCalls(mockOdpEvent); }); it('should handle missing API Endpoint', async () => { const missingApiEndpoint = new SendEventsParameters({ apiKey: 'apiKey', apiEndpoint: '', - events: [], + events: [instance(mockOdpEvent)], }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); @@ -80,10 +84,11 @@ describe('OdpClient Send Events', () => { }); it('should handle missing API Key', async () => { + const missingApiKey = new SendEventsParameters({ apiKey: '', apiEndpoint: 'https://some.example.com/endpoint', - events: [], + events: [instance(mockOdpEvent)], }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); From c18637e4216a6747f31640b448494503ce443325 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 16 Sep 2022 16:10:33 -0400 Subject: [PATCH 08/35] Code review resolutions --- .../lib/plugins/odp/graphql_manager.ts | 17 ++++++++++++----- .../lib/plugins/odp/odp_client.ts | 16 ++++++++-------- .../optimizely-sdk/tests/graphQLManager.spec.ts | 9 +++++---- .../optimizely-sdk/tests/odpSendEvents.spec.ts | 3 --- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index eca44de59..eace8f60c 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -39,7 +39,7 @@ const EMPTY_JSON_RESPONSE = null; * Manager for communicating with the Optimizely Data Platform GraphQL endpoint */ export interface IGraphQLManager { - fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise; + fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise; } /** @@ -73,10 +73,12 @@ export class GraphqlManager implements IGraphQLManager { * @param userValue Associated value to query for the user key * @param segmentsToCheck Audience segments to check for experiment inclusion */ - public async fetchSegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { - if (segmentsToCheck?.length === 0) { - return EMPTY_SEGMENTS_COLLECTION; + public async fetchSegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { + if (!apiEndpoint || !apiKey) { + this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to fetch segments'); + return null; } + const parameters = new QuerySegmentsParameters({ apiKey, apiEndpoint, @@ -84,16 +86,21 @@ export class GraphqlManager implements IGraphQLManager { userValue, segmentsToCheck, }); + const segmentsResponse = await this._odpClient.querySegments(parameters); if (!segmentsResponse) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); + return null; + } + + if (segmentsToCheck?.length === 0) { return EMPTY_SEGMENTS_COLLECTION; } const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); - return EMPTY_SEGMENTS_COLLECTION; + return null; } if (parsedSegments.errors?.length > 0) { diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index fba9b5694..fd60a0003 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -69,10 +69,6 @@ export class OdpClient implements IOdpClient { public async querySegments(parameters: QuerySegmentsParameters): Promise { const { apiEndpoint, apiKey, httpVerb, userKey, userValue, segmentsToCheck } = parameters; - if (segmentsToCheck?.length === 0) { - return ''; - } - if (!apiEndpoint || !apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); return null; @@ -83,6 +79,10 @@ export class OdpClient implements IOdpClient { return null; } + if (segmentsToCheck?.length === 0) { + return ''; + } + const method = httpVerb; const url = apiEndpoint; const headers = { @@ -118,17 +118,17 @@ export class OdpClient implements IOdpClient { public async sendEvents(parameters: SendEventsParameters): Promise { const { apiEndpoint, apiKey, httpVerb, events } = parameters; - if (events?.length === 0) { + if (!apiEndpoint || !apiKey) { + this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); return null; } - if (!apiEndpoint || !apiKey) { - this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); + if (events?.length === 0) { return null; } const method = httpVerb; - const url = apiEndpoint; + const url = apiEndpoint ?? ''; const headers = { 'Content-Type': 'application/json', 'x-api-key': apiKey, diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index b9f71780a..55fa729d0 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -51,6 +51,7 @@ describe('GraphQLManager', () => { resetCalls(mockOdpClient); }); + it('should parse a successful response', () => { const validJsonResponse = `{ "data": { @@ -187,7 +188,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(0); + expect(segments).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); @@ -202,7 +203,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(0); + expect(segments).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).once(); }); @@ -214,7 +215,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(0); + expect(segments).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); @@ -225,7 +226,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(0); + expect(segments).toBeNull(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); diff --git a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts index dd854bfa8..000bdd6e3 100644 --- a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts +++ b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts @@ -80,11 +80,9 @@ describe('OdpClient Send Events', () => { expect(statusReturned).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); }); it('should handle missing API Key', async () => { - const missingApiKey = new SendEventsParameters({ apiKey: '', apiEndpoint: 'https://some.example.com/endpoint', @@ -96,7 +94,6 @@ describe('OdpClient Send Events', () => { expect(statusReturned).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events')).once(); }); it('Browser: should send events successfully', async () => { From fed5e9b11f94f4a68fa072a63b437f8d72af60d1 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 19 Sep 2022 09:40:36 -0400 Subject: [PATCH 09/35] Validation refactoring per code review --- .../lib/plugins/odp/graphql_manager.ts | 16 +-- .../lib/plugins/odp/odp_client.ts | 27 +--- .../lib/plugins/odp/odp_request_parameters.ts | 20 ++- .../plugins/odp/query_segments_parameters.ts | 26 ++-- .../lib/plugins/odp/send_events_parameters.ts | 16 +-- .../tests/odpQuerySegments.spec.ts | 135 ++++++++---------- .../tests/odpSendEvents.spec.ts | 50 +++---- 7 files changed, 129 insertions(+), 161 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index eace8f60c..4498339a6 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -74,29 +74,23 @@ export class GraphqlManager implements IGraphQLManager { * @param segmentsToCheck Audience segments to check for experiment inclusion */ public async fetchSegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { - if (!apiEndpoint || !apiKey) { - this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to fetch segments'); - return null; + if (segmentsToCheck?.length === 0) { + return EMPTY_SEGMENTS_COLLECTION; } - const parameters = new QuerySegmentsParameters({ + const parameters = new QuerySegmentsParameters( apiKey, apiEndpoint, userKey, userValue, segmentsToCheck, - }); - + ); const segmentsResponse = await this._odpClient.querySegments(parameters); if (!segmentsResponse) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); return null; } - if (segmentsToCheck?.length === 0) { - return EMPTY_SEGMENTS_COLLECTION; - } - const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); @@ -131,6 +125,8 @@ export class GraphqlManager implements IGraphQLManager { try { jsonObject = JSON.parse(jsonResponse); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index fd60a0003..f3cfaac39 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -67,21 +67,7 @@ export class OdpClient implements IOdpClient { * @returns JSON response string from ODP or null */ public async querySegments(parameters: QuerySegmentsParameters): Promise { - const { apiEndpoint, apiKey, httpVerb, userKey, userValue, segmentsToCheck } = parameters; - - if (!apiEndpoint || !apiKey) { - this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); - return null; - } - - if (!userKey || !userValue) { - this._logger.log(LogLevel.ERROR, 'No UserKey or UserValue set before querying segments'); - return null; - } - - if (segmentsToCheck?.length === 0) { - return ''; - } + const { apiEndpoint, apiKey, httpVerb } = parameters; const method = httpVerb; const url = apiEndpoint; @@ -95,6 +81,8 @@ export class OdpClient implements IOdpClient { try { const request = this._requestHandler.makeRequest(url, headers, method, data); response = await request.responsePromise; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); @@ -118,17 +106,12 @@ export class OdpClient implements IOdpClient { public async sendEvents(parameters: SendEventsParameters): Promise { const { apiEndpoint, apiKey, httpVerb, events } = parameters; - if (!apiEndpoint || !apiKey) { - this._logger.log(LogLevel.ERROR, 'No ApiEndpoint or ApiKey set before attempting to send ODP events'); - return null; - } - if (events?.length === 0) { return null; } const method = httpVerb; - const url = apiEndpoint ?? ''; + const url = apiEndpoint; const headers = { 'Content-Type': 'application/json', 'x-api-key': apiKey, @@ -139,6 +122,8 @@ export class OdpClient implements IOdpClient { try { const request = this._requestHandler.makeRequest(url, headers, method, data); response = await request.responsePromise; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts index 87ee97466..f5c4c42ce 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts @@ -15,8 +15,8 @@ */ export interface IOdpRequestParameters { - apiKey?: string; - apiEndpoint?: string; + apiKey: string; + apiEndpoint: string; httpVerb: string; } @@ -24,15 +24,25 @@ export abstract class OdpRequestParameters implements IOdpRequestParameters { /** * Optimizely Data Platform API key */ - public apiKey?: string; + public readonly apiKey: string; /** * Fully-qualified URL to ODP events endpoint */ - public apiEndpoint?: string; + public readonly apiEndpoint: string; /** * HTTP Verb used to send request */ - public abstract readonly httpVerb: string; + public readonly httpVerb: string; + + protected constructor(apiKey: string, apiEndpoint: string, httpVerb: string) { + if (!apiEndpoint || !apiKey) { + throw new Error('Parameters apiKey and apiEndpoint are required'); + } + + this.apiKey = apiKey; + this.apiEndpoint = apiEndpoint; + this.httpVerb = httpVerb ?? 'POST'; + } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts index 2f7eab785..78718805d 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -23,26 +23,32 @@ export class QuerySegmentsParameters extends OdpRequestParameters { /** * 'vuid' or 'fs_user_id' (client device id or fullstack id) */ - public userKey?: string; + public readonly userKey: string; /** * Value for the user key */ - public userValue?: string; + public readonly userValue: string; /** * Audience segments to check for inclusion in the experiment */ - public segmentsToCheck?: string[]; + public readonly segmentsToCheck: string[]; - /** - * HTTP Verb used to send request - */ - public readonly httpVerb = 'POST'; + constructor(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]) { + super(apiKey, apiEndpoint, 'POST'); + + if (!userKey || !userValue) { + throw new Error('Parameters userKey or userValue are required'); + } + + if (segmentsToCheck.length < 1) { + throw new Error('Parameter segmentsToCheck must have elements'); + } - constructor(parameters: { apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[] }) { - super(); - Object.assign(this, parameters); + this.userKey = userKey; + this.userValue = userValue; + this.segmentsToCheck = segmentsToCheck; } /** diff --git a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts index 879356a5e..879ba6526 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts @@ -24,16 +24,16 @@ export class SendEventsParameters extends OdpRequestParameters { /** * Collection of ODP events to transmit */ - public events?: OdpEvent[]; + public readonly events: OdpEvent[]; - /** - * HTTP Verb used to send request - */ - public httpVerb = 'POST'; + constructor(apiKey: string, apiEndpoint: string, events: OdpEvent[]) { + super(apiKey, apiEndpoint, 'POST'); + + if (OdpEvent.length < 1) { + throw new Error('Parameter events must have elements'); + } - constructor(parameters: { apiKey: string, apiEndpoint: string, events: OdpEvent[] }) { - super(); - Object.assign(this, parameters); + this.events = events; } /** diff --git a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts index 3d2222874..c2cbbc878 100644 --- a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts +++ b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts @@ -23,17 +23,17 @@ import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_param import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ - apiKey: 'not-real-api-key', - apiEndpoint: 'https://api.example.com/v3/graphql', - userKey: 'fs_user_id', - userValue: 'mock-user-id', - segmentsToCheck: [ +const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters( + 'not-real-api-key', + 'https://api.example.com/v3/graphql', + 'fs_user_id', + 'mock-user-id', + [ 'has_email', 'has_email_opted_in', 'push_on_sale', ], -}); +); const VALID_RESPONSE_JSON = { 'data': { 'customer': { @@ -78,85 +78,64 @@ describe('OdpClient Query Segments', () => { resetCalls(mockNodeRequestHandler); }); - it('should handle missing API Endpoint', async () => { - const missingApiEndpoint = new QuerySegmentsParameters({ - apiKey: 'apiKey', - apiEndpoint: '', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - await client.querySegments(missingApiEndpoint); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); + it('should handle missing API Endpoint', () => { + expect(() => { + new QuerySegmentsParameters( + 'apiKey', + '', + 'userKey', + 'userValue', + ['segmentToCheck'], + ); + }).toThrow('Parameters apiKey and apiEndpoint are required'); }); - it('should handle missing API Key', async () => { - const missingApiHost = new QuerySegmentsParameters({ - apiKey: '', - apiEndpoint: 'apiEndpoint', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - await client.querySegments(missingApiHost); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); + it('should handle missing API Key', () => { + expect(() => { + new QuerySegmentsParameters( + '', + 'apiEndpoint', + 'userKey', + 'userValue', + ['segmentToCheck'], + ); + }).toThrow('Parameters apiKey and apiEndpoint are required'); }); - it('should handle missing User Key', async () => { - const missingApiEndpoint = new QuerySegmentsParameters({ - apiKey: 'apiKey', - apiEndpoint: 'apiEndpoint', - userKey: '', - userValue: 'userValue', - segmentsToCheck: ['segmentToCheck'], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - await client.querySegments(missingApiEndpoint); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No UserKey or UserValue set before querying segments')).once(); + it('should handle missing User Key', () => { + expect(() => { + new QuerySegmentsParameters( + 'apiKey', + 'apiEndpoint', + '', + 'userValue', + ['segmentToCheck'], + ); + }).toThrow('Parameters userKey or userValue are required'); }); - it('should handle missing User Value', async () => { - const missingApiHost = new QuerySegmentsParameters({ - apiKey: 'apiKey', - apiEndpoint: 'apiEndpoint', - userKey: 'userKey', - userValue: '', - segmentsToCheck: ['segmentToCheck'], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - await client.querySegments(missingApiHost); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(LogLevel.ERROR, 'No UserKey or UserValue set before querying segments')).once(); + it('should handle missing User Value', () => { + expect(() => { + new QuerySegmentsParameters( + 'apiKey', + 'apiEndpoint', + 'userKey', + '', + ['segmentToCheck'], + ); + }).toThrow('Parameters userKey or userValue are required'); }); - it('should handle no segments being requested', async () => { - const missingApiHost = new QuerySegmentsParameters({ - apiKey: 'apiKey', - apiEndpoint: 'apiEndpoint', - userKey: 'userKey', - userValue: 'userValue', - segmentsToCheck: [], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const json = await client.querySegments(missingApiHost); - - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); - expect(json).toBe(''); + it('should handle no segments being requested', () => { + expect(() => { + new QuerySegmentsParameters( + 'apiKey', + 'apiEndpoint', + 'userKey', + 'userValue', + [], + ); + }).toThrow('Parameter segmentsToCheck must have elements'); }); it('Browser: should get mocked segments successfully', async () => { diff --git a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts index 000bdd6e3..6377a47a3 100644 --- a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts +++ b/packages/optimizely-sdk/tests/odpSendEvents.spec.ts @@ -24,10 +24,10 @@ import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_reque import { SendEventsParameters } from '../lib/plugins/odp/send_events_parameters'; import { OdpEvent } from '../lib/plugins/odp/odp_event'; -const MOCK_SEND_PARAMETERS = new SendEventsParameters({ - apiKey: 'not-real-api-key', - apiEndpoint: 'https://events.example.com/v2/api', - events: [ +const MOCK_SEND_PARAMETERS = new SendEventsParameters( + 'not-real-api-key', + 'https://events.example.com/v2/api', + [ new OdpEvent('t1', 'a1', new Map([['id-key-1', 'id-value-1']]), new Map(Object.entries({ @@ -42,7 +42,7 @@ const MOCK_SEND_PARAMETERS = new SendEventsParameters({ key2: 'value-2', }))), ], -}); +); const VALID_RESPONSE_CODE = 200; describe('OdpClient Send Events', () => { @@ -68,32 +68,24 @@ describe('OdpClient Send Events', () => { resetCalls(mockOdpEvent); }); - it('should handle missing API Endpoint', async () => { - const missingApiEndpoint = new SendEventsParameters({ - apiKey: 'apiKey', - apiEndpoint: '', - events: [instance(mockOdpEvent)], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const statusReturned = await client.sendEvents(missingApiEndpoint); - - expect(statusReturned).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); + it('should handle missing API Endpoint', () => { + expect(() => { + new SendEventsParameters( + 'apiKey', + '', + [instance(mockOdpEvent)], + ); + }).toThrow('Parameters apiKey and apiEndpoint are required'); }); - it('should handle missing API Key', async () => { - const missingApiKey = new SendEventsParameters({ - apiKey: '', - apiEndpoint: 'https://some.example.com/endpoint', - events: [instance(mockOdpEvent)], - }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - - const statusReturned = await client.sendEvents(missingApiKey); - - expect(statusReturned).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); + it('should handle missing API Key', () => { + expect(() => { + new SendEventsParameters( + '', + 'https://some.example.com/endpoint', + [instance(mockOdpEvent)], + ); + }).toThrow('Parameters apiKey and apiEndpoint are required'); }); it('Browser: should send events successfully', async () => { From f40bd80a8c8ef23dc6aed25fb932a99d33d4dbe7 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 12:08:36 -0400 Subject: [PATCH 10/35] Replace execution context --- packages/optimizely-sdk/lib/index.browser.ts | 4 -- packages/optimizely-sdk/lib/index.node.ts | 4 -- .../optimizely-sdk/lib/index.react_native.ts | 4 -- .../optimizely-sdk/lib/utils/enums/index.ts | 17 ++++--- .../lib/utils/execution_context/index.ts | 45 ------------------- .../request_handler_factory.ts | 15 +++---- 6 files changed, 18 insertions(+), 71 deletions(-) delete mode 100644 packages/optimizely-sdk/lib/utils/execution_context/index.ts diff --git a/packages/optimizely-sdk/lib/index.browser.ts b/packages/optimizely-sdk/lib/index.browser.ts index 5749ebf43..c8532d7cd 100644 --- a/packages/optimizely-sdk/lib/index.browser.ts +++ b/packages/optimizely-sdk/lib/index.browser.ts @@ -32,8 +32,6 @@ import { createNotificationCenter } from './core/notification_center'; import { default as eventProcessor } from './plugins/event_processor'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; -import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; -import { ExecutionContext } from './utils/execution_context'; const logger = getLogger(); logHelper.setLogHandler(loggerPlugin.createLogger()); @@ -46,8 +44,6 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; let hasRetriedEvents = false; -ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER; - /** * Creates an instance of the Optimizely class * @param {Config} config diff --git a/packages/optimizely-sdk/lib/index.node.ts b/packages/optimizely-sdk/lib/index.node.ts index 6fe59eb22..56ba69c71 100644 --- a/packages/optimizely-sdk/lib/index.node.ts +++ b/packages/optimizely-sdk/lib/index.node.ts @@ -32,8 +32,6 @@ import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; -import { ExecutionContext } from './utils/execution_context'; -import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; const logger = getLogger(); setLogLevel(LogLevel.ERROR); @@ -42,8 +40,6 @@ const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 30000; // Unit is ms, default is 30s const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; -ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.NODE; - /** * Creates an instance of the Optimizely class * @param {Config} config diff --git a/packages/optimizely-sdk/lib/index.react_native.ts b/packages/optimizely-sdk/lib/index.react_native.ts index cece646f2..b2c67bac8 100644 --- a/packages/optimizely-sdk/lib/index.react_native.ts +++ b/packages/optimizely-sdk/lib/index.react_native.ts @@ -32,8 +32,6 @@ import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor/index.react_native'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; -import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; -import { ExecutionContext } from './utils/execution_context'; const logger = getLogger(); setLogHandler(loggerPlugin.createLogger()); @@ -43,8 +41,6 @@ const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; -ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER; - /** * Creates an instance of the Optimizely class * @param {Config} config diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index e9eecdcf4..f3e7dd1e4 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -292,12 +292,19 @@ export enum NOTIFICATION_TYPES { TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', } +/** + * ODP User Key + */ +export enum ODP_USER_KEY { + VUID = 'vuid', + FS_USER_ID = 'fs_user_id', +} /** - * Valid types of Javascript contexts in which this code is executing + * Possible states of ODP integration */ -export enum EXECUTION_CONTEXT_TYPE { - NOT_DEFINED, - BROWSER, - NODE, +export enum ODP_CONFIG_STATE { + UNDETERMINED = 0, + INTEGRATED, + NOT_INTEGRATED = 2, } diff --git a/packages/optimizely-sdk/lib/utils/execution_context/index.ts b/packages/optimizely-sdk/lib/utils/execution_context/index.ts deleted file mode 100644 index f9f3e2457..000000000 --- a/packages/optimizely-sdk/lib/utils/execution_context/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { EXECUTION_CONTEXT_TYPE } from '../enums'; - -/** - * Determine the running or execution context for JavaScript - * Note: React Native is considered a browser context - */ -export class ExecutionContext { - /** - * Holds the current value of the execution context - * @private - */ - private static _currentContext: EXECUTION_CONTEXT_TYPE = EXECUTION_CONTEXT_TYPE.NOT_DEFINED; - - /** - * Gets the current running context - * @constructor - */ - public static get Current(): EXECUTION_CONTEXT_TYPE { - return this._currentContext; - } - - /** - * Sets the current running context ideally from package initialization - * @param newValue The new execution context - * @constructor - */ - public static set Current(newValue: EXECUTION_CONTEXT_TYPE) { - this._currentContext = newValue; - } -} diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts index 455754cb5..ddab23faf 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts @@ -18,21 +18,18 @@ import { LogHandler } from '../../modules/logging'; import { RequestHandler } from './http'; import { NodeRequestHandler } from './node_request_handler'; import { BrowserRequestHandler } from './browser_request_handler'; -import { ExecutionContext } from '../execution_context'; -import { EXECUTION_CONTEXT_TYPE } from '../enums'; /** * Factory to create the appropriate type of RequestHandler based on a provided context */ export class RequestHandlerFactory { public static createHandler(logger: LogHandler, timeout?: number): RequestHandler { - switch (ExecutionContext.Current) { - case EXECUTION_CONTEXT_TYPE.BROWSER: - return new BrowserRequestHandler(logger, timeout); - case EXECUTION_CONTEXT_TYPE.NODE: - return new NodeRequestHandler(logger, timeout); - default: - return null as unknown as RequestHandler; + if (window) { + return new BrowserRequestHandler(logger, timeout); + } else if (process) { + return new NodeRequestHandler(logger, timeout); + } else { + return null as unknown as RequestHandler; } } } From bbd16856cdd3b5e6d374bc6363f53c32804f9888 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 12:11:44 -0400 Subject: [PATCH 11/35] Ready OdpEvent for EventManager --- .../lib/plugins/odp/odp_event.ts | 81 ++++++++++++++++--- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index 468710fd0..ff7b7b0c6 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -13,22 +13,79 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ODP_CONFIG_STATE } from '../../utils/enums'; -export class OdpEvent { - private readonly type: string; +export class OdpConfig { + /** + * Host of ODP audience segments API. + * @private + */ + private _apiHost: string; - private readonly action: string; + /** + * Getter to retrieve the ODP server host + * @public + */ + get apiHost(): string { + return this._apiHost; + } + + /** + * Public API key for the ODP account from which the audience segments will be fetched (optional). + * @private + */ + private _apiKey: string; + + /** + * Getter to retrieve the ODP API key + * @public + */ + get apiKey(): string { + return this._apiKey; + } + + /** + * All ODP segments used in the current datafile (associated with apiHost/apiKey). + * @private + */ + private _segmentsToCheck: string[]; + + /** + * Getter for ODP segments to check + * @public + */ + get segmentsToCheck(): string[] { + return this._segmentsToCheck; + } + + /** + * Indicates whether ODP is integrated for the project + * @private + */ + private _odpServiceIntegrated = ODP_CONFIG_STATE.UNDETERMINED; + + constructor(apiKey: string, apiHost: string, segmentsToCheck: string[]) { + this._apiKey = apiKey; + this._apiHost = apiHost; + this._segmentsToCheck = segmentsToCheck; + this._odpServiceIntegrated = this._apiKey && this._apiHost ? ODP_CONFIG_STATE.INTEGRATED : ODP_CONFIG_STATE.UNDETERMINED; + } + + public update(apiKey: string, apiEndpoint: string, segmentsToCheck: string[]): boolean { + this._odpServiceIntegrated = apiKey && apiEndpoint ? ODP_CONFIG_STATE.INTEGRATED : ODP_CONFIG_STATE.NOT_INTEGRATED; - private readonly identifiers: Map; + if (this._apiKey === apiKey && this._apiHost === apiEndpoint && this._segmentsToCheck === segmentsToCheck) { + return false; + } else { + this._apiKey = apiKey; + this._apiHost = apiEndpoint; + this._segmentsToCheck = segmentsToCheck; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly data: Map; + return true; + } + } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(type: string, action: string, identifiers: Map, data: Map) { - this.type = type; - this.action = action; - this.identifiers = identifiers; - this.data = data; + public isReady(): boolean { + return this._apiKey !== null && this._apiKey !== '' && this._apiHost !== null && this._apiHost !== ''; } } From ec2506490fde90106e6de8984bf8e7883abb89f8 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 14:14:46 -0400 Subject: [PATCH 12/35] Remove parameter wraps --- .../lib/plugins/odp/odp_request_parameters.ts | 48 ------------- .../plugins/odp/query_segments_parameters.ts | 70 ------------------- .../lib/plugins/odp/send_events_parameters.ts | 45 ------------ 3 files changed, 163 deletions(-) delete mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts delete mode 100644 packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts delete mode 100644 packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts deleted file mode 100644 index f5c4c42ce..000000000 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_request_parameters.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface IOdpRequestParameters { - apiKey: string; - apiEndpoint: string; - httpVerb: string; -} - -export abstract class OdpRequestParameters implements IOdpRequestParameters { - /** - * Optimizely Data Platform API key - */ - public readonly apiKey: string; - - /** - * Fully-qualified URL to ODP events endpoint - */ - public readonly apiEndpoint: string; - - /** - * HTTP Verb used to send request - */ - public readonly httpVerb: string; - - protected constructor(apiKey: string, apiEndpoint: string, httpVerb: string) { - if (!apiEndpoint || !apiKey) { - throw new Error('Parameters apiKey and apiEndpoint are required'); - } - - this.apiKey = apiKey; - this.apiEndpoint = apiEndpoint; - this.httpVerb = httpVerb ?? 'POST'; - } -} diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts deleted file mode 100644 index 78718805d..000000000 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { OdpRequestParameters } from './odp_request_parameters'; - -/** - * Handles parameters used in querying ODP segments - */ -export class QuerySegmentsParameters extends OdpRequestParameters { - /** - * 'vuid' or 'fs_user_id' (client device id or fullstack id) - */ - public readonly userKey: string; - - /** - * Value for the user key - */ - public readonly userValue: string; - - /** - * Audience segments to check for inclusion in the experiment - */ - public readonly segmentsToCheck: string[]; - - constructor(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]) { - super(apiKey, apiEndpoint, 'POST'); - - if (!userKey || !userValue) { - throw new Error('Parameters userKey or userValue are required'); - } - - if (segmentsToCheck.length < 1) { - throw new Error('Parameter segmentsToCheck must have elements'); - } - - this.userKey = userKey; - this.userValue = userValue; - this.segmentsToCheck = segmentsToCheck; - } - - /** - * Converts the QuerySegmentsParameters to a GraphQL JSON payload - * @returns GraphQL JSON string - */ - public toGraphQLJson(): string { - const segmentsArrayJson = JSON.stringify(this.segmentsToCheck); - - const json: string[] = []; - json.push('{"query" : "query {customer"'); - json.push(`(${this.userKey} : "${this.userValue}") `); - json.push('{audiences'); - json.push(`(subset: ${segmentsArrayJson})`); - json.push('{edges {node {name state}}}}}"}'); - - return json.join(''); - } -} diff --git a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts deleted file mode 100644 index 879ba6526..000000000 --- a/packages/optimizely-sdk/lib/plugins/odp/send_events_parameters.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { OdpEvent } from './odp_event'; -import { OdpRequestParameters } from './odp_request_parameters'; - -/** - * Handles parameters used in send ODP events - */ -export class SendEventsParameters extends OdpRequestParameters { - /** - * Collection of ODP events to transmit - */ - public readonly events: OdpEvent[]; - - constructor(apiKey: string, apiEndpoint: string, events: OdpEvent[]) { - super(apiKey, apiEndpoint, 'POST'); - - if (OdpEvent.length < 1) { - throw new Error('Parameter events must have elements'); - } - - this.events = events; - } - - /** - * Convert events to JSON format - */ - public toJson(): string { - return JSON.stringify(this.events); - } -} From 6eb4f18db87491bc8c2836f4e787469e497ce4c6 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 14:15:25 -0400 Subject: [PATCH 13/35] Update OdpResponseSchema jsdoc --- packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index 98a16b4c3..a2d59ab16 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -17,7 +17,7 @@ import { JSONSchema4 } from 'json-schema'; /** - * ODP Response JSON Schema file used to validate the project json datafile + * JSON Schema used to validate the ODP GraphQL response */ export const OdpResponseSchema = { $schema: 'https://json-schema.org/draft/2019-09/schema', From 5134c6e46c4ea41edac77c2c81b097b0b263a95a Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 14:18:11 -0400 Subject: [PATCH 14/35] Correct GraphQL vs REST managers --- .../lib/plugins/odp/graphql_manager.ts | 39 ++++++--- .../lib/plugins/odp/odp_client.ts | 48 +++++------ .../lib/plugins/odp/odp_event.ts | 82 +++---------------- .../lib/plugins/odp/rest_api_manager.ts | 72 ++++++++++++++++ .../tests/graphQLManager.spec.ts | 14 ++-- .../tests/odpQuerySegments.spec.ts | 14 ++-- 6 files changed, 149 insertions(+), 120 deletions(-) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 4498339a6..2154dbfc6 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -19,8 +19,8 @@ import { Response } from './odp_types'; import { IOdpClient, OdpClient } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; -import { QuerySegmentsParameters } from './query_segments_parameters'; import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; +import { ODP_USER_KEY } from '../../utils/enums'; /** * Expected value for a qualified/valid segment @@ -43,7 +43,7 @@ export interface IGraphQLManager { } /** - * Concrete implementation for communicating with the Optimizely Data Platform GraphQL endpoint + * Concrete implementation for communicating with the ODP GraphQL endpoint */ export class GraphqlManager implements IGraphQLManager { private readonly _errorHandler: ErrorHandler; @@ -51,7 +51,7 @@ export class GraphqlManager implements IGraphQLManager { private readonly _odpClient: IOdpClient; /** - * Retrieves the audience segments from the Optimizely Data Platform (ODP) + * Communicates with Optimizely Data Platform's GraphQL endpoint * @param errorHandler Handler to record exceptions * @param logger Collect and record events/errors for this GraphQL implementation * @param client Client to use to send queries to ODP @@ -68,24 +68,20 @@ export class GraphqlManager implements IGraphQLManager { /** * Retrieves the audience segments from ODP * @param apiKey ODP public key - * @param apiEndpoint Fully-qualified URL of ODP + * @param apiHost Host of ODP endpoint * @param userKey 'vuid' or 'fs_user_id key' * @param userValue Associated value to query for the user key * @param segmentsToCheck Audience segments to check for experiment inclusion */ - public async fetchSegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { + public async fetchSegments(apiKey: string, apiHost: string, userKey: ODP_USER_KEY, userValue: string, segmentsToCheck: string[]): Promise { if (segmentsToCheck?.length === 0) { return EMPTY_SEGMENTS_COLLECTION; } - const parameters = new QuerySegmentsParameters( - apiKey, - apiEndpoint, - userKey, - userValue, - segmentsToCheck, - ); - const segmentsResponse = await this._odpClient.querySegments(parameters); + const endpoint = `${apiHost}/v3/events`; + const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); + + const segmentsResponse = await this._odpClient.querySegments(apiKey, endpoint, userKey, userValue, query); if (!segmentsResponse) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); return null; @@ -114,6 +110,23 @@ export class GraphqlManager implements IGraphQLManager { return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); } + /** + * Converts the query parameters to a GraphQL JSON payload + * @returns GraphQL JSON string + */ + private toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string { + const segmentsArrayJson = JSON.stringify(segmentsToCheck); + + const json: string[] = []; + json.push('{"query" : "query {customer"'); + json.push(`(${userKey} : "${userValue}") `); + json.push('{audiences'); + json.push(`(subset: ${segmentsArrayJson})`); + json.push('{edges {node {name state}}}}}"}'); + + return json.join(''); + } + /** * Parses JSON response * @param jsonResponse JSON response from ODP diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index f3cfaac39..c9730b095 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -15,10 +15,9 @@ */ import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; -import { QuerySegmentsParameters } from './query_segments_parameters'; import { RequestHandler, Response } from '../../utils/http_request_handler/http'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; -import { SendEventsParameters } from './send_events_parameters'; +import { ODP_USER_KEY } from '../../utils/enums'; /** * Standard message for audience querying fetch errors @@ -33,9 +32,9 @@ const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; * Interface for sending requests and handling responses to Optimizely Data Platform */ export interface IOdpClient { - querySegments(parameters: QuerySegmentsParameters): Promise; + querySegments(apiKey: string, graphQlEndpoint: string, userKey: ODP_USER_KEY, userValue: string, data: string): Promise; - sendEvents(parameters: SendEventsParameters): Promise; + sendEvents(apiKey: string, restApiEndpoint: string, data: string): Promise; } /** @@ -66,16 +65,24 @@ export class OdpClient implements IOdpClient { * @param parameters Query parameters to send to ODP * @returns JSON response string from ODP or null */ - public async querySegments(parameters: QuerySegmentsParameters): Promise { - const { apiEndpoint, apiKey, httpVerb } = parameters; - const method = httpVerb; - const url = apiEndpoint; + /** + * Handler for querying the ODP GraphQL endpoint + * @param apiKey ODP API key + * @param graphQlEndpoint Fully-qualified GraphQL endpoint URL + * @param userKey 'vuid' or 'fs_user_id' + * @param userValue userKey's value + * @param data GraphyQL query string + * @returns JSON response string from ODP or null + */ + public async querySegments(apiKey: string, graphQlEndpoint: string, userKey: string, userValue: string, data: string): Promise { + + const method = 'POST'; + const url = graphQlEndpoint; const headers = { 'Content-Type': 'application/json', 'x-api-key': apiKey, }; - const data = parameters.toGraphQLJson(); let response: Response; try { @@ -96,27 +103,20 @@ export class OdpClient implements IOdpClient { /** * Handler for sending ODP events - * @param parameters + * @param apiKey ODP API key + * @param restApiEndpoint Fully-qualified REST API endpoint URL + * @param data JSON event data payload * @returns - * 1. null, When there was a non-recoverable error and no retry is needed. - * 2. 0 If an unexpected error occurred and retrying can be useful. - * 3. HTTPStatus code if httpclient was able to make the request and was able to receive response. - * It is recommended to retry if status code was 5xx. + * 0 = If an unexpected error occurred and retrying can be useful + * Otherwise HTTPStatus code NOTE: it is recommended to retry if status code was 5xx. */ - public async sendEvents(parameters: SendEventsParameters): Promise { - const { apiEndpoint, apiKey, httpVerb, events } = parameters; - - if (events?.length === 0) { - return null; - } - - const method = httpVerb; - const url = apiEndpoint; + public async sendEvents(apiKey: string, restApiEndpoint: string, data: string): Promise { + const method = 'POST'; + const url = restApiEndpoint; const headers = { 'Content-Type': 'application/json', 'x-api-key': apiKey, }; - const data = parameters.toJson(); let response: Response; try { diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index ff7b7b0c6..c555abec9 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -13,79 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ODP_CONFIG_STATE } from '../../utils/enums'; -export class OdpConfig { - /** - * Host of ODP audience segments API. - * @private - */ - private _apiHost: string; +export class OdpEvent { + public type: string; - /** - * Getter to retrieve the ODP server host - * @public - */ - get apiHost(): string { - return this._apiHost; - } - - /** - * Public API key for the ODP account from which the audience segments will be fetched (optional). - * @private - */ - private _apiKey: string; - - /** - * Getter to retrieve the ODP API key - * @public - */ - get apiKey(): string { - return this._apiKey; - } - - /** - * All ODP segments used in the current datafile (associated with apiHost/apiKey). - * @private - */ - private _segmentsToCheck: string[]; - - /** - * Getter for ODP segments to check - * @public - */ - get segmentsToCheck(): string[] { - return this._segmentsToCheck; - } - - /** - * Indicates whether ODP is integrated for the project - * @private - */ - private _odpServiceIntegrated = ODP_CONFIG_STATE.UNDETERMINED; - - constructor(apiKey: string, apiHost: string, segmentsToCheck: string[]) { - this._apiKey = apiKey; - this._apiHost = apiHost; - this._segmentsToCheck = segmentsToCheck; - this._odpServiceIntegrated = this._apiKey && this._apiHost ? ODP_CONFIG_STATE.INTEGRATED : ODP_CONFIG_STATE.UNDETERMINED; - } - - public update(apiKey: string, apiEndpoint: string, segmentsToCheck: string[]): boolean { - this._odpServiceIntegrated = apiKey && apiEndpoint ? ODP_CONFIG_STATE.INTEGRATED : ODP_CONFIG_STATE.NOT_INTEGRATED; + public action: string; - if (this._apiKey === apiKey && this._apiHost === apiEndpoint && this._segmentsToCheck === segmentsToCheck) { - return false; - } else { - this._apiKey = apiKey; - this._apiHost = apiEndpoint; - this._segmentsToCheck = segmentsToCheck; + public identifiers: Map; - return true; - } - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public data: Map; - public isReady(): boolean { - return this._apiKey !== null && this._apiKey !== '' && this._apiHost !== null && this._apiHost !== ''; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(type: string, action: string, identifiers?: Map, data?: Map) { + this.type = type; + this.action = action; + this.identifiers = identifiers ?? new Map(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.data = data ?? new Map(); } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts new file mode 100644 index 000000000..bd7a32c87 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { ErrorHandler, LogHandler } from '../../modules/logging'; +import { IOdpClient, OdpClient } from './odp_client'; +import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; +import { OdpEvent } from './odp_event'; + +/** + * Manager for communicating with the Optimizely Data Platform REST API + */ +export interface IRestApiManager { + sendEvents(apiKey: string, apiHost: string, events: OdpEvent[]): Promise; +} + +/** + * Concrete implementation for accessing the ODP REST API + */ +export class ResetApiManager implements IRestApiManager { + private readonly errorHandler: ErrorHandler; + private readonly logger: LogHandler; + private readonly odpClient: IOdpClient; + + /** + * Creates instance to access Optimizely Data Platform (ODP) REST API + * @param errorHandler Handler to record exceptions + * @param logger Collect and record events/errors for this REST implementation + * @param client HTTP Client used to send data to ODP + */ + constructor(errorHandler: ErrorHandler, logger: LogHandler, client?: IOdpClient) { + this.errorHandler = errorHandler; + this.logger = logger; + + this.odpClient = client ?? new OdpClient(this.errorHandler, + this.logger, + RequestHandlerFactory.createHandler(this.logger)); + } + + /** + * Service for sending ODP events to REST API + * @param apiKey ODP public key + * @param apiHost Host of ODP endpoint + * @param events ODP events to send + * @returns Retry is true - if network or server error (5xx), otherwise false + */ + public async sendEvents(apiKey: string, apiHost: string, events: OdpEvent[]): Promise { + if (events.length === 0) { + return false; + } + + const endpoint = `${apiHost}/v3/events`; + const data = JSON.stringify(events); + + const statusCode = await this.odpClient.sendEvents(apiKey, endpoint, data); + + return statusCode === 0 || (statusCode >= 500 && statusCode < 600); + } +} diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index 55fa729d0..b3bb81c7e 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -129,7 +129,7 @@ describe('GraphQLManager', () => { '{"edges":[{"node":{"name":"has_email",' + '"state":"qualified"}},{"node":{"name":' + '"has_email_opted_in","state":"qualified"}}]}}}}'; - when(mockOdpClient.querySegments(anything())).thenResolve(responseJsonWithQualifiedSegments); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(responseJsonWithQualifiedSegments); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); @@ -154,7 +154,7 @@ describe('GraphQLManager', () => { it('should handle empty qualified segments', async () => { const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; - when(mockOdpClient.querySegments(anything())).thenResolve(responseJsonWithNoQualifiedSegments); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(responseJsonWithNoQualifiedSegments); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); @@ -172,7 +172,7 @@ describe('GraphQLManager', () => { '"locations":[{"line":1,"column":8}],"path":["customer"],' + '"extensions":{"classification":"DataFetchingException"}}],' + '"data":{"customer":null}}'; - when(mockOdpClient.querySegments(anything())).thenResolve(errorJsonResponse); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(errorJsonResponse); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); @@ -183,7 +183,7 @@ describe('GraphQLManager', () => { it('should handle unrecognized JSON responses', async () => { const unrecognizedJson = '{"unExpectedObject":{ "withSome": "value", "thatIsNotParseable": "true" }}'; - when(mockOdpClient.querySegments(anything())).thenResolve(unrecognizedJson); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(unrecognizedJson); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); @@ -198,7 +198,7 @@ describe('GraphQLManager', () => { 'UnknownArgument: Unknown field argument not_real_userKey @ ' + '\'customer\'","locations":[{"line":1,"column":17}],' + '"extensions":{"classification":"ValidationError"}}]}'; - when(mockOdpClient.querySegments(anything())).thenResolve(errorJsonResponse); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(errorJsonResponse); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); @@ -210,7 +210,7 @@ describe('GraphQLManager', () => { it('should handle bad responses', async () => { const badResponse = '{"data":{ }}'; - when(mockOdpClient.querySegments(anything())).thenResolve(badResponse); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(badResponse); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); @@ -221,7 +221,7 @@ describe('GraphQLManager', () => { }); it('should handle non 200 HTTP status code response', async () => { - when(mockOdpClient.querySegments(anything())).thenResolve(null); + when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(null); const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); diff --git a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts index c2cbbc878..153c30b14 100644 --- a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts +++ b/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts @@ -150,7 +150,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + const response = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck) ?? ''; expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); verify(mockErrorHandler.handleError(anything())).never(); @@ -169,7 +169,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + const response = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck) ?? ''; expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); verify(mockErrorHandler.handleError(anything())).never(); @@ -188,7 +188,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); @@ -207,7 +207,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); @@ -226,7 +226,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); @@ -245,7 +245,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); @@ -260,7 +260,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); From e3058584df95e3cef00e3d791ea8f276676d104c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 15:38:00 -0400 Subject: [PATCH 15/35] Update unit tests + refactors --- .../lib/plugins/odp/odp_client.ts | 44 +++---- .../optimizely-sdk/lib/utils/enums/index.ts | 8 ++ ...Manager.spec.ts => graphQlManager.spec.ts} | 32 ++--- ...spec.ts => odpClientQuerySegments.spec.ts} | 116 ++++-------------- ...ts.spec.ts => odpClientSendEvents.spec.ts} | 68 ++++------ 5 files changed, 90 insertions(+), 178 deletions(-) rename packages/optimizely-sdk/tests/{graphQLManager.spec.ts => graphQlManager.spec.ts} (82%) rename packages/optimizely-sdk/tests/{odpQuerySegments.spec.ts => odpClientQuerySegments.spec.ts} (60%) rename packages/optimizely-sdk/tests/{odpSendEvents.spec.ts => odpClientSendEvents.spec.ts} (68%) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index c9730b095..5c0830be5 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,7 +17,6 @@ import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; import { RequestHandler, Response } from '../../utils/http_request_handler/http'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; -import { ODP_USER_KEY } from '../../utils/enums'; /** * Standard message for audience querying fetch errors @@ -32,19 +31,19 @@ const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; * Interface for sending requests and handling responses to Optimizely Data Platform */ export interface IOdpClient { - querySegments(apiKey: string, graphQlEndpoint: string, userKey: ODP_USER_KEY, userValue: string, data: string): Promise; + querySegments(apiKey: string, graphQlEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise; - sendEvents(apiKey: string, restApiEndpoint: string, data: string): Promise; + sendEvents(apiKey: string, restApiEndpoint: string, jsonData: string): Promise; } /** * Http implementation for sending requests and handling responses to Optimizely Data Platform */ export class OdpClient implements IOdpClient { - private readonly _errorHandler: ErrorHandler; - private readonly _logger: LogHandler; - private readonly _timeout: number; - private readonly _requestHandler: RequestHandler; + private readonly errorHandler: ErrorHandler; + private readonly logger: LogHandler; + private readonly timeout: number; + private readonly requestHandler: RequestHandler; /** * An implementation for sending requests and handling responses to Optimizely Data Platform (ODP) @@ -54,29 +53,22 @@ export class OdpClient implements IOdpClient { * @param timeout Maximum milliseconds before requests are considered timed out */ constructor(errorHandler: ErrorHandler, logger: LogHandler, requestHandler: RequestHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._errorHandler = errorHandler; - this._logger = logger; - this._requestHandler = requestHandler; - this._timeout = timeout; + this.errorHandler = errorHandler; + this.logger = logger; + this.requestHandler = requestHandler; + this.timeout = timeout; } - /** - * Handler for querying the ODP GraphQL endpoint - * @param parameters Query parameters to send to ODP - * @returns JSON response string from ODP or null - */ - /** * Handler for querying the ODP GraphQL endpoint * @param apiKey ODP API key * @param graphQlEndpoint Fully-qualified GraphQL endpoint URL * @param userKey 'vuid' or 'fs_user_id' * @param userValue userKey's value - * @param data GraphyQL query string + * @param graphQlQuery GraphQL formatted query string * @returns JSON response string from ODP or null */ - public async querySegments(apiKey: string, graphQlEndpoint: string, userKey: string, userValue: string, data: string): Promise { - + public async querySegments(apiKey: string, graphQlEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise { const method = 'POST'; const url = graphQlEndpoint; const headers = { @@ -86,14 +78,14 @@ export class OdpClient implements IOdpClient { let response: Response; try { - const request = this._requestHandler.makeRequest(url, headers, method, data); + const request = this.requestHandler.makeRequest(url, headers, method, graphQlQuery); response = await request.responsePromise; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); + this.errorHandler.handleError(error); + this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); return null; } @@ -120,14 +112,14 @@ export class OdpClient implements IOdpClient { let response: Response; try { - const request = this._requestHandler.makeRequest(url, headers, method, data); + const request = this.requestHandler.makeRequest(url, headers, method, data); response = await request.responsePromise; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); + this.errorHandler.handleError(error); + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); return 0; } diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index f3e7dd1e4..2e37927df 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -308,3 +308,11 @@ export enum ODP_CONFIG_STATE { INTEGRATED, NOT_INTEGRATED = 2, } + +/** + * Flag for an execution context (browser or node) + */ +export enum EXECUTION_CONTEXT { + BROWSER = 'browser', + NODE = 'node', +} diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts similarity index 82% rename from packages/optimizely-sdk/tests/graphQLManager.spec.ts rename to packages/optimizely-sdk/tests/graphQlManager.spec.ts index b3bb81c7e..d65cb0e88 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -21,11 +21,11 @@ import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; import { GraphqlManager } from '../lib/plugins/odp/graphql_manager'; import { Response } from '../lib/plugins/odp/odp_types'; +import { ODP_USER_KEY } from '../lib/utils/enums'; describe('GraphQLManager', () => { const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; const ODP_GRAPHQL_URL = 'https://some.example.com/graphql/endpoint'; - const FS_USER_ID = 'fs_user_id'; const VALID_FS_USER_ID = 'tester-101'; const SEGMENTS_TO_CHECK = [ 'has_email', @@ -129,10 +129,10 @@ describe('GraphQLManager', () => { '{"edges":[{"node":{"name":"has_email",' + '"state":"qualified"}},{"node":{"name":' + '"has_email_opted_in","state":"qualified"}}]}}}}'; - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(responseJsonWithQualifiedSegments); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(responseJsonWithQualifiedSegments); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(2); expect(segments).toContain('has_email'); @@ -144,7 +144,7 @@ describe('GraphQLManager', () => { it('should handle a request to query no segments', async () => { const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, []); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, []); expect(segments).toHaveLength(0); verify(mockErrorHandler.handleError(anything())).never(); @@ -154,10 +154,10 @@ describe('GraphQLManager', () => { it('should handle empty qualified segments', async () => { const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(responseJsonWithNoQualifiedSegments); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(responseJsonWithNoQualifiedSegments); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); verify(mockErrorHandler.handleError(anything())).never(); @@ -172,9 +172,9 @@ describe('GraphQLManager', () => { '"locations":[{"line":1,"column":8}],"path":["customer"],' + '"extensions":{"classification":"DataFetchingException"}}],' + '"data":{"customer":null}}'; - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(errorJsonResponse); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(errorJsonResponse); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); verify(mockErrorHandler.handleError(anything())).never(); @@ -183,10 +183,10 @@ describe('GraphQLManager', () => { it('should handle unrecognized JSON responses', async () => { const unrecognizedJson = '{"unExpectedObject":{ "withSome": "value", "thatIsNotParseable": "true" }}'; - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(unrecognizedJson); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(unrecognizedJson); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); @@ -198,10 +198,10 @@ describe('GraphQLManager', () => { 'UnknownArgument: Unknown field argument not_real_userKey @ ' + '\'customer\'","locations":[{"line":1,"column":17}],' + '"extensions":{"classification":"ValidationError"}}]}'; - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(errorJsonResponse); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(errorJsonResponse); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); @@ -210,10 +210,10 @@ describe('GraphQLManager', () => { it('should handle bad responses', async () => { const badResponse = '{"data":{ }}'; - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(badResponse); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(badResponse); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockErrorHandler.handleError(anything())).never(); @@ -221,10 +221,10 @@ describe('GraphQLManager', () => { }); it('should handle non 200 HTTP status code response', async () => { - when(mockOdpClient.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck)).thenResolve(null); + when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(null); const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); diff --git a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts similarity index 60% rename from packages/optimizely-sdk/tests/odpQuerySegments.spec.ts rename to packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts index 153c30b14..94c6fc9c3 100644 --- a/packages/optimizely-sdk/tests/odpQuerySegments.spec.ts +++ b/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts @@ -19,21 +19,16 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpClient } from '../lib/plugins/odp/odp_client'; -import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; +import { EXECUTION_CONTEXT, ODP_USER_KEY } from '../lib/utils/enums'; + +const API_KEY = 'not-real-api-key'; +const GRAPHQL_ENDPOINT = 'https://api.example.com/v3/graphql'; +const USER_KEY = ODP_USER_KEY.FS_USER_ID; +const USER_VALUE = 'mock-user-id'; +const GRAPHQL_QUERY = `{"query" : "query {customer"(fs_user_id : "mock-user-id") {audiences(subset: [\\"has_email\\", \\"has_email_opted_in\\", \\"push_on_sale\\"] {edges {node {name state}}}}}"}`; -const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters( - 'not-real-api-key', - 'https://api.example.com/v3/graphql', - 'fs_user_id', - 'mock-user-id', - [ - 'has_email', - 'has_email_opted_in', - 'push_on_sale', - ], -); const VALID_RESPONSE_JSON = { 'data': { 'customer': { @@ -56,9 +51,14 @@ const VALID_RESPONSE_JSON = { }, }, }; + const BODY_FROM_ERROR = ''; describe('OdpClient Query Segments', () => { + const client = (type: EXECUTION_CONTEXT) => type === EXECUTION_CONTEXT.BROWSER ? + new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)) : + new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockBrowserRequestHandler: BrowserRequestHandler; @@ -78,67 +78,7 @@ describe('OdpClient Query Segments', () => { resetCalls(mockNodeRequestHandler); }); - it('should handle missing API Endpoint', () => { - expect(() => { - new QuerySegmentsParameters( - 'apiKey', - '', - 'userKey', - 'userValue', - ['segmentToCheck'], - ); - }).toThrow('Parameters apiKey and apiEndpoint are required'); - }); - - it('should handle missing API Key', () => { - expect(() => { - new QuerySegmentsParameters( - '', - 'apiEndpoint', - 'userKey', - 'userValue', - ['segmentToCheck'], - ); - }).toThrow('Parameters apiKey and apiEndpoint are required'); - }); - - it('should handle missing User Key', () => { - expect(() => { - new QuerySegmentsParameters( - 'apiKey', - 'apiEndpoint', - '', - 'userValue', - ['segmentToCheck'], - ); - }).toThrow('Parameters userKey or userValue are required'); - }); - - it('should handle missing User Value', () => { - expect(() => { - new QuerySegmentsParameters( - 'apiKey', - 'apiEndpoint', - 'userKey', - '', - ['segmentToCheck'], - ); - }).toThrow('Parameters userKey or userValue are required'); - }); - - it('should handle no segments being requested', () => { - expect(() => { - new QuerySegmentsParameters( - 'apiKey', - 'apiEndpoint', - 'userKey', - 'userValue', - [], - ); - }).toThrow('Parameter segmentsToCheck must have elements'); - }); - - it('Browser: should get mocked segments successfully', async () => { + it('(browser) should get mocked segments successfully', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -148,16 +88,15 @@ describe('OdpClient Query Segments', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const response = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck) ?? ''; + const response = await client(EXECUTION_CONTEXT.BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); - it('Node: should get mocked segments successfully', async () => { + it('(node) should get mocked segments successfully', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -167,16 +106,15 @@ describe('OdpClient Query Segments', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const response = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck) ?? ''; + const response = await client(EXECUTION_CONTEXT.NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); - it('Browser: should handle 400 HTTP response', async () => { + it('(browser) should handle 400 HTTP response', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -186,16 +124,15 @@ describe('OdpClient Query Segments', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); + const responseJson = await client(EXECUTION_CONTEXT.BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); - it('Node: should handle 400 HTTP response', async () => { + it('(node) should handle 400 HTTP response', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -205,16 +142,15 @@ describe('OdpClient Query Segments', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); + const responseJson = await client(EXECUTION_CONTEXT.NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); - it('Browser: should handle 500 HTTP response', async () => { + it('(browser) should handle 500 HTTP response', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -224,16 +160,15 @@ describe('OdpClient Query Segments', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); + const responseJson = await client(EXECUTION_CONTEXT.BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); - it('Node: should handle 500 HTTP response', async () => { + it('(node) should handle 500 HTTP response', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -243,9 +178,8 @@ describe('OdpClient Query Segments', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); + const responseJson = await client(EXECUTION_CONTEXT.NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); verify(mockErrorHandler.handleError(anything())).never(); @@ -260,7 +194,7 @@ describe('OdpClient Query Segments', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - const responseJson = await client.querySegments(apiKey, graphQlEndpoint, userKey, userValue, segmentsToCheck); + const responseJson = await client.querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBeNull(); verify(mockErrorHandler.handleError(anything())).once(); diff --git a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts similarity index 68% rename from packages/optimizely-sdk/tests/odpSendEvents.spec.ts rename to packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts index 6377a47a3..d265bfd5d 100644 --- a/packages/optimizely-sdk/tests/odpSendEvents.spec.ts +++ b/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts @@ -21,12 +21,12 @@ import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -import { SendEventsParameters } from '../lib/plugins/odp/send_events_parameters'; import { OdpEvent } from '../lib/plugins/odp/odp_event'; +import { EXECUTION_CONTEXT } from '../lib/utils/enums'; -const MOCK_SEND_PARAMETERS = new SendEventsParameters( - 'not-real-api-key', - 'https://events.example.com/v2/api', +const API_KEY = 'not-real-api-key'; +const REST_API_ENDPOINT = 'https://events.example.com/v2/api'; +const JSON_EVENT_DATA = JSON.stringify( [ new OdpEvent('t1', 'a1', new Map([['id-key-1', 'id-value-1']]), @@ -46,6 +46,10 @@ const MOCK_SEND_PARAMETERS = new SendEventsParameters( const VALID_RESPONSE_CODE = 200; describe('OdpClient Send Events', () => { + const client = (type: EXECUTION_CONTEXT) => type === EXECUTION_CONTEXT.BROWSER ? + new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)) : + new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockBrowserRequestHandler: BrowserRequestHandler; @@ -68,65 +72,43 @@ describe('OdpClient Send Events', () => { resetCalls(mockOdpEvent); }); - it('should handle missing API Endpoint', () => { - expect(() => { - new SendEventsParameters( - 'apiKey', - '', - [instance(mockOdpEvent)], - ); - }).toThrow('Parameters apiKey and apiEndpoint are required'); - }); - - it('should handle missing API Key', () => { - expect(() => { - new SendEventsParameters( - '', - 'https://some.example.com/endpoint', - [instance(mockOdpEvent)], - ); - }).toThrow('Parameters apiKey and apiEndpoint are required'); - }); - - it('Browser: should send events successfully', async () => { + it('(browser) should send events successfully', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, responsePromise: Promise.resolve({ statusCode: 200, - body: JSON.stringify(MOCK_SEND_PARAMETERS), + body: 'body-unimportant', headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const response = await client.sendEvents(MOCK_SEND_PARAMETERS) ?? ''; + const response = await client(EXECUTION_CONTEXT.BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(response).toEqual(VALID_RESPONSE_CODE); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); - it('Node: should send events successfully', async () => { + it('(node) should send events successfully', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, responsePromise: Promise.resolve({ statusCode: 200, - body: JSON.stringify(MOCK_SEND_PARAMETERS), + body: 'body-unimportant', headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const response = await client.sendEvents(MOCK_SEND_PARAMETERS) ?? ''; + const response = await client(EXECUTION_CONTEXT.NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(response).toEqual(VALID_RESPONSE_CODE); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); - it('Browser: should handle and return 400 HTTP response', async () => { + it('(browser) should handle and return 400 HTTP response', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -136,16 +118,15 @@ describe('OdpClient Send Events', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + const statusCode = await client(EXECUTION_CONTEXT.BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(400); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); - it('Node: should handle and return 400 HTTP response', async () => { + it('(node) should handle and return 400 HTTP response', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -155,16 +136,15 @@ describe('OdpClient Send Events', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + const statusCode = await client(EXECUTION_CONTEXT.NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(400); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); - it('Browser: should handle and return 500 HTTP response', async () => { + it('(browser) should handle and return 500 HTTP response', async () => { when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -174,16 +154,15 @@ describe('OdpClient Send Events', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); - const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + const statusCode = await client(EXECUTION_CONTEXT.BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(500); verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); - it('Node: should handle and return 500 HTTP response', async () => { + it('(node) should handle and return 500 HTTP response', async () => { when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ abort: () => { }, @@ -193,9 +172,8 @@ describe('OdpClient Send Events', () => { headers: {}, }), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); - const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + const statusCode = await client(EXECUTION_CONTEXT.NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(500); verify(mockErrorHandler.handleError(anything())).never(); @@ -210,7 +188,7 @@ describe('OdpClient Send Events', () => { }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); - const statusCode = await client.sendEvents(MOCK_SEND_PARAMETERS); + const statusCode = await client.sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toBe(0); verify(mockErrorHandler.handleError(anything())).once(); From 1a44aa7bedc76d63719aaf65e3d30137a046deac Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 15:38:20 -0400 Subject: [PATCH 16/35] Bug fix GraphQL-ifying query parameters --- .../lib/plugins/odp/graphql_manager.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 2154dbfc6..292917698 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -115,15 +115,17 @@ export class GraphqlManager implements IGraphQLManager { * @returns GraphQL JSON string */ private toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string { - const segmentsArrayJson = JSON.stringify(segmentsToCheck); - const json: string[] = []; json.push('{"query" : "query {customer"'); json.push(`(${userKey} : "${userValue}") `); json.push('{audiences'); - json.push(`(subset: ${segmentsArrayJson})`); - json.push('{edges {node {name state}}}}}"}'); - + json.push(`(subset: [`); + if (segmentsToCheck) { + segmentsToCheck.forEach((segment, index) => { + json.push(`\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}`); + }); + } + json.push('] {edges {node {name state}}}}}"}'); return json.join(''); } From 0ad944714f61b8a6e30f1411fb3d1196f9443e14 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 15:38:52 -0400 Subject: [PATCH 17/35] Change return on REST API Manager sendEvents() --- packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index bd7a32c87..5c670da31 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -24,7 +24,7 @@ import { OdpEvent } from './odp_event'; * Manager for communicating with the Optimizely Data Platform REST API */ export interface IRestApiManager { - sendEvents(apiKey: string, apiHost: string, events: OdpEvent[]): Promise; + sendEvents(apiKey: string, apiHost: string, events: OdpEvent[]): Promise; } /** From e9b2f386746ea7dbaf06add3150046904e4842d6 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 17:27:03 -0400 Subject: [PATCH 18/35] WIP REST API Manager's test skeleton --- .../tests/restApiManager.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/optimizely-sdk/tests/restApiManager.spec.ts diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts new file mode 100644 index 000000000..15e2d4494 --- /dev/null +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; +import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; +import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import { GraphqlManager } from '../lib/plugins/odp/graphql_manager'; +import { Response } from '../lib/plugins/odp/odp_types'; +import { ODP_USER_KEY } from '../lib/utils/enums'; +import { ResetApiManager } from '../lib/plugins/odp/rest_api_manager'; + +describe('RestApiManager', () => { + const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; + const ODP_GRAPHQL_URL = 'https://api.example.com/data/'; + + const makeManagerInstance = () => new ResetApiManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); + + let mockErrorHandler: ErrorHandler; + let mockLogger: LogHandler; + let mockOdpClient: IOdpClient; + + beforeAll(() => { + mockErrorHandler = mock(); + mockLogger = mock(); + mockOdpClient = mock(); + }); + + beforeEach(() => { + resetCalls(mockErrorHandler); + resetCalls(mockLogger); + resetCalls(mockOdpClient); + }); + + it('should should send events successfully', async () => { + const manager = makeManagerInstance(); + + }); + + it('should handle and return 400 HTTP response', async () => { + const manager = makeManagerInstance(); + + }); + + it('should handle and return 500 HTTP response', async () => { + const manager = makeManagerInstance(); + + }); + + it('should handle a network timeout', async () => { + const manager = makeManagerInstance(); + + }); + +}); + From d5e5ec15d6ab5c42c4bfaac09ef162e1f0ffcc7e Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 21 Sep 2022 08:48:49 -0400 Subject: [PATCH 19/35] Refactor GQLMgr's name --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 6 +++--- packages/optimizely-sdk/tests/graphQlManager.spec.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 292917698..76db9e7ec 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -38,14 +38,14 @@ const EMPTY_JSON_RESPONSE = null; /** * Manager for communicating with the Optimizely Data Platform GraphQL endpoint */ -export interface IGraphQLManager { +export interface IGraphQlManager { fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise; } /** * Concrete implementation for communicating with the ODP GraphQL endpoint */ -export class GraphqlManager implements IGraphQLManager { +export class GraphQlManager implements IGraphQlManager { private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; private readonly _odpClient: IOdpClient; @@ -78,7 +78,7 @@ export class GraphqlManager implements IGraphQLManager { return EMPTY_SEGMENTS_COLLECTION; } - const endpoint = `${apiHost}/v3/events`; + const endpoint = `${apiHost}/v3/graphql`; const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); const segmentsResponse = await this._odpClient.querySegments(apiKey, endpoint, userKey, userValue, query); diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index d65cb0e88..2932bab0e 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -19,7 +19,7 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; -import { GraphqlManager } from '../lib/plugins/odp/graphql_manager'; +import { GraphQlManager } from '../lib/plugins/odp/graphql_manager'; import { Response } from '../lib/plugins/odp/odp_types'; import { ODP_USER_KEY } from '../lib/utils/enums'; @@ -33,7 +33,7 @@ describe('GraphQLManager', () => { 'push_on_sale', ]; - const makeManagerInstance = () => new GraphqlManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); + const makeManagerInstance = () => new GraphQlManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; From b480af4cd1a7afdfc9936e7bab2be63820bb13db Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 21 Sep 2022 09:11:16 -0400 Subject: [PATCH 20/35] Unit tests for REST API Manager --- .../tests/restApiManager.spec.ts | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts index 15e2d4494..9e072fac2 100644 --- a/packages/optimizely-sdk/tests/restApiManager.spec.ts +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -18,15 +18,28 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; -import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; -import { GraphqlManager } from '../lib/plugins/odp/graphql_manager'; -import { Response } from '../lib/plugins/odp/odp_types'; -import { ODP_USER_KEY } from '../lib/utils/enums'; +import { ErrorHandler, LogHandler } from '../lib/modules/logging'; import { ResetApiManager } from '../lib/plugins/odp/rest_api_manager'; +import { OdpEvent } from '../lib/plugins/odp/odp_event'; describe('RestApiManager', () => { const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; - const ODP_GRAPHQL_URL = 'https://api.example.com/data/'; + const ODP_REST_API_HOST = 'https://api.example.com'; + const ODP_EVENTS = [ + new OdpEvent('t1', 'a1', + new Map([['id-key-1', 'id-value-1']]), + new Map(Object.entries({ + key11: 'value-1', + key12: true, + key13: 3.5, + key14: null, + }))), + new OdpEvent('t2', 'a2', + new Map([['id-key-2', 'id-value-2']]), + new Map(Object.entries({ + key2: 'value-2', + }))), + ]; const makeManagerInstance = () => new ResetApiManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); @@ -46,25 +59,48 @@ describe('RestApiManager', () => { resetCalls(mockOdpClient); }); - it('should should send events successfully', async () => { + it('should should send events successfully and not suggest retry', async () => { + when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(200); const manager = makeManagerInstance(); + const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); + + expect(shouldRetry).toBe(false); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); }); - it('should handle and return 400 HTTP response', async () => { + it('should not suggest a retry for 400 HTTP response', async () => { + when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(400); const manager = makeManagerInstance(); + const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); + + expect(shouldRetry).toBe(false); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); }); - it('should handle and return 500 HTTP response', async () => { + it('should suggest a retry for 500 HTTP response', async () => { + when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(500); const manager = makeManagerInstance(); + const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); + + expect(shouldRetry).toBe(true); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); }); - it('should handle a network timeout', async () => { + it('should suggest a retry for network timeout', async () => { + when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(0); const manager = makeManagerInstance(); - }); + const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); + expect(shouldRetry).toBe(true); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); }); From d170d3dc54aa66e21102c8bafaabe991fdebc330 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 21 Sep 2022 13:35:40 -0400 Subject: [PATCH 21/35] Fix name of REST API class --- packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts | 2 +- packages/optimizely-sdk/tests/restApiManager.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index 5c670da31..9abf51129 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -30,7 +30,7 @@ export interface IRestApiManager { /** * Concrete implementation for accessing the ODP REST API */ -export class ResetApiManager implements IRestApiManager { +export class RestApiManager implements IRestApiManager { private readonly errorHandler: ErrorHandler; private readonly logger: LogHandler; private readonly odpClient: IOdpClient; diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts index 9e072fac2..5fe67d98e 100644 --- a/packages/optimizely-sdk/tests/restApiManager.spec.ts +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -19,7 +19,7 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; import { ErrorHandler, LogHandler } from '../lib/modules/logging'; -import { ResetApiManager } from '../lib/plugins/odp/rest_api_manager'; +import { RestApiManager } from '../lib/plugins/odp/rest_api_manager'; import { OdpEvent } from '../lib/plugins/odp/odp_event'; describe('RestApiManager', () => { @@ -41,7 +41,7 @@ describe('RestApiManager', () => { }))), ]; - const makeManagerInstance = () => new ResetApiManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); + const makeManagerInstance = () => new RestApiManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; From 182bf29296b6ddb853bf5e46f4e15633a67c9cfc Mon Sep 17 00:00:00 2001 From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:56:50 -0400 Subject: [PATCH 22/35] Update packages/optimizely-sdk/lib/plugins/odp/odp_client.ts Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> --- packages/optimizely-sdk/lib/plugins/odp/odp_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 5c0830be5..28c41d476 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -31,7 +31,7 @@ const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; * Interface for sending requests and handling responses to Optimizely Data Platform */ export interface IOdpClient { - querySegments(apiKey: string, graphQlEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise; + querySegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise; sendEvents(apiKey: string, restApiEndpoint: string, jsonData: string): Promise; } From 97b9f05aecd4a182b5216ea6a555537051991cec Mon Sep 17 00:00:00 2001 From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:56:58 -0400 Subject: [PATCH 23/35] Update packages/optimizely-sdk/lib/plugins/odp/odp_client.ts Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> --- packages/optimizely-sdk/lib/plugins/odp/odp_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 28c41d476..540866b0a 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -33,7 +33,7 @@ const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; export interface IOdpClient { querySegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise; - sendEvents(apiKey: string, restApiEndpoint: string, jsonData: string): Promise; + sendEvents(apiKey: string, apiEndpoint: string, jsonData: string): Promise; } /** From 768aea1549f84fad99748f86a6425ae6636e65f8 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 22 Sep 2022 09:41:36 -0400 Subject: [PATCH 24/35] Revert "Replace execution context" This reverts commit f40bd80a8c8ef23dc6aed25fb932a99d33d4dbe7. --- packages/optimizely-sdk/lib/index.browser.ts | 4 ++ packages/optimizely-sdk/lib/index.node.ts | 4 ++ .../optimizely-sdk/lib/index.react_native.ts | 4 ++ .../optimizely-sdk/lib/utils/enums/index.ts | 17 +++---- .../lib/utils/execution_context/index.ts | 45 +++++++++++++++++++ .../request_handler_factory.ts | 15 ++++--- 6 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 packages/optimizely-sdk/lib/utils/execution_context/index.ts diff --git a/packages/optimizely-sdk/lib/index.browser.ts b/packages/optimizely-sdk/lib/index.browser.ts index c8532d7cd..5749ebf43 100644 --- a/packages/optimizely-sdk/lib/index.browser.ts +++ b/packages/optimizely-sdk/lib/index.browser.ts @@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center'; import { default as eventProcessor } from './plugins/event_processor'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; +import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; +import { ExecutionContext } from './utils/execution_context'; const logger = getLogger(); logHelper.setLogHandler(loggerPlugin.createLogger()); @@ -44,6 +46,8 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; let hasRetriedEvents = false; +ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER; + /** * Creates an instance of the Optimizely class * @param {Config} config diff --git a/packages/optimizely-sdk/lib/index.node.ts b/packages/optimizely-sdk/lib/index.node.ts index 56ba69c71..6fe59eb22 100644 --- a/packages/optimizely-sdk/lib/index.node.ts +++ b/packages/optimizely-sdk/lib/index.node.ts @@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; +import { ExecutionContext } from './utils/execution_context'; +import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; const logger = getLogger(); setLogLevel(LogLevel.ERROR); @@ -40,6 +42,8 @@ const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 30000; // Unit is ms, default is 30s const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; +ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.NODE; + /** * Creates an instance of the Optimizely class * @param {Config} config diff --git a/packages/optimizely-sdk/lib/index.react_native.ts b/packages/optimizely-sdk/lib/index.react_native.ts index b2c67bac8..cece646f2 100644 --- a/packages/optimizely-sdk/lib/index.react_native.ts +++ b/packages/optimizely-sdk/lib/index.react_native.ts @@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor/index.react_native'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; +import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; +import { ExecutionContext } from './utils/execution_context'; const logger = getLogger(); setLogHandler(loggerPlugin.createLogger()); @@ -41,6 +43,8 @@ const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; +ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER; + /** * Creates an instance of the Optimizely class * @param {Config} config diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index 2e37927df..c386a09c5 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -292,21 +292,14 @@ export enum NOTIFICATION_TYPES { TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', } -/** - * ODP User Key - */ -export enum ODP_USER_KEY { - VUID = 'vuid', - FS_USER_ID = 'fs_user_id', -} /** - * Possible states of ODP integration + * Valid types of Javascript contexts in which this code is executing */ -export enum ODP_CONFIG_STATE { - UNDETERMINED = 0, - INTEGRATED, - NOT_INTEGRATED = 2, +export enum EXECUTION_CONTEXT_TYPE { + NOT_DEFINED, + BROWSER, + NODE, } /** diff --git a/packages/optimizely-sdk/lib/utils/execution_context/index.ts b/packages/optimizely-sdk/lib/utils/execution_context/index.ts new file mode 100644 index 000000000..f9f3e2457 --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/execution_context/index.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EXECUTION_CONTEXT_TYPE } from '../enums'; + +/** + * Determine the running or execution context for JavaScript + * Note: React Native is considered a browser context + */ +export class ExecutionContext { + /** + * Holds the current value of the execution context + * @private + */ + private static _currentContext: EXECUTION_CONTEXT_TYPE = EXECUTION_CONTEXT_TYPE.NOT_DEFINED; + + /** + * Gets the current running context + * @constructor + */ + public static get Current(): EXECUTION_CONTEXT_TYPE { + return this._currentContext; + } + + /** + * Sets the current running context ideally from package initialization + * @param newValue The new execution context + * @constructor + */ + public static set Current(newValue: EXECUTION_CONTEXT_TYPE) { + this._currentContext = newValue; + } +} diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts index ddab23faf..455754cb5 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts @@ -18,18 +18,21 @@ import { LogHandler } from '../../modules/logging'; import { RequestHandler } from './http'; import { NodeRequestHandler } from './node_request_handler'; import { BrowserRequestHandler } from './browser_request_handler'; +import { ExecutionContext } from '../execution_context'; +import { EXECUTION_CONTEXT_TYPE } from '../enums'; /** * Factory to create the appropriate type of RequestHandler based on a provided context */ export class RequestHandlerFactory { public static createHandler(logger: LogHandler, timeout?: number): RequestHandler { - if (window) { - return new BrowserRequestHandler(logger, timeout); - } else if (process) { - return new NodeRequestHandler(logger, timeout); - } else { - return null as unknown as RequestHandler; + switch (ExecutionContext.Current) { + case EXECUTION_CONTEXT_TYPE.BROWSER: + return new BrowserRequestHandler(logger, timeout); + case EXECUTION_CONTEXT_TYPE.NODE: + return new NodeRequestHandler(logger, timeout); + default: + return null as unknown as RequestHandler; } } } From afae4ce09512ed7d280c7905d399adbea1363095 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 20 Sep 2022 12:08:36 -0400 Subject: [PATCH 25/35] Apply only needed ODP_USER_KEY & ODP_CONFIG_STATE --- .../optimizely-sdk/lib/utils/enums/index.ts | 17 +++++++++++++---- .../request_handler_factory.ts | 15 ++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index c386a09c5..f631f96f1 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -303,9 +303,18 @@ export enum EXECUTION_CONTEXT_TYPE { } /** - * Flag for an execution context (browser or node) + * ODP User Key */ -export enum EXECUTION_CONTEXT { - BROWSER = 'browser', - NODE = 'node', +export enum ODP_USER_KEY { + VUID = 'vuid', + FS_USER_ID = 'fs_user_id', +} + +/** + * Possible states of ODP integration + */ +export enum ODP_CONFIG_STATE { + UNDETERMINED = 0, + INTEGRATED, + NOT_INTEGRATED = 2, } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts index 455754cb5..ddab23faf 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts @@ -18,21 +18,18 @@ import { LogHandler } from '../../modules/logging'; import { RequestHandler } from './http'; import { NodeRequestHandler } from './node_request_handler'; import { BrowserRequestHandler } from './browser_request_handler'; -import { ExecutionContext } from '../execution_context'; -import { EXECUTION_CONTEXT_TYPE } from '../enums'; /** * Factory to create the appropriate type of RequestHandler based on a provided context */ export class RequestHandlerFactory { public static createHandler(logger: LogHandler, timeout?: number): RequestHandler { - switch (ExecutionContext.Current) { - case EXECUTION_CONTEXT_TYPE.BROWSER: - return new BrowserRequestHandler(logger, timeout); - case EXECUTION_CONTEXT_TYPE.NODE: - return new NodeRequestHandler(logger, timeout); - default: - return null as unknown as RequestHandler; + if (window) { + return new BrowserRequestHandler(logger, timeout); + } else if (process) { + return new NodeRequestHandler(logger, timeout); + } else { + return null as unknown as RequestHandler; } } } From 33114a0adc8d0704bcd591f800adc3e9fa07cb30 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 22 Sep 2022 10:51:40 -0400 Subject: [PATCH 26/35] Code review changes --- .../lib/plugins/odp/graphql_manager.ts | 41 +++++++++---------- .../lib/plugins/odp/odp_client.ts | 19 ++------- .../lib/plugins/odp/rest_api_manager.ts | 15 +++---- .../tests/graphQlManager.spec.ts | 14 +------ .../tests/odpClientQuerySegments.spec.ts | 38 +++++++---------- .../tests/odpClientSendEvents.spec.ts | 35 ++++++---------- .../tests/restApiManager.spec.ts | 2 +- 7 files changed, 61 insertions(+), 103 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 76db9e7ec..f74bb10c4 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; +import { LogHandler, LogLevel } from '../../modules/logging'; import { Response } from './odp_types'; import { IOdpClient, OdpClient } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; @@ -46,23 +46,19 @@ export interface IGraphQlManager { * Concrete implementation for communicating with the ODP GraphQL endpoint */ export class GraphQlManager implements IGraphQlManager { - private readonly _errorHandler: ErrorHandler; - private readonly _logger: LogHandler; - private readonly _odpClient: IOdpClient; + private readonly logger: LogHandler; + private readonly odpClient: IOdpClient; /** * Communicates with Optimizely Data Platform's GraphQL endpoint - * @param errorHandler Handler to record exceptions * @param logger Collect and record events/errors for this GraphQL implementation * @param client Client to use to send queries to ODP */ - constructor(errorHandler: ErrorHandler, logger: LogHandler, client?: IOdpClient) { - this._errorHandler = errorHandler; - this._logger = logger; + constructor(logger: LogHandler, client?: IOdpClient) { + this.logger = logger; - this._odpClient = client ?? new OdpClient(this._errorHandler, - this._logger, - RequestHandlerFactory.createHandler(this._logger)); + this.odpClient = client ?? new OdpClient(this.logger, + RequestHandlerFactory.createHandler(this.logger)); } /** @@ -74,6 +70,11 @@ export class GraphQlManager implements IGraphQlManager { * @param segmentsToCheck Audience segments to check for experiment inclusion */ public async fetchSegments(apiKey: string, apiHost: string, userKey: ODP_USER_KEY, userValue: string, segmentsToCheck: string[]): Promise { + if (!apiKey || !apiHost) { + this.logger.log(LogLevel.ERROR, 'Audience segments fetch failed (Parameters apiKey or apiHost invalid)'); + return null; + } + if (segmentsToCheck?.length === 0) { return EMPTY_SEGMENTS_COLLECTION; } @@ -81,29 +82,29 @@ export class GraphQlManager implements IGraphQlManager { const endpoint = `${apiHost}/v3/graphql`; const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); - const segmentsResponse = await this._odpClient.querySegments(apiKey, endpoint, userKey, userValue, query); + const segmentsResponse = await this.odpClient.querySegments(apiKey, endpoint, userKey, userValue, query); if (!segmentsResponse) { - this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); + this.logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); return null; } const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { - this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); + this.logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); return null; } if (parsedSegments.errors?.length > 0) { const errors = parsedSegments.errors.map((e) => e.message).join('; '); - this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); + this.logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); return EMPTY_SEGMENTS_COLLECTION; } const edges = parsedSegments?.data?.customer?.audiences?.edges; if (!edges) { - this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); + this.logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); return EMPTY_SEGMENTS_COLLECTION; } @@ -140,12 +141,8 @@ export class GraphQlManager implements IGraphQlManager { try { jsonObject = JSON.parse(jsonResponse); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); + } catch { + this.logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); return EMPTY_JSON_RESPONSE; } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 540866b0a..d5085a887 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; +import { LogHandler, LogLevel } from '../../modules/logging'; import { RequestHandler, Response } from '../../utils/http_request_handler/http'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; @@ -40,20 +40,17 @@ export interface IOdpClient { * Http implementation for sending requests and handling responses to Optimizely Data Platform */ export class OdpClient implements IOdpClient { - private readonly errorHandler: ErrorHandler; private readonly logger: LogHandler; private readonly timeout: number; private readonly requestHandler: RequestHandler; /** * An implementation for sending requests and handling responses to Optimizely Data Platform (ODP) - * @param errorHandler Handler to record exceptions * @param logger Collect and record events/errors for this ODP client * @param requestHandler Client implementation to send/receive requests over HTTP * @param timeout Maximum milliseconds before requests are considered timed out */ - constructor(errorHandler: ErrorHandler, logger: LogHandler, requestHandler: RequestHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this.errorHandler = errorHandler; + constructor(logger: LogHandler, requestHandler: RequestHandler, timeout: number = REQUEST_TIMEOUT_MS) { this.logger = logger; this.requestHandler = requestHandler; this.timeout = timeout; @@ -80,11 +77,7 @@ export class OdpClient implements IOdpClient { try { const request = this.requestHandler.makeRequest(url, headers, method, graphQlQuery); response = await request.responsePromise; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - this.errorHandler.handleError(error); + } catch { this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); return null; @@ -114,11 +107,7 @@ export class OdpClient implements IOdpClient { try { const request = this.requestHandler.makeRequest(url, headers, method, data); response = await request.responsePromise; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - this.errorHandler.handleError(error); + } catch { this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); return 0; diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index 9abf51129..b87e56e6d 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -15,7 +15,7 @@ */ -import { ErrorHandler, LogHandler } from '../../modules/logging'; +import { LogHandler, LogLevel } from '../../modules/logging'; import { IOdpClient, OdpClient } from './odp_client'; import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { OdpEvent } from './odp_event'; @@ -31,22 +31,18 @@ export interface IRestApiManager { * Concrete implementation for accessing the ODP REST API */ export class RestApiManager implements IRestApiManager { - private readonly errorHandler: ErrorHandler; private readonly logger: LogHandler; private readonly odpClient: IOdpClient; /** * Creates instance to access Optimizely Data Platform (ODP) REST API - * @param errorHandler Handler to record exceptions * @param logger Collect and record events/errors for this REST implementation * @param client HTTP Client used to send data to ODP */ - constructor(errorHandler: ErrorHandler, logger: LogHandler, client?: IOdpClient) { - this.errorHandler = errorHandler; + constructor(logger: LogHandler, client?: IOdpClient) { this.logger = logger; - this.odpClient = client ?? new OdpClient(this.errorHandler, - this.logger, + this.odpClient = client ?? new OdpClient(this.logger, RequestHandlerFactory.createHandler(this.logger)); } @@ -58,6 +54,11 @@ export class RestApiManager implements IRestApiManager { * @returns Retry is true - if network or server error (5xx), otherwise false */ public async sendEvents(apiKey: string, apiHost: string, events: OdpEvent[]): Promise { + if (!apiKey || !apiHost) { + this.logger.log(LogLevel.ERROR, 'ODP event send failed (Parameters apiKey or apiHost invalid)'); + return false; + } + if (events.length === 0) { return false; } diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index 2932bab0e..1df409dc4 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -18,7 +18,7 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; -import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import { LogHandler, LogLevel } from '../lib/modules/logging'; import { GraphQlManager } from '../lib/plugins/odp/graphql_manager'; import { Response } from '../lib/plugins/odp/odp_types'; import { ODP_USER_KEY } from '../lib/utils/enums'; @@ -33,20 +33,17 @@ describe('GraphQLManager', () => { 'push_on_sale', ]; - const makeManagerInstance = () => new GraphQlManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); + const makeManagerInstance = () => new GraphQlManager(instance(mockLogger), instance(mockOdpClient)); - let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockOdpClient: IOdpClient; beforeAll(() => { - mockErrorHandler = mock(); mockLogger = mock(); mockOdpClient = mock(); }); beforeEach(() => { - resetCalls(mockErrorHandler); resetCalls(mockLogger); resetCalls(mockOdpClient); }); @@ -137,7 +134,6 @@ describe('GraphQLManager', () => { expect(segments).toHaveLength(2); expect(segments).toContain('has_email'); expect(segments).toContain('has_email_opted_in'); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -147,7 +143,6 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, []); expect(segments).toHaveLength(0); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -160,7 +155,6 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -177,7 +171,6 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).once(); }); @@ -189,7 +182,6 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); @@ -204,7 +196,6 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).once(); }); @@ -216,7 +207,6 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); diff --git a/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts index 94c6fc9c3..adb0b9723 100644 --- a/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts +++ b/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts @@ -17,18 +17,17 @@ /// import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import { LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -import { EXECUTION_CONTEXT, ODP_USER_KEY } from '../lib/utils/enums'; +import { ODP_USER_KEY } from '../lib/utils/enums'; const API_KEY = 'not-real-api-key'; const GRAPHQL_ENDPOINT = 'https://api.example.com/v3/graphql'; const USER_KEY = ODP_USER_KEY.FS_USER_ID; const USER_VALUE = 'mock-user-id'; const GRAPHQL_QUERY = `{"query" : "query {customer"(fs_user_id : "mock-user-id") {audiences(subset: [\\"has_email\\", \\"has_email_opted_in\\", \\"push_on_sale\\"] {edges {node {name state}}}}}"}`; - const VALID_RESPONSE_JSON = { 'data': { 'customer': { @@ -51,28 +50,26 @@ const VALID_RESPONSE_JSON = { }, }, }; - const BODY_FROM_ERROR = ''; +const BROWSER = 'browser'; +const NODE = 'node'; describe('OdpClient Query Segments', () => { - const client = (type: EXECUTION_CONTEXT) => type === EXECUTION_CONTEXT.BROWSER ? - new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)) : - new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + const client = (type: string) => type === BROWSER ? + new OdpClient(instance(mockLogger), instance(mockBrowserRequestHandler)) : + new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler)); - let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockBrowserRequestHandler: BrowserRequestHandler; let mockNodeRequestHandler: NodeRequestHandler; beforeAll(() => { - mockErrorHandler = mock(); mockLogger = mock(); mockBrowserRequestHandler = mock(); mockNodeRequestHandler = mock(); }); beforeEach(() => { - resetCalls(mockErrorHandler); resetCalls(mockLogger); resetCalls(mockBrowserRequestHandler); resetCalls(mockNodeRequestHandler); @@ -89,10 +86,9 @@ describe('OdpClient Query Segments', () => { }), }); - const response = await client(EXECUTION_CONTEXT.BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); + const response = await client(BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -107,10 +103,9 @@ describe('OdpClient Query Segments', () => { }), }); - const response = await client(EXECUTION_CONTEXT.NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); + const response = await client(NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -125,10 +120,9 @@ describe('OdpClient Query Segments', () => { }), }); - const responseJson = await client(EXECUTION_CONTEXT.BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); + const responseJson = await client(BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -143,10 +137,9 @@ describe('OdpClient Query Segments', () => { }), }); - const responseJson = await client(EXECUTION_CONTEXT.NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); + const responseJson = await client(NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -161,10 +154,9 @@ describe('OdpClient Query Segments', () => { }), }); - const responseJson = await client(EXECUTION_CONTEXT.BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); + const responseJson = await client(BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -179,10 +171,9 @@ describe('OdpClient Query Segments', () => { }), }); - const responseJson = await client(EXECUTION_CONTEXT.NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); + const responseJson = await client(NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -192,12 +183,11 @@ describe('OdpClient Query Segments', () => { }, responsePromise: Promise.reject(new Error('Request timed out')), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); + const client = new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler), 10); const responseJson = await client.querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).once(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); diff --git a/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts index d265bfd5d..cc28b96bf 100644 --- a/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts +++ b/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts @@ -17,12 +17,11 @@ /// import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import { LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; import { OdpEvent } from '../lib/plugins/odp/odp_event'; -import { EXECUTION_CONTEXT } from '../lib/utils/enums'; const API_KEY = 'not-real-api-key'; const REST_API_ENDPOINT = 'https://events.example.com/v2/api'; @@ -44,20 +43,20 @@ const JSON_EVENT_DATA = JSON.stringify( ], ); const VALID_RESPONSE_CODE = 200; +const BROWSER = 'browser'; +const NODE = 'node'; describe('OdpClient Send Events', () => { - const client = (type: EXECUTION_CONTEXT) => type === EXECUTION_CONTEXT.BROWSER ? - new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)) : - new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + const client = (type: string) => type === BROWSER ? + new OdpClient(instance(mockLogger), instance(mockBrowserRequestHandler)) : + new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler)); - let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockBrowserRequestHandler: BrowserRequestHandler; let mockNodeRequestHandler: NodeRequestHandler; let mockOdpEvent: OdpEvent; beforeAll(() => { - mockErrorHandler = mock(); mockLogger = mock(); mockBrowserRequestHandler = mock(); mockNodeRequestHandler = mock(); @@ -65,7 +64,6 @@ describe('OdpClient Send Events', () => { }); beforeEach(() => { - resetCalls(mockErrorHandler); resetCalls(mockLogger); resetCalls(mockBrowserRequestHandler); resetCalls(mockNodeRequestHandler); @@ -83,10 +81,9 @@ describe('OdpClient Send Events', () => { }), }); - const response = await client(EXECUTION_CONTEXT.BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); + const response = await client(BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(response).toEqual(VALID_RESPONSE_CODE); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -101,10 +98,9 @@ describe('OdpClient Send Events', () => { }), }); - const response = await client(EXECUTION_CONTEXT.NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); + const response = await client(NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(response).toEqual(VALID_RESPONSE_CODE); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -119,10 +115,9 @@ describe('OdpClient Send Events', () => { }), }); - const statusCode = await client(EXECUTION_CONTEXT.BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); + const statusCode = await client(BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(400); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -137,10 +132,9 @@ describe('OdpClient Send Events', () => { }), }); - const statusCode = await client(EXECUTION_CONTEXT.NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); + const statusCode = await client(NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(400); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -155,10 +149,9 @@ describe('OdpClient Send Events', () => { }), }); - const statusCode = await client(EXECUTION_CONTEXT.BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); + const statusCode = await client(BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(500); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -173,10 +166,9 @@ describe('OdpClient Send Events', () => { }), }); - const statusCode = await client(EXECUTION_CONTEXT.NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); + const statusCode = await client(NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toEqual(500); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); }); @@ -186,12 +178,11 @@ describe('OdpClient Send Events', () => { }, responsePromise: Promise.reject(new Error('Request timed out')), }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); + const client = new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler), 10); const statusCode = await client.sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); expect(statusCode).toBe(0); - verify(mockErrorHandler.handleError(anything())).once(); verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (network error)')).once(); }); }); diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts index 5fe67d98e..02f381f9e 100644 --- a/packages/optimizely-sdk/tests/restApiManager.spec.ts +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -41,7 +41,7 @@ describe('RestApiManager', () => { }))), ]; - const makeManagerInstance = () => new RestApiManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); + const makeManagerInstance = () => new RestApiManager(instance(mockLogger), instance(mockOdpClient)); let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; From 829c6deeee40c1a29edce8a985f181bfad6347cb Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 22 Sep 2022 17:53:38 -0400 Subject: [PATCH 27/35] Moved OdpClient logic up to GraphQLManager and... WIP REST API manager test --- .../lib/plugins/odp/graphql_manager.ts | 72 +++++-- .../lib/plugins/odp/odp_client.ts | 118 ----------- .../lib/plugins/odp/rest_api_manager.ts | 56 +++-- .../tests/graphQlManager.spec.ts | 129 +++++++----- .../tests/odpClientQuerySegments.spec.ts | 194 ------------------ 5 files changed, 179 insertions(+), 390 deletions(-) delete mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_client.ts delete mode 100644 packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index f74bb10c4..09b6a8aec 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -15,12 +15,14 @@ */ import { LogHandler, LogLevel } from '../../modules/logging'; -import { Response } from './odp_types'; -import { IOdpClient, OdpClient } from './odp_client'; + import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { ODP_USER_KEY } from '../../utils/enums'; +import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http'; +import { Response as GraphQLResponse } from './odp_types'; +import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; /** * Expected value for a qualified/valid segment @@ -34,31 +36,36 @@ const EMPTY_SEGMENTS_COLLECTION: string[] = []; * Return value for scenarios with no valid JSON */ const EMPTY_JSON_RESPONSE = null; +/** + * Standard message for audience querying fetch errors + */ +const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; /** * Manager for communicating with the Optimizely Data Platform GraphQL endpoint */ -export interface IGraphQlManager { +export interface IGraphQLManager { fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise; } /** * Concrete implementation for communicating with the ODP GraphQL endpoint */ -export class GraphQlManager implements IGraphQlManager { +export class GraphQLManager implements IGraphQLManager { private readonly logger: LogHandler; - private readonly odpClient: IOdpClient; + private readonly timeout: number; + private readonly requestHandler: RequestHandler; /** * Communicates with Optimizely Data Platform's GraphQL endpoint * @param logger Collect and record events/errors for this GraphQL implementation - * @param client Client to use to send queries to ODP + * @param timeout Milliseconds to wait for a response + * @param requestHandler Desired request handler for testing */ - constructor(logger: LogHandler, client?: IOdpClient) { + constructor(logger: LogHandler, timeout?: number, requestHandler?: RequestHandler) { this.logger = logger; - - this.odpClient = client ?? new OdpClient(this.logger, - RequestHandlerFactory.createHandler(this.logger)); + this.timeout = timeout ?? REQUEST_TIMEOUT_MS; + this.requestHandler = requestHandler ?? RequestHandlerFactory.createHandler(this.logger); } /** @@ -71,7 +78,7 @@ export class GraphQlManager implements IGraphQlManager { */ public async fetchSegments(apiKey: string, apiHost: string, userKey: ODP_USER_KEY, userValue: string, segmentsToCheck: string[]): Promise { if (!apiKey || !apiHost) { - this.logger.log(LogLevel.ERROR, 'Audience segments fetch failed (Parameters apiKey or apiHost invalid)'); + this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (Parameters apiKey or apiHost invalid)`); return null; } @@ -82,29 +89,29 @@ export class GraphQlManager implements IGraphQlManager { const endpoint = `${apiHost}/v3/graphql`; const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); - const segmentsResponse = await this.odpClient.querySegments(apiKey, endpoint, userKey, userValue, query); + const segmentsResponse = await this.querySegments(apiKey, endpoint, userKey, userValue, query); if (!segmentsResponse) { - this.logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); + this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); return null; } const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { - this.logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); + this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); return null; } if (parsedSegments.errors?.length > 0) { const errors = parsedSegments.errors.map((e) => e.message).join('; '); - this.logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); + this.logger.log(LogLevel.WARNING, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${errors})`); return EMPTY_SEGMENTS_COLLECTION; } const edges = parsedSegments?.data?.customer?.audiences?.edges; if (!edges) { - this.logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); + this.logger.log(LogLevel.WARNING, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); return EMPTY_SEGMENTS_COLLECTION; } @@ -130,24 +137,51 @@ export class GraphQlManager implements IGraphQlManager { return json.join(''); } + /** + * Handler for querying the ODP GraphQL endpoint + * @param apiKey ODP API key + * @param endpoint Fully-qualified GraphQL endpoint URL + * @param userKey 'vuid' or 'fs_user_id' + * @param userValue userKey's value + * @param query GraphQL formatted query string + * @returns JSON response string from ODP or null + */ + private async querySegments(apiKey: string, endpoint: string, userKey: string, userValue: string, query: string): Promise { + const method = 'POST'; + const url = endpoint; + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }; + + let response: HttpResponse; + try { + const request = this.requestHandler.makeRequest(url, headers, method, query); + response = await request.responsePromise; + } catch { + return null; + } + + return response.body; + } + /** * Parses JSON response * @param jsonResponse JSON response from ODP * @private * @returns Response Strongly-typed ODP Response object */ - private parseSegmentsResponseJson(jsonResponse: string): Response | null { + private parseSegmentsResponseJson(jsonResponse: string): GraphQLResponse | null { let jsonObject = {}; try { jsonObject = JSON.parse(jsonResponse); } catch { - this.logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); return EMPTY_JSON_RESPONSE; } if (validate(jsonObject, OdpResponseSchema, false)) { - return jsonObject as Response; + return jsonObject as GraphQLResponse; } return EMPTY_JSON_RESPONSE; diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts deleted file mode 100644 index d5085a887..000000000 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LogHandler, LogLevel } from '../../modules/logging'; -import { RequestHandler, Response } from '../../utils/http_request_handler/http'; -import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; - -/** - * Standard message for audience querying fetch errors - */ -const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; -/** - * Standard message for sending events errors - */ -const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; - -/** - * Interface for sending requests and handling responses to Optimizely Data Platform - */ -export interface IOdpClient { - querySegments(apiKey: string, apiEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise; - - sendEvents(apiKey: string, apiEndpoint: string, jsonData: string): Promise; -} - -/** - * Http implementation for sending requests and handling responses to Optimizely Data Platform - */ -export class OdpClient implements IOdpClient { - private readonly logger: LogHandler; - private readonly timeout: number; - private readonly requestHandler: RequestHandler; - - /** - * An implementation for sending requests and handling responses to Optimizely Data Platform (ODP) - * @param logger Collect and record events/errors for this ODP client - * @param requestHandler Client implementation to send/receive requests over HTTP - * @param timeout Maximum milliseconds before requests are considered timed out - */ - constructor(logger: LogHandler, requestHandler: RequestHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this.logger = logger; - this.requestHandler = requestHandler; - this.timeout = timeout; - } - - /** - * Handler for querying the ODP GraphQL endpoint - * @param apiKey ODP API key - * @param graphQlEndpoint Fully-qualified GraphQL endpoint URL - * @param userKey 'vuid' or 'fs_user_id' - * @param userValue userKey's value - * @param graphQlQuery GraphQL formatted query string - * @returns JSON response string from ODP or null - */ - public async querySegments(apiKey: string, graphQlEndpoint: string, userKey: string, userValue: string, graphQlQuery: string): Promise { - const method = 'POST'; - const url = graphQlEndpoint; - const headers = { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - }; - - let response: Response; - try { - const request = this.requestHandler.makeRequest(url, headers, method, graphQlQuery); - response = await request.responsePromise; - } catch { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); - - return null; - } - - return response.body; - } - - /** - * Handler for sending ODP events - * @param apiKey ODP API key - * @param restApiEndpoint Fully-qualified REST API endpoint URL - * @param data JSON event data payload - * @returns - * 0 = If an unexpected error occurred and retrying can be useful - * Otherwise HTTPStatus code NOTE: it is recommended to retry if status code was 5xx. - */ - public async sendEvents(apiKey: string, restApiEndpoint: string, data: string): Promise { - const method = 'POST'; - const url = restApiEndpoint; - const headers = { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - }; - - let response: Response; - try { - const request = this.requestHandler.makeRequest(url, headers, method, data); - response = await request.responsePromise; - } catch { - this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); - - return 0; - } - - return response.statusCode ?? 0; - } -} diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index b87e56e6d..9494549e7 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -14,11 +14,13 @@ * limitations under the License. */ - import { LogHandler, LogLevel } from '../../modules/logging'; -import { IOdpClient, OdpClient } from './odp_client'; import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { OdpEvent } from './odp_event'; +import { RequestHandler } from '../../utils/http_request_handler/http'; +import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; + +const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; /** * Manager for communicating with the Optimizely Data Platform REST API @@ -32,18 +34,18 @@ export interface IRestApiManager { */ export class RestApiManager implements IRestApiManager { private readonly logger: LogHandler; - private readonly odpClient: IOdpClient; + private readonly timeout: number; + private readonly requestHandler: RequestHandler; /** * Creates instance to access Optimizely Data Platform (ODP) REST API * @param logger Collect and record events/errors for this REST implementation - * @param client HTTP Client used to send data to ODP + * @param timeout Milliseconds to wait for a response */ - constructor(logger: LogHandler, client?: IOdpClient) { + constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { this.logger = logger; - - this.odpClient = client ?? new OdpClient(this.logger, - RequestHandlerFactory.createHandler(this.logger)); + this.timeout = timeout; + this.requestHandler = RequestHandlerFactory.createHandler(this.logger); } /** @@ -54,20 +56,48 @@ export class RestApiManager implements IRestApiManager { * @returns Retry is true - if network or server error (5xx), otherwise false */ public async sendEvents(apiKey: string, apiHost: string, events: OdpEvent[]): Promise { + let shouldRetry = false; + if (!apiKey || !apiHost) { - this.logger.log(LogLevel.ERROR, 'ODP event send failed (Parameters apiKey or apiHost invalid)'); - return false; + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (Parameters apiKey or apiHost invalid)`); + return shouldRetry; } if (events.length === 0) { - return false; + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (no events)`); + return shouldRetry; } const endpoint = `${apiHost}/v3/events`; const data = JSON.stringify(events); - const statusCode = await this.odpClient.sendEvents(apiKey, endpoint, data); + const method = 'POST'; + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }; + + let statusCode = 0; + try { + const request = this.requestHandler.makeRequest(endpoint, headers, method, data); + const response = await request.responsePromise; + statusCode = response.statusCode ?? statusCode; + } catch { + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); + } + + if (statusCode === 0) { + shouldRetry = true; + } + + if (statusCode >= 400) { + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${statusCode})`); + } + + if (statusCode >= 500) { + shouldRetry = true; + } - return statusCode === 0 || (statusCode >= 500 && statusCode < 600); + return shouldRetry; } } diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index 1df409dc4..239943207 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -17,37 +17,49 @@ /// import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { GraphQlManager } from '../lib/plugins/odp/graphql_manager'; -import { Response } from '../lib/plugins/odp/odp_types'; +import { GraphQLManager } from '../lib/plugins/odp/graphql_manager'; +import { RequestHandler } from '../lib/utils/http_request_handler/http'; +import { Response as GraphQLResponse } from '../lib/plugins/odp/odp_types'; import { ODP_USER_KEY } from '../lib/utils/enums'; - +import { REQUEST_TIMEOUT_MS } from '../lib/utils/http_request_handler/config'; + +const API_key = 'not-real-api-key'; +const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; +const USER_KEY = ODP_USER_KEY.FS_USER_ID; +const USER_VALUE = 'tester-101'; +const SEGMENTS_TO_CHECK = [ + 'has_email', + 'has_email_opted_in', + 'push_on_sale', +]; describe('GraphQLManager', () => { - const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; - const ODP_GRAPHQL_URL = 'https://some.example.com/graphql/endpoint'; - const VALID_FS_USER_ID = 'tester-101'; - const SEGMENTS_TO_CHECK = [ - 'has_email', - 'has_email_opted_in', - 'push_on_sale', - ]; - - const makeManagerInstance = () => new GraphQlManager(instance(mockLogger), instance(mockOdpClient)); - let mockLogger: LogHandler; - let mockOdpClient: IOdpClient; + let mockRequestHandler: RequestHandler; beforeAll(() => { mockLogger = mock(); - mockOdpClient = mock(); + mockRequestHandler = mock(); }); beforeEach(() => { resetCalls(mockLogger); - resetCalls(mockOdpClient); + resetCalls(mockRequestHandler); }); + const managerInstance = () => new GraphQLManager(instance(mockLogger), REQUEST_TIMEOUT_MS, instance(mockRequestHandler)); + + const abortableRequest = (statusCode: number, body: string) => { + return { + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode, + body, + headers: {}, + }), + }; + }; it('should parse a successful response', () => { const validJsonResponse = `{ @@ -72,9 +84,9 @@ describe('GraphQLManager', () => { } } }`; - const manager = makeManagerInstance(); + const manager = managerInstance(); - const response = manager['parseSegmentsResponseJson'](validJsonResponse) as Response; + const response = manager['parseSegmentsResponseJson'](validJsonResponse) as GraphQLResponse; expect(response).not.toBeUndefined(); expect(response?.errors).toHaveLength(0); @@ -92,7 +104,7 @@ describe('GraphQLManager', () => { const errorJsonResponse = `{ "errors": [ { - "message": "Exception while fetching data (/customer) : Exception: could not resolve _fs_user_id = asdsdaddddd", + "message": "Exception while fetching data (/customer) : Exception: could not resolve _fs_user_id = mock_user_id", "locations": [ { "line": 2, @@ -111,9 +123,9 @@ describe('GraphQLManager', () => { "customer": null } }`; - const manager = makeManagerInstance(); + const manager = managerInstance(); - const response = manager['parseSegmentsResponseJson'](errorJsonResponse) as Response; + const response = manager['parseSegmentsResponseJson'](errorJsonResponse) as GraphQLResponse; expect(response).not.toBeUndefined(); expect(response.data.customer).toBeNull(); @@ -121,15 +133,25 @@ describe('GraphQLManager', () => { expect(response.errors[0].extensions.classification).toEqual('InvalidIdentifierException'); }); + it('should construct a valid GraphQL query string', () => { + const manager = managerInstance(); + + const response = manager['toGraphQLJson'](USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(response) + .toBe(`{"query" : "query {customer"(${USER_KEY} : "${USER_VALUE}") {audiences(subset: [\\"has_email\\",\\"has_email_opted_in\\",\\"push_on_sale\\"] {edges {node {name state}}}}}"}`, + ); + }); + it('should fetch valid qualified segments', async () => { const responseJsonWithQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[{"node":{"name":"has_email",' + '"state":"qualified"}},{"node":{"name":' + '"has_email_opted_in","state":"qualified"}}]}}}}'; - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(responseJsonWithQualifiedSegments); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, responseJsonWithQualifiedSegments)); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(2); expect(segments).toContain('has_email'); @@ -138,9 +160,9 @@ describe('GraphQLManager', () => { }); it('should handle a request to query no segments', async () => { - const manager = makeManagerInstance(); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, []); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, ODP_USER_KEY.FS_USER_ID, USER_VALUE, []); expect(segments).toHaveLength(0); verify(mockLogger.log(anything(), anyString())).never(); @@ -149,10 +171,10 @@ describe('GraphQLManager', () => { it('should handle empty qualified segments', async () => { const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(responseJsonWithNoQualifiedSegments); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, responseJsonWithNoQualifiedSegments)); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); verify(mockLogger.log(anything(), anyString())).never(); @@ -166,9 +188,10 @@ describe('GraphQLManager', () => { '"locations":[{"line":1,"column":8}],"path":["customer"],' + '"extensions":{"classification":"DataFetchingException"}}],' + '"data":{"customer":null}}'; - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(errorJsonResponse); - const manager = makeManagerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, errorJsonResponse)); + const manager = managerInstance(); + + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, INVALID_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); verify(mockLogger.log(anything(), anyString())).once(); @@ -176,10 +199,10 @@ describe('GraphQLManager', () => { it('should handle unrecognized JSON responses', async () => { const unrecognizedJson = '{"unExpectedObject":{ "withSome": "value", "thatIsNotParseable": "true" }}'; - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(unrecognizedJson); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, unrecognizedJson)); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); @@ -190,10 +213,10 @@ describe('GraphQLManager', () => { 'UnknownArgument: Unknown field argument not_real_userKey @ ' + '\'customer\'","locations":[{"line":1,"column":17}],' + '"extensions":{"classification":"ValidationError"}}]}'; - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(errorJsonResponse); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, errorJsonResponse)); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockLogger.log(anything(), anyString())).once(); @@ -201,20 +224,34 @@ describe('GraphQLManager', () => { it('should handle bad responses', async () => { const badResponse = '{"data":{ }}'; - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(badResponse); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, badResponse)); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); it('should handle non 200 HTTP status code response', async () => { - when(mockOdpClient.querySegments(anyString(), anyString(), anyString(), anyString(), anyString())).thenResolve(null); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(400, '')); + const manager = managerInstance(); + + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); + }); + + it('should handle a timeout', async () => { + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const manager = managerInstance(); - const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, ODP_USER_KEY.FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(segments).toBeNull(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); diff --git a/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts b/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts deleted file mode 100644 index adb0b9723..000000000 --- a/packages/optimizely-sdk/tests/odpClientQuerySegments.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// - -import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpClient } from '../lib/plugins/odp/odp_client'; -import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; -import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -import { ODP_USER_KEY } from '../lib/utils/enums'; - -const API_KEY = 'not-real-api-key'; -const GRAPHQL_ENDPOINT = 'https://api.example.com/v3/graphql'; -const USER_KEY = ODP_USER_KEY.FS_USER_ID; -const USER_VALUE = 'mock-user-id'; -const GRAPHQL_QUERY = `{"query" : "query {customer"(fs_user_id : "mock-user-id") {audiences(subset: [\\"has_email\\", \\"has_email_opted_in\\", \\"push_on_sale\\"] {edges {node {name state}}}}}"}`; -const VALID_RESPONSE_JSON = { - 'data': { - 'customer': { - 'audiences': { - 'edges': [ - { - 'node': { - 'name': 'has_email', - 'state': 'qualified', - }, - }, - { - 'node': { - 'name': 'has_email_opted_in', - 'state': 'not-ready', - }, - }, - ], - }, - }, - }, -}; -const BODY_FROM_ERROR = ''; -const BROWSER = 'browser'; -const NODE = 'node'; - -describe('OdpClient Query Segments', () => { - const client = (type: string) => type === BROWSER ? - new OdpClient(instance(mockLogger), instance(mockBrowserRequestHandler)) : - new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler)); - - let mockLogger: LogHandler; - let mockBrowserRequestHandler: BrowserRequestHandler; - let mockNodeRequestHandler: NodeRequestHandler; - - beforeAll(() => { - mockLogger = mock(); - mockBrowserRequestHandler = mock(); - mockNodeRequestHandler = mock(); - }); - - beforeEach(() => { - resetCalls(mockLogger); - resetCalls(mockBrowserRequestHandler); - resetCalls(mockNodeRequestHandler); - }); - - it('(browser) should get mocked segments successfully', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(VALID_RESPONSE_JSON), - headers: {}, - }), - }); - - const response = await client(BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('(node) should get mocked segments successfully', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: JSON.stringify(VALID_RESPONSE_JSON), - headers: {}, - }), - }); - - const response = await client(NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('(browser) should handle 400 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - - const responseJson = await client(BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('(node) should handle 400 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - - const responseJson = await client(NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('(browser) should handle 500 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - - const responseJson = await client(BROWSER).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('(node) should handle 500 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: BODY_FROM_ERROR, - headers: {}, - }), - }); - - const responseJson = await client(NODE).querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(responseJson).toBe(BODY_FROM_ERROR); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('should handle a network timeout', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - const client = new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler), 10); - - const responseJson = await client.querySegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, GRAPHQL_QUERY); - - expect(responseJson).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); - }); -}); - From a9273fa28385a6fa0214df2c8f5cbdf6fba72a5b Mon Sep 17 00:00:00 2001 From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com> Date: Fri, 23 Sep 2022 10:31:19 -0400 Subject: [PATCH 28/35] Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> --- .../lib/plugins/odp/graphql_manager.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 09b6a8aec..a35a90f40 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -122,20 +122,16 @@ export class GraphQLManager implements IGraphQLManager { * Converts the query parameters to a GraphQL JSON payload * @returns GraphQL JSON string */ - private toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string { - const json: string[] = []; - json.push('{"query" : "query {customer"'); - json.push(`(${userKey} : "${userValue}") `); - json.push('{audiences'); - json.push(`(subset: [`); - if (segmentsToCheck) { - segmentsToCheck.forEach((segment, index) => { - json.push(`\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}`); - }); - } - json.push('] {edges {node {name state}}}}}"}'); - return json.join(''); - } + private toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string => ([ + '{"query" : "query {customer"', + `(${userKey} : "${userValue}") `, + '{audiences', + '(subset: [', + ... segmentsToCheck?.map((segment, index) => + `\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}` + ) || '', + '] {edges {node {name state}}}}}"}', + ].join('')); /** * Handler for querying the ODP GraphQL endpoint From 21a969c03b18722d0d8151888448eb15c0d2856c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 23 Sep 2022 13:04:05 -0400 Subject: [PATCH 29/35] More code review changes --- .../lib/plugins/odp/graphql_manager.ts | 9 +- .../lib/plugins/odp/odp_event.ts | 28 ++- .../lib/plugins/odp/rest_api_manager.ts | 19 +- .../tests/graphQlManager.spec.ts | 30 ++- .../tests/odpClientSendEvents.spec.ts | 189 ------------------ .../tests/restApiManager.spec.ts | 96 +++++---- 6 files changed, 103 insertions(+), 268 deletions(-) delete mode 100644 packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 09b6a8aec..8863432bf 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -87,7 +87,7 @@ export class GraphQLManager implements IGraphQLManager { } const endpoint = `${apiHost}/v3/graphql`; - const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); + const query = GraphQLManager.toGraphQLJson(userKey, userValue, segmentsToCheck); const segmentsResponse = await this.querySegments(apiKey, endpoint, userKey, userValue, query); if (!segmentsResponse) { @@ -95,7 +95,7 @@ export class GraphQLManager implements IGraphQLManager { return null; } - const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); + const parsedSegments = GraphQLManager.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); return null; @@ -122,7 +122,7 @@ export class GraphQLManager implements IGraphQLManager { * Converts the query parameters to a GraphQL JSON payload * @returns GraphQL JSON string */ - private toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string { + public static toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string { const json: string[] = []; json.push('{"query" : "query {customer"'); json.push(`(${userKey} : "${userValue}") `); @@ -168,10 +168,9 @@ export class GraphQLManager implements IGraphQLManager { /** * Parses JSON response * @param jsonResponse JSON response from ODP - * @private * @returns Response Strongly-typed ODP Response object */ - private parseSegmentsResponseJson(jsonResponse: string): GraphQLResponse | null { + public static parseSegmentsResponseJson(jsonResponse: string): GraphQLResponse | null { let jsonObject = {}; try { diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index c555abec9..8d105a599 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -15,21 +15,37 @@ */ export class OdpEvent { + /** + * Type of event (typically "fullstack") + */ public type: string; + /** + * Subcategory of the event type + */ public action: string; + /** + * Key-value map of user identifiers + */ public identifiers: Map; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public data: Map; + /** + * Event data in a key-value map + */ + public data: Map; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(type: string, action: string, identifiers?: Map, data?: Map) { + /** + * Event to be sent and stored in the Optimizely Data Platform + * @param type Type of event (typically "fullstack") + * @param action Subcategory of the event type + * @param identifiers Key-value map of user identifiers + * @param data Event data in a key-value map. Use wrapped primitives if needed + */ + constructor(type: string, action: string, identifiers?: Map, data?: Map) { this.type = type; this.action = action; this.identifiers = identifiers ?? new Map(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.data = data ?? new Map(); + this.data = data ?? new Map(); } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index 9494549e7..f28a7005d 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -39,13 +39,14 @@ export class RestApiManager implements IRestApiManager { /** * Creates instance to access Optimizely Data Platform (ODP) REST API - * @param logger Collect and record events/errors for this REST implementation + * @param logger Collect and record events/errors for this GraphQL implementation * @param timeout Milliseconds to wait for a response + * @param requestHandler Desired request handler for testing */ - constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + constructor(logger: LogHandler, timeout?: number, requestHandler?: RequestHandler) { this.logger = logger; - this.timeout = timeout; - this.requestHandler = RequestHandlerFactory.createHandler(this.logger); + this.timeout = timeout ?? REQUEST_TIMEOUT_MS; + this.requestHandler = requestHandler ?? RequestHandlerFactory.createHandler(this.logger); } /** @@ -82,11 +83,17 @@ export class RestApiManager implements IRestApiManager { const request = this.requestHandler.makeRequest(endpoint, headers, method, data); const response = await request.responsePromise; statusCode = response.statusCode ?? statusCode; - } catch { - this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); + } catch (err) { + let message = 'network error'; + if (err instanceof Error) { + message = (err as Error).message; + } + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${message})`); + shouldRetry = true; } if (statusCode === 0) { + this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); shouldRetry = true; } diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index 239943207..c2c43ce8b 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -20,7 +20,6 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 't import { LogHandler, LogLevel } from '../lib/modules/logging'; import { GraphQLManager } from '../lib/plugins/odp/graphql_manager'; import { RequestHandler } from '../lib/utils/http_request_handler/http'; -import { Response as GraphQLResponse } from '../lib/plugins/odp/odp_types'; import { ODP_USER_KEY } from '../lib/utils/enums'; import { REQUEST_TIMEOUT_MS } from '../lib/utils/http_request_handler/config'; @@ -84,20 +83,19 @@ describe('GraphQLManager', () => { } } }`; - const manager = managerInstance(); - const response = manager['parseSegmentsResponseJson'](validJsonResponse) as GraphQLResponse; + const response = GraphQLManager.parseSegmentsResponseJson(validJsonResponse); expect(response).not.toBeUndefined(); expect(response?.errors).toHaveLength(0); expect(response?.data?.customer?.audiences?.edges).not.toBeNull(); - expect(response.data.customer.audiences.edges).toHaveLength(2); - let node = response.data.customer.audiences.edges[0].node; - expect(node.name).toEqual('has_email'); - expect(node.state).toEqual('qualified'); - node = response.data.customer.audiences.edges[1].node; - expect(node.name).toEqual('has_email_opted_in'); - expect(node.state).not.toEqual('qualified'); + expect(response?.data.customer.audiences.edges).toHaveLength(2); + let node = response?.data.customer.audiences.edges[0].node; + expect(node?.name).toEqual('has_email'); + expect(node?.state).toEqual('qualified'); + node = response?.data.customer.audiences.edges[1].node; + expect(node?.name).toEqual('has_email_opted_in'); + expect(node?.state).not.toEqual('qualified'); }); it('should parse an error response', () => { @@ -123,20 +121,17 @@ describe('GraphQLManager', () => { "customer": null } }`; - const manager = managerInstance(); - const response = manager['parseSegmentsResponseJson'](errorJsonResponse) as GraphQLResponse; + const response = GraphQLManager.parseSegmentsResponseJson(errorJsonResponse); expect(response).not.toBeUndefined(); - expect(response.data.customer).toBeNull(); + expect(response?.data.customer).toBeNull(); expect(response?.errors).not.toBeNull(); - expect(response.errors[0].extensions.classification).toEqual('InvalidIdentifierException'); + expect(response?.errors[0].extensions.classification).toEqual('InvalidIdentifierException'); }); it('should construct a valid GraphQL query string', () => { - const manager = managerInstance(); - - const response = manager['toGraphQLJson'](USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + const response = GraphQLManager.toGraphQLJson(USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(response) .toBe(`{"query" : "query {customer"(${USER_KEY} : "${USER_VALUE}") {audiences(subset: [\\"has_email\\",\\"has_email_opted_in\\",\\"push_on_sale\\"] {edges {node {name state}}}}}"}`, @@ -257,4 +252,3 @@ describe('GraphQLManager', () => { verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); - diff --git a/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts b/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts deleted file mode 100644 index cc28b96bf..000000000 --- a/packages/optimizely-sdk/tests/odpClientSendEvents.spec.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// - -import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpClient } from '../lib/plugins/odp/odp_client'; -import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; -import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -import { OdpEvent } from '../lib/plugins/odp/odp_event'; - -const API_KEY = 'not-real-api-key'; -const REST_API_ENDPOINT = 'https://events.example.com/v2/api'; -const JSON_EVENT_DATA = JSON.stringify( - [ - new OdpEvent('t1', 'a1', - new Map([['id-key-1', 'id-value-1']]), - new Map(Object.entries({ - key11: 'value-1', - key12: true, - key13: 3.5, - key14: null, - }))), - new OdpEvent('t2', 'a2', - new Map([['id-key-2', 'id-value-2']]), - new Map(Object.entries({ - key2: 'value-2', - }))), - ], -); -const VALID_RESPONSE_CODE = 200; -const BROWSER = 'browser'; -const NODE = 'node'; - -describe('OdpClient Send Events', () => { - const client = (type: string) => type === BROWSER ? - new OdpClient(instance(mockLogger), instance(mockBrowserRequestHandler)) : - new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler)); - - let mockLogger: LogHandler; - let mockBrowserRequestHandler: BrowserRequestHandler; - let mockNodeRequestHandler: NodeRequestHandler; - let mockOdpEvent: OdpEvent; - - beforeAll(() => { - mockLogger = mock(); - mockBrowserRequestHandler = mock(); - mockNodeRequestHandler = mock(); - mockOdpEvent = mock(); - }); - - beforeEach(() => { - resetCalls(mockLogger); - resetCalls(mockBrowserRequestHandler); - resetCalls(mockNodeRequestHandler); - resetCalls(mockOdpEvent); - }); - - it('(browser) should send events successfully', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: 'body-unimportant', - headers: {}, - }), - }); - - const response = await client(BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(response).toEqual(VALID_RESPONSE_CODE); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('(node) should send events successfully', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 200, - body: 'body-unimportant', - headers: {}, - }), - }); - - const response = await client(NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(response).toEqual(VALID_RESPONSE_CODE); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('(browser) should handle and return 400 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: '', - headers: {}, - }), - }); - - const statusCode = await client(BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(statusCode).toEqual(400); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('(node) should handle and return 400 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 400, - body: '', - headers: {}, - }), - }); - - const statusCode = await client(NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(statusCode).toEqual(400); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('(browser) should handle and return 500 HTTP response', async () => { - when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: '', - headers: {}, - }), - }); - - const statusCode = await client(BROWSER).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(statusCode).toEqual(500); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('(node) should handle and return 500 HTTP response', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.resolve({ - statusCode: 500, - body: '', - headers: {}, - }), - }); - - const statusCode = await client(NODE).sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(statusCode).toEqual(500); - verify(mockLogger.log(LogLevel.ERROR, anyString())).never(); - }); - - it('should handle a network timeout', async () => { - when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => { - }, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - const client = new OdpClient(instance(mockLogger), instance(mockNodeRequestHandler), 10); - - const statusCode = await client.sendEvents(API_KEY, REST_API_ENDPOINT, JSON_EVENT_DATA); - - expect(statusCode).toBe(0); - verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (network error)')).once(); - }); -}); - diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts index 02f381f9e..047014bf0 100644 --- a/packages/optimizely-sdk/tests/restApiManager.spec.ts +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -17,90 +17,98 @@ /// import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; -import { ErrorHandler, LogHandler } from '../lib/modules/logging'; +import { LogHandler, LogLevel } from '../lib/modules/logging'; import { RestApiManager } from '../lib/plugins/odp/rest_api_manager'; import { OdpEvent } from '../lib/plugins/odp/odp_event'; +import { REQUEST_TIMEOUT_MS } from '../lib/utils/http_request_handler/config'; +import { RequestHandler } from '../lib/utils/http_request_handler/http'; + +const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; +const ODP_REST_API_HOST = 'https://events.example.com/v2/api'; +const ODP_EVENTS = [ + new OdpEvent('t1', 'a1', + new Map([['id-key-1', 'id-value-1']]), + new Map(Object.entries({ + key11: 'value-1', + key12: true, + key13: 3.5, + key14: null, + }))), + new OdpEvent('t2', 'a2', + new Map([['id-key-2', 'id-value-2']]), + new Map(Object.entries({ + key2: 'value-2', + }))), +]; describe('RestApiManager', () => { - const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; - const ODP_REST_API_HOST = 'https://api.example.com'; - const ODP_EVENTS = [ - new OdpEvent('t1', 'a1', - new Map([['id-key-1', 'id-value-1']]), - new Map(Object.entries({ - key11: 'value-1', - key12: true, - key13: 3.5, - key14: null, - }))), - new OdpEvent('t2', 'a2', - new Map([['id-key-2', 'id-value-2']]), - new Map(Object.entries({ - key2: 'value-2', - }))), - ]; - - const makeManagerInstance = () => new RestApiManager(instance(mockLogger), instance(mockOdpClient)); - - let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; - let mockOdpClient: IOdpClient; + let mockRequestHandler: RequestHandler; beforeAll(() => { - mockErrorHandler = mock(); mockLogger = mock(); - mockOdpClient = mock(); + mockRequestHandler = mock(); }); beforeEach(() => { - resetCalls(mockErrorHandler); resetCalls(mockLogger); - resetCalls(mockOdpClient); + resetCalls(mockRequestHandler); }); + const managerInstance = () => new RestApiManager(instance(mockLogger), REQUEST_TIMEOUT_MS, instance(mockRequestHandler)); + const abortableRequest = (statusCode: number, body: string) => { + return { + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode, + body, + headers: {}, + }), + }; + }; + it('should should send events successfully and not suggest retry', async () => { - when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(200); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(200, '')); + const manager = managerInstance(); const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); expect(shouldRetry).toBe(false); - verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); it('should not suggest a retry for 400 HTTP response', async () => { - when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(400); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(400, '')); + const manager = managerInstance(); const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); expect(shouldRetry).toBe(false); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (400)')).once(); }); it('should suggest a retry for 500 HTTP response', async () => { - when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(500); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn(abortableRequest(500, '')); + const manager = managerInstance(); const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); expect(shouldRetry).toBe(true); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (500)')).once(); }); it('should suggest a retry for network timeout', async () => { - when(mockOdpClient.sendEvents(anyString(), anyString(), anyString())).thenResolve(0); - const manager = makeManagerInstance(); + when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const manager = managerInstance(); const shouldRetry = await manager.sendEvents(VALID_ODP_PUBLIC_KEY, ODP_REST_API_HOST, ODP_EVENTS); expect(shouldRetry).toBe(true); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); + verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (Request timed out)')).once(); }); }); - From 192b90b05d6d929f2f622b283f4911bf5cdea976 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 23 Sep 2022 13:16:52 -0400 Subject: [PATCH 30/35] Fixes after forgetting to pull first --- .../optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 6 +++--- packages/optimizely-sdk/tests/graphQlManager.spec.ts | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 06ac3bae3..0ea7d6df1 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -122,13 +122,13 @@ export class GraphQLManager implements IGraphQLManager { * Converts the query parameters to a GraphQL JSON payload * @returns GraphQL JSON string */ - public static toGraphQLJson(userKey: string, userValue: string, segmentsToCheck: string[]): string => ([ + private toGraphQLJson = (userKey: string, userValue: string, segmentsToCheck: string[]): string => ([ '{"query" : "query {customer"', `(${userKey} : "${userValue}") `, '{audiences', '(subset: [', - ... segmentsToCheck?.map((segment, index) => - `\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}` + ...segmentsToCheck?.map((segment, index) => + `\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}`, ) || '', '] {edges {node {name state}}}}}"}', ].join('')); diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index c2c43ce8b..b40ee11ed 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -83,8 +83,9 @@ describe('GraphQLManager', () => { } } }`; + const manager = managerInstance(); - const response = GraphQLManager.parseSegmentsResponseJson(validJsonResponse); + const response = manager['parseSegmentsResponseJson'](validJsonResponse); expect(response).not.toBeUndefined(); expect(response?.errors).toHaveLength(0); @@ -121,8 +122,9 @@ describe('GraphQLManager', () => { "customer": null } }`; + const manager = managerInstance(); - const response = GraphQLManager.parseSegmentsResponseJson(errorJsonResponse); + const response = manager['parseSegmentsResponseJson'](errorJsonResponse); expect(response).not.toBeUndefined(); expect(response?.data.customer).toBeNull(); @@ -131,7 +133,9 @@ describe('GraphQLManager', () => { }); it('should construct a valid GraphQL query string', () => { - const response = GraphQLManager.toGraphQLJson(USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + const manager = managerInstance(); + + const response = manager['toGraphQLJson'](USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); expect(response) .toBe(`{"query" : "query {customer"(${USER_KEY} : "${USER_VALUE}") {audiences(subset: [\\"has_email\\",\\"has_email_opted_in\\",\\"push_on_sale\\"] {edges {node {name state}}}}}"}`, From 220bed53351290cd7dcc379897e2e055ee714161 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 23 Sep 2022 14:18:36 -0400 Subject: [PATCH 31/35] Refactor constructors --- .../lib/plugins/odp/graphql_manager.ts | 11 +++--- .../lib/plugins/odp/odp_event.ts | 2 +- .../lib/plugins/odp/rest_api_manager.ts | 11 +++--- .../request_handler_factory.ts | 35 ------------------- .../tests/graphQlManager.spec.ts | 2 +- .../tests/restApiManager.spec.ts | 2 +- 6 files changed, 13 insertions(+), 50 deletions(-) delete mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 0ea7d6df1..d86854937 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -18,7 +18,6 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; -import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { ODP_USER_KEY } from '../../utils/enums'; import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http'; import { Response as GraphQLResponse } from './odp_types'; @@ -58,14 +57,14 @@ export class GraphQLManager implements IGraphQLManager { /** * Communicates with Optimizely Data Platform's GraphQL endpoint - * @param logger Collect and record events/errors for this GraphQL implementation - * @param timeout Milliseconds to wait for a response * @param requestHandler Desired request handler for testing + * @param logger Collect and record events/errors for this GraphQL implementation + * @param timeout?? Milliseconds to wait for a response */ - constructor(logger: LogHandler, timeout?: number, requestHandler?: RequestHandler) { + constructor(requestHandler: RequestHandler, logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + this.requestHandler = requestHandler; this.logger = logger; - this.timeout = timeout ?? REQUEST_TIMEOUT_MS; - this.requestHandler = requestHandler ?? RequestHandlerFactory.createHandler(this.logger); + this.timeout = timeout; } /** diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts index 8d105a599..4260cd30d 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_event.ts @@ -40,7 +40,7 @@ export class OdpEvent { * @param type Type of event (typically "fullstack") * @param action Subcategory of the event type * @param identifiers Key-value map of user identifiers - * @param data Event data in a key-value map. Use wrapped primitives if needed + * @param data Event data in a key-value map. */ constructor(type: string, action: string, identifiers?: Map, data?: Map) { this.type = type; diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index f28a7005d..5e36c2181 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -15,7 +15,6 @@ */ import { LogHandler, LogLevel } from '../../modules/logging'; -import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { OdpEvent } from './odp_event'; import { RequestHandler } from '../../utils/http_request_handler/http'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; @@ -39,14 +38,14 @@ export class RestApiManager implements IRestApiManager { /** * Creates instance to access Optimizely Data Platform (ODP) REST API - * @param logger Collect and record events/errors for this GraphQL implementation - * @param timeout Milliseconds to wait for a response * @param requestHandler Desired request handler for testing + * @param logger Collect and record events/errors for this GraphQL implementation + * @param timeout?? Milliseconds to wait for a response */ - constructor(logger: LogHandler, timeout?: number, requestHandler?: RequestHandler) { + constructor(requestHandler: RequestHandler, logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + this.requestHandler = requestHandler; this.logger = logger; - this.timeout = timeout ?? REQUEST_TIMEOUT_MS; - this.requestHandler = requestHandler ?? RequestHandlerFactory.createHandler(this.logger); + this.timeout = timeout; } /** diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts deleted file mode 100644 index ddab23faf..000000000 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright 2022 Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LogHandler } from '../../modules/logging'; -import { RequestHandler } from './http'; -import { NodeRequestHandler } from './node_request_handler'; -import { BrowserRequestHandler } from './browser_request_handler'; - -/** - * Factory to create the appropriate type of RequestHandler based on a provided context - */ -export class RequestHandlerFactory { - public static createHandler(logger: LogHandler, timeout?: number): RequestHandler { - if (window) { - return new BrowserRequestHandler(logger, timeout); - } else if (process) { - return new NodeRequestHandler(logger, timeout); - } else { - return null as unknown as RequestHandler; - } - } -} diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index b40ee11ed..c0a426849 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -46,7 +46,7 @@ describe('GraphQLManager', () => { resetCalls(mockRequestHandler); }); - const managerInstance = () => new GraphQLManager(instance(mockLogger), REQUEST_TIMEOUT_MS, instance(mockRequestHandler)); + const managerInstance = () => new GraphQLManager(instance(mockRequestHandler), instance(mockLogger), REQUEST_TIMEOUT_MS); const abortableRequest = (statusCode: number, body: string) => { return { diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts index 047014bf0..e2e9f2e08 100644 --- a/packages/optimizely-sdk/tests/restApiManager.spec.ts +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -55,7 +55,7 @@ describe('RestApiManager', () => { resetCalls(mockRequestHandler); }); - const managerInstance = () => new RestApiManager(instance(mockLogger), REQUEST_TIMEOUT_MS, instance(mockRequestHandler)); + const managerInstance = () => new RestApiManager(instance(mockRequestHandler), instance(mockLogger), REQUEST_TIMEOUT_MS); const abortableRequest = (statusCode: number, body: string) => { return { abort: () => { From 7bfd1ca0f85e187edcb9c2dad880e7baad66ed23 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 26 Sep 2022 09:37:13 -0400 Subject: [PATCH 32/35] More code review changes --- .../lib/plugins/odp/graphql_manager.ts | 10 +++------ .../lib/plugins/odp/rest_api_manager.ts | 6 +----- .../optimizely-sdk/lib/utils/enums/index.ts | 4 ++++ .../browser_request_handler.ts | 18 ++++++++-------- .../lib/utils/http_request_handler/config.ts | 20 ------------------ .../node_request_handler.ts | 14 ++++++------- .../tests/graphQlManager.spec.ts | 6 +++--- .../tests/restApiManager.spec.ts | 21 +++++++++---------- 8 files changed, 37 insertions(+), 62 deletions(-) delete mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/config.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index d86854937..fea78ef2e 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -21,7 +21,6 @@ import { OdpResponseSchema } from './odp_response_schema'; import { ODP_USER_KEY } from '../../utils/enums'; import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http'; import { Response as GraphQLResponse } from './odp_types'; -import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; /** * Expected value for a qualified/valid segment @@ -52,19 +51,16 @@ export interface IGraphQLManager { */ export class GraphQLManager implements IGraphQLManager { private readonly logger: LogHandler; - private readonly timeout: number; private readonly requestHandler: RequestHandler; /** * Communicates with Optimizely Data Platform's GraphQL endpoint * @param requestHandler Desired request handler for testing * @param logger Collect and record events/errors for this GraphQL implementation - * @param timeout?? Milliseconds to wait for a response */ - constructor(requestHandler: RequestHandler, logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + constructor(requestHandler: RequestHandler, logger: LogHandler) { this.requestHandler = requestHandler; this.logger = logger; - this.timeout = timeout; } /** @@ -103,9 +99,9 @@ export class GraphQLManager implements IGraphQLManager { if (parsedSegments.errors?.length > 0) { const errors = parsedSegments.errors.map((e) => e.message).join('; '); - this.logger.log(LogLevel.WARNING, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${errors})`); + this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${errors})`); - return EMPTY_SEGMENTS_COLLECTION; + return null; } const edges = parsedSegments?.data?.customer?.audiences?.edges; diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index 5e36c2181..25009bef8 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -17,7 +17,6 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { OdpEvent } from './odp_event'; import { RequestHandler } from '../../utils/http_request_handler/http'; -import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; @@ -33,19 +32,16 @@ export interface IRestApiManager { */ export class RestApiManager implements IRestApiManager { private readonly logger: LogHandler; - private readonly timeout: number; private readonly requestHandler: RequestHandler; /** * Creates instance to access Optimizely Data Platform (ODP) REST API * @param requestHandler Desired request handler for testing * @param logger Collect and record events/errors for this GraphQL implementation - * @param timeout?? Milliseconds to wait for a response */ - constructor(requestHandler: RequestHandler, logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + constructor(requestHandler: RequestHandler, logger: LogHandler) { this.requestHandler = requestHandler; this.logger = logger; - this.timeout = timeout; } /** diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index f631f96f1..10461a7ac 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -292,6 +292,10 @@ export enum NOTIFICATION_TYPES { TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', } +/** + * Default milliseconds before request timeout + */ +export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute /** * Valid types of Javascript contexts in which this code is executing diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index 187408a65..3a2fe73f0 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -15,19 +15,19 @@ */ import { AbortableRequest, Headers, RequestHandler, Response } from './http'; -import { REQUEST_TIMEOUT_MS } from './config'; import { LogHandler, LogLevel } from '../../modules/logging'; +import { REQUEST_TIMEOUT_MS } from '../enums'; /** * Handles sending requests and receiving responses over HTTP via XMLHttpRequest */ export class BrowserRequestHandler implements RequestHandler { - private readonly _logger: LogHandler; - private readonly _timeout: number; + private readonly logger: LogHandler; + private readonly timeout: number; public constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._logger = logger; - this._timeout = timeout; + this.logger = logger; + this.timeout = timeout; } /** @@ -35,7 +35,7 @@ export class BrowserRequestHandler implements RequestHandler { * @param requestUrl Fully-qualified URL to which to send the request * @param headers List of headers to include in the request * @param method HTTP method to use - * @param data stringified version of data to POST, PUT, etc + * @param data?? stringified version of data to POST, PUT, etc * @returns AbortableRequest contains both the response Promise and capability to abort() */ public makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { @@ -64,10 +64,10 @@ export class BrowserRequestHandler implements RequestHandler { } }; - request.timeout = this._timeout; + request.timeout = this.timeout; request.ontimeout = (): void => { - this._logger.log(LogLevel.WARNING, 'Request timed out'); + this.logger.log(LogLevel.WARNING, 'Request timed out'); }; request.send(data); @@ -122,7 +122,7 @@ export class BrowserRequestHandler implements RequestHandler { } } } catch { - this._logger.log(LogLevel.WARNING, `Unable to parse & skipped header item '${headerLine}'`); + this.logger.log(LogLevel.WARNING, `Unable to parse & skipped header item '${headerLine}'`); } }); return headers; diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts deleted file mode 100644 index 76ab92f82..000000000 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2019-2020, 2022 Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Default milliseconds before request timeout - */ -export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index d2d9c0bf9..7ddafa1f8 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -18,20 +18,20 @@ import http from 'http'; import https from 'https'; import url from 'url'; import { AbortableRequest, Headers, RequestHandler, Response } from './http'; -import { REQUEST_TIMEOUT_MS } from './config'; import decompressResponse from 'decompress-response'; import { LogHandler } from '../../modules/logging'; +import { REQUEST_TIMEOUT_MS } from '../enums'; /** * Handles sending requests and receiving responses over HTTP via NodeJS http module */ export class NodeRequestHandler implements RequestHandler { - private readonly _logger: LogHandler; - private readonly _timeout: number; + private readonly logger: LogHandler; + private readonly timeout: number; public constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._logger = logger; - this._timeout = timeout; + this.logger = logger; + this.timeout = timeout; } /** @@ -39,7 +39,7 @@ export class NodeRequestHandler implements RequestHandler { * @param requestUrl Fully-qualified URL to which to send the request * @param headers List of headers to include in the request * @param method HTTP method to use - * @param data stringified version of data to POST, PUT, etc + * @param data? stringified version of data to POST, PUT, etc * @returns AbortableRequest contains both the response Promise and capability to abort() */ public makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { @@ -60,7 +60,7 @@ export class NodeRequestHandler implements RequestHandler { ...headers, 'accept-encoding': 'gzip,deflate', }, - timeout: this._timeout, + timeout: this.timeout, }); const responsePromise = this.getResponseFromRequest(request); diff --git a/packages/optimizely-sdk/tests/graphQlManager.spec.ts b/packages/optimizely-sdk/tests/graphQlManager.spec.ts index c0a426849..8f2c228ff 100644 --- a/packages/optimizely-sdk/tests/graphQlManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQlManager.spec.ts @@ -21,7 +21,6 @@ import { LogHandler, LogLevel } from '../lib/modules/logging'; import { GraphQLManager } from '../lib/plugins/odp/graphql_manager'; import { RequestHandler } from '../lib/utils/http_request_handler/http'; import { ODP_USER_KEY } from '../lib/utils/enums'; -import { REQUEST_TIMEOUT_MS } from '../lib/utils/http_request_handler/config'; const API_key = 'not-real-api-key'; const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; @@ -32,6 +31,7 @@ const SEGMENTS_TO_CHECK = [ 'has_email_opted_in', 'push_on_sale', ]; + describe('GraphQLManager', () => { let mockLogger: LogHandler; let mockRequestHandler: RequestHandler; @@ -46,7 +46,7 @@ describe('GraphQLManager', () => { resetCalls(mockRequestHandler); }); - const managerInstance = () => new GraphQLManager(instance(mockRequestHandler), instance(mockLogger), REQUEST_TIMEOUT_MS); + const managerInstance = () => new GraphQLManager(instance(mockRequestHandler), instance(mockLogger)); const abortableRequest = (statusCode: number, body: string) => { return { @@ -192,7 +192,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, INVALID_USER_ID, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(0); + expect(segments).toBeNull(); verify(mockLogger.log(anything(), anyString())).once(); }); diff --git a/packages/optimizely-sdk/tests/restApiManager.spec.ts b/packages/optimizely-sdk/tests/restApiManager.spec.ts index e2e9f2e08..132649da7 100644 --- a/packages/optimizely-sdk/tests/restApiManager.spec.ts +++ b/packages/optimizely-sdk/tests/restApiManager.spec.ts @@ -20,25 +20,24 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 't import { LogHandler, LogLevel } from '../lib/modules/logging'; import { RestApiManager } from '../lib/plugins/odp/rest_api_manager'; import { OdpEvent } from '../lib/plugins/odp/odp_event'; -import { REQUEST_TIMEOUT_MS } from '../lib/utils/http_request_handler/config'; import { RequestHandler } from '../lib/utils/http_request_handler/http'; const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; const ODP_REST_API_HOST = 'https://events.example.com/v2/api'; +const data1 = new Map(); +data1.set('key11', 'value-1'); +data1.set('key12', true); +data1.set('key12', 3.5); +data1.set('key14', null); +const data2 = new Map(); +data2.set('key2', 'value-2'); const ODP_EVENTS = [ new OdpEvent('t1', 'a1', new Map([['id-key-1', 'id-value-1']]), - new Map(Object.entries({ - key11: 'value-1', - key12: true, - key13: 3.5, - key14: null, - }))), + data1), new OdpEvent('t2', 'a2', new Map([['id-key-2', 'id-value-2']]), - new Map(Object.entries({ - key2: 'value-2', - }))), + data2), ]; describe('RestApiManager', () => { @@ -55,7 +54,7 @@ describe('RestApiManager', () => { resetCalls(mockRequestHandler); }); - const managerInstance = () => new RestApiManager(instance(mockRequestHandler), instance(mockLogger), REQUEST_TIMEOUT_MS); + const managerInstance = () => new RestApiManager(instance(mockRequestHandler), instance(mockLogger)); const abortableRequest = (statusCode: number, body: string) => { return { abort: () => { From be1767d0fbe69481990bcdfa670e717e36efacab Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 26 Sep 2022 09:57:57 -0400 Subject: [PATCH 33/35] Formatting & whitespace edits --- .../lib/plugins/odp/graphql_manager.ts | 1 - .../lib/plugins/odp/odp_response_schema.ts | 46 +++++++++---------- .../node_request_handler.ts | 5 -- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index fea78ef2e..2c97694de 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -15,7 +15,6 @@ */ import { LogHandler, LogLevel } from '../../modules/logging'; - import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { ODP_USER_KEY } from '../../utils/enums'; diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index a2d59ab16..9aad4ac35 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -38,7 +38,7 @@ export const OdpResponseSchema = { customer: { title: 'The customer Schema', type: 'object', - required: [ ], + required: [], properties: { audiences: { title: 'The audiences Schema', @@ -107,15 +107,15 @@ export const OdpResponseSchema = { required: [ 'message', 'locations', - 'extensions' + 'extensions', ], properties: { message: { title: 'The message Schema', type: 'string', examples: [ - 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd' - ] + 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd', + ], }, locations: { title: 'The locations Schema', @@ -125,27 +125,27 @@ export const OdpResponseSchema = { type: 'object', required: [ 'line', - 'column' + 'column', ], properties: { line: { title: 'The line Schema', type: 'integer', examples: [ - 2 - ] + 2, + ], }, column: { title: 'The column Schema', type: 'integer', examples: [ - 3 - ] - } + 3, + ], + }, }, - examples: [] + examples: [], }, - examples: [] + examples: [], }, path: { title: 'The path Schema', @@ -154,32 +154,32 @@ export const OdpResponseSchema = { title: 'A Schema', type: 'string', examples: [ - 'customer' - ] + 'customer', + ], }, - examples: [] + examples: [], }, extensions: { title: 'The extensions Schema', type: 'object', required: [ - 'classification' + 'classification', ], properties: { classification: { title: 'The classification Schema', type: 'string', examples: [ - 'InvalidIdentifierException' - ] - } + 'InvalidIdentifierException', + ], + }, }, - examples: [] - } + examples: [], + }, }, - examples: [] + examples: [], }, - examples: [] + examples: [], }, }, examples: [], diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index 7ddafa1f8..3de55a9e7 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -170,8 +170,3 @@ export class NodeRequestHandler implements RequestHandler { }); } } - - - - - From ef9df47c1c27c19278ed45d737548cf677fa1a55 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 26 Sep 2022 13:19:14 -0400 Subject: [PATCH 34/35] Final clean up of execution context --- packages/optimizely-sdk/lib/utils/enums/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index 10461a7ac..38ea49a0c 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -297,15 +297,6 @@ export enum NOTIFICATION_TYPES { */ export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute -/** - * Valid types of Javascript contexts in which this code is executing - */ -export enum EXECUTION_CONTEXT_TYPE { - NOT_DEFINED, - BROWSER, - NODE, -} - /** * ODP User Key */ From 6552e7f9da540ea2b07cf82e67fd4344d2bb7127 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 26 Sep 2022 13:25:45 -0400 Subject: [PATCH 35/35] Code review changes --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 4 ++-- packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 2c97694de..12d8fbc8f 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -105,8 +105,8 @@ export class GraphQLManager implements IGraphQLManager { const edges = parsedSegments?.data?.customer?.audiences?.edges; if (!edges) { - this.logger.log(LogLevel.WARNING, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); - return EMPTY_SEGMENTS_COLLECTION; + this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); + return null; } return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); diff --git a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts index 25009bef8..de872f3cd 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/rest_api_manager.ts @@ -87,11 +87,6 @@ export class RestApiManager implements IRestApiManager { shouldRetry = true; } - if (statusCode === 0) { - this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (network error)`); - shouldRetry = true; - } - if (statusCode >= 400) { this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${statusCode})`); }