Skip to content

Commit f6c2232

Browse files
mikechu-optimizelyjaeoptzashraf1985
authored
feat: ODP REST API for Sending ODP Events (optimizely#786)
* Add ODP event, send event, & refactors * Unit tests + code ODP Client edits * Refactor apiHost to apiEndpoint * Separate/refactor send events and query segments unit tests * Code review requested changes * Code review resolutions * Validation refactoring per code review * Replace execution context * Ready OdpEvent for EventManager * Remove parameter wraps * Update OdpResponseSchema jsdoc * Correct GraphQL vs REST managers * Update unit tests + refactors * Bug fix GraphQL-ifying query parameters * Change return on REST API Manager sendEvents() * Refactor GQLMgr's name * Unit tests for REST API Manager * Fix name of REST API class * Update packages/optimizely-sdk/lib/plugins/odp/odp_client.ts Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Update packages/optimizely-sdk/lib/plugins/odp/odp_client.ts Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Revert "Replace execution context" This reverts commit f40bd80. * Apply only needed ODP_USER_KEY & ODP_CONFIG_STATE * Code review changes * Moved OdpClient logic up to GraphQLManager and... * Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> * More code review changes * Fixes after forgetting to pull first * Refactor constructors * More code review changes * Formatting & whitespace edits * Final clean up of execution context * Code review changes Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com>
1 parent 267f12b commit f6c2232

16 files changed

+679
-780
lines changed

packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging';
18-
import { Response } from './odp_types';
19-
import { IOdpClient, OdpClient } from './odp_client';
17+
import { LogHandler, LogLevel } from '../../modules/logging';
2018
import { validate } from '../../utils/json_schema_validator';
2119
import { OdpResponseSchema } from './odp_response_schema';
22-
import { QuerySegmentsParameters } from './query_segments_parameters';
23-
import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory';
20+
import { ODP_USER_KEY } from '../../utils/enums';
21+
import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http';
22+
import { Response as GraphQLResponse } from './odp_types';
2423

2524
/**
2625
* Expected value for a qualified/valid segment
@@ -34,102 +33,145 @@ const EMPTY_SEGMENTS_COLLECTION: string[] = [];
3433
* Return value for scenarios with no valid JSON
3534
*/
3635
const EMPTY_JSON_RESPONSE = null;
36+
/**
37+
* Standard message for audience querying fetch errors
38+
*/
39+
const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed';
3740

3841
/**
3942
* Manager for communicating with the Optimizely Data Platform GraphQL endpoint
4043
*/
4144
export interface IGraphQLManager {
42-
fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[]>;
45+
fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[] | null>;
4346
}
4447

4548
/**
46-
* Concrete implementation for communicating with the Optimizely Data Platform GraphQL endpoint
49+
* Concrete implementation for communicating with the ODP GraphQL endpoint
4750
*/
48-
export class GraphqlManager implements IGraphQLManager {
49-
private readonly _errorHandler: ErrorHandler;
50-
private readonly _logger: LogHandler;
51-
private readonly _odpClient: IOdpClient;
51+
export class GraphQLManager implements IGraphQLManager {
52+
private readonly logger: LogHandler;
53+
private readonly requestHandler: RequestHandler;
5254

5355
/**
54-
* Retrieves the audience segments from the Optimizely Data Platform (ODP)
55-
* @param errorHandler Handler to record exceptions
56+
* Communicates with Optimizely Data Platform's GraphQL endpoint
57+
* @param requestHandler Desired request handler for testing
5658
* @param logger Collect and record events/errors for this GraphQL implementation
57-
* @param client Client to use to send queries to ODP
5859
*/
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));
60+
constructor(requestHandler: RequestHandler, logger: LogHandler) {
61+
this.requestHandler = requestHandler;
62+
this.logger = logger;
6663
}
6764

6865
/**
6966
* Retrieves the audience segments from ODP
7067
* @param apiKey ODP public key
71-
* @param apiHost Fully-qualified URL of ODP
68+
* @param apiHost Host of ODP endpoint
7269
* @param userKey 'vuid' or 'fs_user_id key'
7370
* @param userValue Associated value to query for the user key
7471
* @param segmentsToCheck Audience segments to check for experiment inclusion
7572
*/
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)');
73+
public async fetchSegments(apiKey: string, apiHost: string, userKey: ODP_USER_KEY, userValue: string, segmentsToCheck: string[]): Promise<string[] | null> {
74+
if (!apiKey || !apiHost) {
75+
this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (Parameters apiKey or apiHost invalid)`);
76+
return null;
77+
}
78+
79+
if (segmentsToCheck?.length === 0) {
8780
return EMPTY_SEGMENTS_COLLECTION;
8881
}
8982

83+
const endpoint = `${apiHost}/v3/graphql`;
84+
const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck);
85+
86+
const segmentsResponse = await this.querySegments(apiKey, endpoint, userKey, userValue, query);
87+
if (!segmentsResponse) {
88+
this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`);
89+
return null;
90+
}
91+
9092
const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse);
9193
if (!parsedSegments) {
92-
this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)');
93-
return EMPTY_SEGMENTS_COLLECTION;
94+
this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`);
95+
return null;
9496
}
9597

9698
if (parsedSegments.errors?.length > 0) {
9799
const errors = parsedSegments.errors.map((e) => e.message).join('; ');
98100

99-
this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`);
101+
this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${errors})`);
100102

101-
return EMPTY_SEGMENTS_COLLECTION;
103+
return null;
102104
}
103105

104106
const edges = parsedSegments?.data?.customer?.audiences?.edges;
105107
if (!edges) {
106-
this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)');
107-
return EMPTY_SEGMENTS_COLLECTION;
108+
this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`);
109+
return null;
108110
}
109111

110112
return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name);
111113
}
112114

115+
/**
116+
* Converts the query parameters to a GraphQL JSON payload
117+
* @returns GraphQL JSON string
118+
*/
119+
private toGraphQLJson = (userKey: string, userValue: string, segmentsToCheck: string[]): string => ([
120+
'{"query" : "query {customer"',
121+
`(${userKey} : "${userValue}") `,
122+
'{audiences',
123+
'(subset: [',
124+
...segmentsToCheck?.map((segment, index) =>
125+
`\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}`,
126+
) || '',
127+
'] {edges {node {name state}}}}}"}',
128+
].join(''));
129+
130+
/**
131+
* Handler for querying the ODP GraphQL endpoint
132+
* @param apiKey ODP API key
133+
* @param endpoint Fully-qualified GraphQL endpoint URL
134+
* @param userKey 'vuid' or 'fs_user_id'
135+
* @param userValue userKey's value
136+
* @param query GraphQL formatted query string
137+
* @returns JSON response string from ODP or null
138+
*/
139+
private async querySegments(apiKey: string, endpoint: string, userKey: string, userValue: string, query: string): Promise<string | null> {
140+
const method = 'POST';
141+
const url = endpoint;
142+
const headers = {
143+
'Content-Type': 'application/json',
144+
'x-api-key': apiKey,
145+
};
146+
147+
let response: HttpResponse;
148+
try {
149+
const request = this.requestHandler.makeRequest(url, headers, method, query);
150+
response = await request.responsePromise;
151+
} catch {
152+
return null;
153+
}
154+
155+
return response.body;
156+
}
157+
113158
/**
114159
* Parses JSON response
115160
* @param jsonResponse JSON response from ODP
116161
* @private
117162
* @returns Response Strongly-typed ODP Response object
118163
*/
119-
private parseSegmentsResponseJson(jsonResponse: string): Response | null {
164+
private parseSegmentsResponseJson(jsonResponse: string): GraphQLResponse | null {
120165
let jsonObject = {};
121166

122167
try {
123168
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.');
169+
} catch {
128170
return EMPTY_JSON_RESPONSE;
129171
}
130172

131173
if (validate(jsonObject, OdpResponseSchema, false)) {
132-
return jsonObject as Response;
174+
return jsonObject as GraphQLResponse;
133175
}
134176

135177
return EMPTY_JSON_RESPONSE;

packages/optimizely-sdk/lib/plugins/odp/odp_client.ts

Lines changed: 0 additions & 94 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
export class OdpEvent {
18+
/**
19+
* Type of event (typically "fullstack")
20+
*/
21+
public type: string;
22+
23+
/**
24+
* Subcategory of the event type
25+
*/
26+
public action: string;
27+
28+
/**
29+
* Key-value map of user identifiers
30+
*/
31+
public identifiers: Map<string, string>;
32+
33+
/**
34+
* Event data in a key-value map
35+
*/
36+
public data: Map<string, unknown>;
37+
38+
/**
39+
* Event to be sent and stored in the Optimizely Data Platform
40+
* @param type Type of event (typically "fullstack")
41+
* @param action Subcategory of the event type
42+
* @param identifiers Key-value map of user identifiers
43+
* @param data Event data in a key-value map.
44+
*/
45+
constructor(type: string, action: string, identifiers?: Map<string, string>, data?: Map<string, unknown>) {
46+
this.type = type;
47+
this.action = action;
48+
this.identifiers = identifiers ?? new Map<string, string>();
49+
this.data = data ?? new Map<string, unknown>();
50+
}
51+
}

0 commit comments

Comments
 (0)