8000 feat: Add ODP GraphQL AP Interface (#778) · optimizely/javascript-sdk@1a20b08 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1a20b08

Browse files
feat: Add ODP GraphQL AP Interface (#778)
* Initial implementation of GraphQLManager * schema definition for ODP * ODP response json schema * Add missing props to ODP response schema * Adjust schema validator to handle injected schemas * Implement schema validation for ODP JSON response * Building ODP clients for browser and node * GraphQLManager testing * Corrected ODP response schema * Handle all non-200 HTTP statuses * Add ODP tests * Fix tests * Resolve code review requests * More code review changes * Refactors to remove warns * WIP (tests failing) Browser vs Node request handling * still: cleaned code a bit; tests need attn * Fix enum string * Trying to get timeout test working I need to mock up the RequestHandlers * ODP tests * WIP nodeRequestHandler tests * Handle node request timeouts with tests * BroswerRequestHandler tests * Document RequestHandlers * ODP Client & GraphQL jsdoc * Remove `any` in `catch` * Revert "Remove `any` in `catch`" This reverts commit ef8dc52. * Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> * Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> * Update packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> * Code review changes * Remove throwError Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com>
1 parent 7b53fa8 commit 1a20b08

26 files changed

+2071
-275
lines changed

packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('lib/core/project_config/project_config_manager', function() {
9797
var errorMessage = globalStubErrorHandler.handleError.lastCall.args[0].message;
9898
assert.strictEqual(
9999
errorMessage,
100-
sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')
100+
sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR (Project Config JSON Schema)', 'projectId', 'is missing and it is required'),
101101
);
102102
return manager.onReady().then(function(result) {
103103
assert.include(result, {

packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { JSONSchema4 } from 'json-schema';
2121

2222
var schemaDefinition = {
2323
$schema: 'http://json-schema.org/draft-04/schema#',
24+
title: 'Project Config JSON Schema',
2425
type: 'object',
2526
properties: {
2627
projectId: {

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* https://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,9 +18,9 @@ import {
1818
getLogger,
1919
setErrorHandler,
2020
getErrorHandler,
21-
LogLevel
21+
LogLevel,
2222
} from './modules/logging';
23-
import { LocalStoragePendingEventsDispatcher } from '../lib/modules/event_processor';
23+
import { LocalStoragePendingEventsDispatcher } from './modules/event_processor';
2424
import configValidator from './utils/config_validator';
2525
import defaultErrorHandler from './plugins/error_handler';
2626
import defaultEventDispatcher from './plugins/event_dispatcher/index.browser';
@@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center';
3232
import { default as eventProcessor } from './plugins/event_processor';
3333
import { OptimizelyDecideOption, Client, Config } from './shared_types';
3434
import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager';
35+
import { EXECUTION_CONTEXT_TYPE } from './utils/enums';
36+
import { ExecutionContext } from './utils/execution_context';
3537

3638
const logger = getLogger();
3739
logHelper.setLogHandler(loggerPlugin.createLogger());
@@ -44,11 +46,13 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
4446

4547
let hasRetriedEvents = false;
4648

49+
ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER;
50+
4751
/**
4852
* Creates an instance of the Optimizely class
4953
* @param {Config} config
5054
* @return {Client|null} the Optimizely client object
51-
* null on error
55+
* null on error
5256
*/
5357
const createInstance = function(config: Config): Client | null {
5458
try {
@@ -70,6 +74,7 @@ const createInstance = function(config: Config): Client | null {
7074
try {
7175
configValidator.validate(config);
7276
isValidInstance = true;
77+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7378
} catch (ex: any) {
7479
logger.error(ex);
7580
}
@@ -141,11 +146,13 @@ const createInstance = function(config: Config): Client | null {
141146
false
142147
);
143148
}
149+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
144150
} catch (e: any) {
145151
logger.error(enums.LOG_MESSAGES.UNABLE_TO_ATTACH_UNLOAD, MODULE_NAME, e.message);
146152
}
147153

148154
return optimizely;
155+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
149156
} catch (e: any) {
150157
logger.error(e);
151158
return null;

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License. *
66
* You may obtain a copy of the License at *
77
* *
8-
* http://www.apache.org/licenses/LICENSE-2.0 *
8+
* https://www.apache.org/licenses/LICENSE-2.0 *
99
* *
1010
* Unless required by applicable law or agreed to in writing, software *
1111
* distributed under the License is distributed on an "AS IS" BASIS, *
@@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center';
3232
import { createEventProcessor } from './plugins/event_processor';
3333
import { OptimizelyDecideOption, Client, Config } from './shared_types';
3434
import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager';
35+
import { ExecutionContext } from './utils/execution_context';
36+
import { EXECUTION_CONTEXT_TYPE } from './utils/enums';
3537

3638
const logger = getLogger();
3739
setLogLevel(LogLevel.ERROR);
@@ -40,13 +42,15 @@ const DEFAULT_EVENT_BATCH_SIZE = 10;
4042
const DEFAULT_EVENT_FLUSH_INTERVAL = 30000; // Unit is ms, default is 30s
4143
const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
4244

45+
ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.NODE;
46+
4347
/**
4448
* Creates an instance of the Optimizely class
4549
* @param {Config} config
4650
* @return {Client|null} the Optimizely client object
47-
* null on error
51+
* null on error
4852
*/
49-
const createInstance = function(config: Config): Client | null {
53+
const createInstance = function(config: Config): Client | null {
5054
try {
5155
let hasLogger = false;
5256
let isValidInstance = false;
@@ -68,6 +72,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
6872
try {
6973
configValidator.validate(config);
7074
isValidInstance = true;
75+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7176
} catch (ex: any) {
7277
if (hasLogger) {
7378
logger.error(ex);
@@ -117,6 +122,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
117122
};
118123

119124
return new Optimizely(optimizelyOptions);
125+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
120126
} catch (e: any) {
121127
logger.error(e);
122128
return null;

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* https://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center';
3232
import { createEventProcessor } from './plugins/event_processor/index.react_native';
3333
import { OptimizelyDecideOption, Client, Config } from './shared_types';
3434
import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager';
35+
import { EXECUTION_CONTEXT_TYPE } from './utils/enums';
36+
import { ExecutionContext } from './utils/execution_context';
3537

3638
const logger = getLogger();
3739
setLogHandler(loggerPlugin.createLogger());
@@ -41,13 +43,15 @@ const DEFAULT_EVENT_BATCH_SIZE = 10;
4143
const DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s
4244
const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
4345

46+
ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER;
47+
4448
/**
4549
* Creates an instance of the Optimizely class
4650
* @param {Config} config
4751
* @return {Client|null} the Optimizely client object
48-
* null on error
52+
* null on error
4953
*/
50-
const createInstance = function(config: Config): Client | null {
54+
const createInstance = function(config: Config): Client | null {
5155
try {
5256
// TODO warn about setting per instance errorHandler / logger / logLevel
5357
let isValidInstance = false;
@@ -67,6 +71,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
6771
try {
6872
configValidator.validate(config);
6973
isValidInstance = true;
74+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7075
} catch (ex: any) {
7176
logger.error(ex);
7277
}
@@ -117,6 +122,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000;
117122
}
118123

119124
return new Optimizely(optimizelyOptions);
125+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
120126
} catch (e: any) {
121127
logger.error(e);
122128
return null;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Copyright 2022, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging';
18+
import { Response } from './odp_types';
19+
import { IOdpClient, OdpClient } from './odp_client';
20+
import { validate } from '../../utils/json_schema_validator';
21+
import { OdpResponseSchema } from './odp_response_schema';
22+
import { QuerySegmentsParameters } from './query_segments_parameters';
23+
import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory';
24+
25+
/**
26+
* Expected value for a qualified/valid segment
27+
*/
28+
const QUALIFIED = 'qualified';
29+
/**
30+
* Return value when no valid segments found
31+
*/
32+
const EMPTY_SEGMENTS_COLLECTION: string[] = [];
33+
/**
34+
* Return value for scenarios with no valid JSON
35+
*/
36+
const EMPTY_JSON_RESPONSE = null;
37+
38+
/**
39+
* Manager for communicating with the Optimizely Data Platform GraphQL endpoint
40+
*/
41+
export interface IGraphQLManager {
42+
fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[]>;
43+
}
44+
45+
/**
46+
* Concrete implementation for communicating with the Optimizely Data Platform GraphQL endpoint
47+
*/
48+
export class GraphqlManager implements IGraphQLManager {
49+
private readonly _errorHandler: ErrorHandler;
50+
private readonly _logger: LogHandler;
51+
private readonly _odpClient: IOdpClient;
52+
53+
/**
54+
* Retrieves the audience segments from the Optimizely Data Platform (ODP)
55+
* @param errorHandler Handler to record exceptions
56+
* @param logger Collect and record events/errors for this GraphQL implementation
57+
* @param client Client to use to send queries to ODP
58+
*/
59+
constructor(errorHandler: ErrorHandler, logger: LogHandler, client?: IOdpClient) {
60+
this._errorHandler = errorHandler;
61+
this._logger = logger;
62+
63+
this._odpClient = client ?? new OdpClient(this._errorHandler,
64+
this._logger,
65+
RequestHandlerFactory.createHandler(this._logger));
66+
}
67+
68+
/**
69+
* Retrieves the audience segments from ODP
70+
* @param apiKey ODP public key
71+
* @param apiHost Fully-qualified URL of ODP
72+
* @param userKey 'vuid' or 'fs_user_id key'
73+
* @param userValue Associated value to query for the user key
74+
* @param segmentsToCheck Audience segments to check for experiment inclusion
75+
*/
76+
public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[]> {
77+
const parameters = new QuerySegmentsParameters({
78+
apiKey,
79+
apiHost,
80+
userKey,
81+
userValue,
82+
segmentsToCheck,
83+
});
84+
const segmentsResponse = await this._odpClient.querySegments(parameters);
85+
if (!segmentsResponse) {
86+
this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)');
87+
return EMPTY_SEGMENTS_COLLECTION;
88+
}
89+
90+
const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse);
91+
if (!parsedSegments) {
92+
this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)');
93+
return EMPTY_SEGMENTS_COLLECTION;
94+
}
95+
96+
if (parsedSegments.errors?.length > 0) {
97+
const errors = parsedSegments.errors.map((e) => e.message).join('; ');
98+
99+
this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`);
100+
101+
return EMPTY_SEGMENTS_COLLECTION;
102+
}
103+
104+
const edges = parsedSegments?.data?.customer?.audiences?.edges;
105+
if (!edges) {
106+
this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)');
107+
return EMPTY_SEGMENTS_COLLECTION;
108+
}
109+
110+
return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name);
111+
}
112+
113+
/**
114+
* Parses JSON response
115+
* @param jsonResponse JSON response from ODP
116+
* @private
117+
* @returns Response Strongly-typed ODP Response object
118+
*/
119+
private parseSegmentsResponseJson(jsonResponse: string): Response | null {
120+
let jsonObject = {};
121+
122+
try {
123+
jsonObject = JSON.parse(jsonResponse);
124+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
125+
} catch (error: any) {
126+
this._errorHandler.handleError(error);
127+
this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.');
128+
return EMPTY_JSON_RESPONSE;
129+
}
130+
131+
if (validate(jsonObject, OdpResponseSchema, false)) {
132+
return jsonObject as Response;
133+
}
134+
135+
return EMPTY_JSON_RESPONSE;
136+
}
137+
}

0 commit comments

Comments
 (0)
0