Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3cb9ec6
Add ODP event, send event, & refactors
mikechu-optimizely Sep 2, 2022
5d6144d
WIP testing sendOdpEvents
mikechu-optimizely Sep 2, 2022
2890a0d
Unit tests + code ODP Client edits
mikechu-optimizely Sep 12, 2022
a3463b9
Refactor apiHost to apiEndpoint
mikechu-optimizely Sep 12, 2022
9eba320
WIP code review changes
mikechu-optimizely Sep 13, 2022
e34e842
Separate/refactor send events and query segments unit tests
mikechu-optimizely Sep 13, 2022
5d5f643
Code review requested changes
mikechu-optimizely Sep 15, 2022
c18637e
Code review resolutions
mikechu-optimizely Sep 16, 2022
fed5e9b
Validation refactoring per code review
mikechu-optimizely Sep 19, 2022
f40bd80
Replace execution context
mikechu-optimizely Sep 20, 2022
bbd1685
Ready OdpEvent for EventManager
mikechu-optimizely Sep 20, 2022
ec25064
Remove parameter wraps
mikechu-optimizely Sep 20, 2022
6eb4f18
Update OdpResponseSchema jsdoc
mikechu-optimizely Sep 20, 2022
5134c6e
Correct GraphQL vs REST managers
mikechu-optimizely Sep 20, 2022
e305858
Update unit tests + refactors
mikechu-optimizely Sep 20, 2022
1a44aa7
Bug fix GraphQL-ifying query parameters
mikechu-optimizely Sep 20, 2022
0ad9447
Change return on REST API Manager sendEvents()
mikechu-optimizely Sep 20, 2022
e9b2f38
WIP REST API Manager's test skeleton
mikechu-optimizely Sep 20, 2022
d5e5ec1
Refactor GQLMgr's name
mikechu-optimizely Sep 21, 2022
b480af4
Unit tests for REST API Manager
mikechu-optimizely Sep 21, 2022
d170d3d
Fix name of REST API class
mikechu-optimizely Sep 21, 2022
182bf29
Update packages/optimizely-sdk/lib/plugins/odp/odp_client.ts
mikechu-optimizely Sep 22, 2022
97b9f05
Update packages/optimizely-sdk/lib/plugins/odp/odp_client.ts
mikechu-optimizely Sep 22, 2022
768aea1
Revert "Replace execution context"
mikechu-optimizely Sep 22, 2022
afae4ce
Apply only needed ODP_USER_KEY & ODP_CONFIG_STATE
mikechu-optimizely Sep 20, 2022
33114a0
Code review changes
mikechu-optimizely Sep 22, 2022
829c6de
Moved OdpClient logic up to GraphQLManager and...
mikechu-optimizely Sep 22, 2022
a9273fa
Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts
mikechu-optimizely Sep 23, 2022
21a969c
More code review changes
mikechu-optimizely Sep 23, 2022
9b3a620
Merge remote-tracking branch 'origin/mike/odp-rest-api-interface' int…
mikechu-optimizely Sep 23, 2022
192b90b
Fixes after forgetting to pull first
mikechu-optimizely Sep 23, 2022
52f3bd9
Merge branch 'master' into mike/odp-rest-api-interface
mikechu-optimizely Sep 23, 2022
220bed5
Refactor constructors
mikechu-optimizely Sep 23, 2022
7bfd1ca
More code review changes
mikechu-optimizely Sep 26, 2022
be1767d
Formatting & whitespace edits
mikechu-optimizely Sep 26, 2022
740ae16
Merge branch 'master' into mike/odp-rest-api-interface
mikechu-optimizely Sep 26, 2022
ef9df47
Final clean up of execution context
mikechu-optimizely Sep 26, 2022
6552e7f
Code review changes
mikechu-optimizely Sep 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 87 additions & 45 deletions packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
* limitations under the License.
*/

import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging';
import { Response } from './odp_types';
import { IOdpClient, OdpClient } from './odp_client';
import { LogHandler, LogLevel } from '../../modules/logging';
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';
import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http';
import { Response as GraphQLResponse } from './odp_types';

/**
* Expected value for a qualified/valid segment
Expand All @@ -34,102 +33,145 @@ 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 {
fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[]>;
fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[] | null>;
}

/**
* 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;
private readonly _logger: LogHandler;
private readonly _odpClient: IOdpClient;
export class GraphQLManager implements IGraphQLManager {
private readonly logger: LogHandler;
private readonly requestHandler: RequestHandler;

/**
* Retrieves the audience segments from the Optimizely Data Platform (ODP)
* @param errorHandler Handler to record exceptions
* 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 client Client to use to send queries 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));
constructor(requestHandler: RequestHandler, logger: LogHandler) {
this.requestHandler = requestHandler;
this.logger = logger;
}

/**
* Retrieves the audience segments from ODP
* @param apiKey ODP public key
* @param apiHost 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, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise<string[]> {
const parameters = new QuerySegmentsParameters({
apiKey,
apiHost,
userKey,
userValue,
segmentsToCheck,
});
const segmentsResponse = await this._odpClient.querySegments(parameters);
if (!segmentsResponse) {
this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)');
public async fetchSegments(apiKey: string, apiHost: string, userKey: ODP_USER_KEY, userValue: string, segmentsToCheck: string[]): Promise<string[] | null> {
if (!apiKey || !apiHost) {
this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (Parameters apiKey or apiHost invalid)`);
return null;
}

if (segmentsToCheck?.length === 0) {
return EMPTY_SEGMENTS_COLLECTION;
}

const endpoint = `${apiHost}/v3/graphql`;
const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck);

const segmentsResponse = await this.querySegments(apiKey, endpoint, userKey, userValue, query);
if (!segmentsResponse) {
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)');
return EMPTY_SEGMENTS_COLLECTION;
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.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${errors})`);

return EMPTY_SEGMENTS_COLLECTION;
return null;
}

const edges = parsedSegments?.data?.customer?.audiences?.edges;
if (!edges) {
this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (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);
}

/**
* Converts the query parameters to a GraphQL JSON payload
* @returns GraphQL JSON 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 ? ',' : ''}`,
) || '',
'] {edges {node {name state}}}}}"}',
].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<string | null> {
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);
// 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 {
return EMPTY_JSON_RESPONSE;
}

if (validate(jsonObject, OdpResponseSchema, false)) {
return jsonObject as Response;
return jsonObject as GraphQLResponse;
}

return EMPTY_JSON_RESPONSE;
Expand Down
94 changes: 0 additions & 94 deletions packages/optimizely-sdk/lib/plugins/odp/odp_client.ts

This file was deleted.

51 changes: 51 additions & 0 deletions packages/optimizely-sdk/lib/plugins/odp/odp_event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* 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 {
/**
* 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<string, string>;

/**
* Event data in a key-value map
*/
public data: Map<string, unknown>;

/**
* 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.
*/
constructor(type: string, action: string, identifiers?: Map<string, string>, data?: Map<string, unknown>) {
this.type = type;
this.action = action;
this.identifiers = identifiers ?? new Map<string, string>();
this.data = data ?? new Map<string, unknown>();
}
}
Loading