8000 feat: ODP REST API for Sending ODP Events by mikechu-optimizely · Pull Request #315 · optimizely/csharp-sdk · GitHub
[go: up one dir, main page]

Skip to content

feat: ODP REST API for Sending ODP Events #315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Code review changes
  • Loading branch information
mikechu-optimizely committed Oct 20, 2022
commit ca90ccb038db64f717d24b1e1e8a4c576d596ef2
4 changes: 2 additions & 2 deletions OptimizelySDK.Tests/OdpTests/OdpSegmentApiManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void ShouldParseSuccessfulResponse()
}
}";

var response = OdpSegmentApiManager.ParseSegmentsResponseJson(RESPONSE_JSON);
var response = OdpSegmentApiManager.DeserializeSegmentsFromJson(RESPONSE_JSON);

Assert.IsNull(response.Errors);
Assert.IsNotNull(response.Data);
Expand Down Expand Up @@ -119,7 +119,7 @@ public void ShouldParseErrorResponse()
}
}";

var response = OdpSegmentApiManager.ParseSegmentsResponseJson(RESPONSE_JSON);
var response = OdpSegmentApiManager.DeserializeSegmentsFromJson(RESPONSE_JSON);

Assert.IsNull(response.Data.Customer);
Assert.IsNotNull(response.Errors);
Expand Down
20 changes: 20 additions & 0 deletions OptimizelySDK/Odp/Constants.cs
8000
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,25 @@ public static class Constants
/// Specific key for designating the ODP API public key
/// </summary>
public const string HEADER_API_KEY = "x-api-key";

/// <summary>
/// Media type for json requests
/// </summary>
public const string APPLICATION_JSON_MEDIA_TYPE = "application/json";

/// <summary>
/// Path to ODP REST events API
/// </summary>
public const string ODP_EVENTS_API_ENDPOINT_PATH = "/v3/events";

/// <summary>
/// Path to ODP GraphQL API
/// </summary>
public const string ODP_GRAPHQL_API_ENDPOINT_PATH = "/v3/graphql";

/// <summary>
/// Default message when numeric HTTP status code is not available
/// </summary>
public const string NETWORK_ERROR_REASON = "network error";
}
}
31 changes: 17 additions & 14 deletions OptimizelySDK/Odp/OdpEventApiManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,31 +89,33 @@ public bool SendEvents(string apiKey, string apiHost, List<OdpEvent> events)
return false;
}

var endpoint = $"{apiHost}/v3/events";
var endpoint = $"{apiHost}{Constants.ODP_EVENTS_API_ENDPOINT_PATH}";
var data = JsonConvert.SerializeObject(events);
var shouldRetry = false;

HttpResponseMessage response = default;
try
{
response = SendEventsAsync(apiKey, endpoint, data).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
catch (HttpRequestException ex)
{
_errorHandler.HandleError(ex);
_logger.Log(LogLevel.ERROR, $"{EVENT_SENDING_FAILURE_MESSAGE} (network error)");
shouldRetry = true;
}

var responseStatusCode = response == null ? 0 : (int)response.StatusCode;
if (responseStatusCode >= 400)
{
var statusCode = response == null ? 0 : (int)response.StatusCode;

_logger.Log(LogLevel.ERROR,
$"{EVENT_SENDING_FAILURE_MESSAGE} (${responseStatusCode})");
}
$"{EVENT_SENDING_FAILURE_MESSAGE} ({(statusCode == 0 ? Constants.NETWORK_ERROR_REASON : statusCode.ToString())})");

if (responseStatusCode >= 500)
shouldRetry = statusCode >= 500;
}
catch (Exception ex)
{
_errorHandler.HandleError(ex);

_logger.Log(LogLevel.ERROR, $"{EVENT_SENDING_FAILURE_MESSAGE} ({Constants.NETWORK_ERROR_REASON})");

shouldRetry = true;
}

Expand All @@ -127,13 +129,13 @@ public bool SendEvents(string apiKey, string apiHost, List<OdpEvent> events)
/// <param name="endpoint">Fully-qualified ODP REST API endpoint</param>
/// <param name="data">JSON string version of ODP event data</param>
/// <returns>HTTP response endpoint</returns>
private async Task<HttpResponseMessage> SendEventsAsync(string apiKey, string endpoint,
private Task<HttpResponseMessage> SendEventsAsync(string apiKey, string endpoint,
string data
)
{
var request = BuildOdpEventMessage(apiKey, endpoint, data);

return await _httpClient.SendAsync(request);
return _httpClient.SendAsync(request);
}

/// <summary>
Expand All @@ -157,7 +159,8 @@ string data
Constants.HEADER_API_KEY, apiKey
},
},
Content = new StringContent(data, Encoding.UTF8, "application/json"),
Content = new StringContent(data, Encoding.UTF8,
Constants.APPLICATION_JSON_MEDIA_TYPE),
};

return request;
Expand Down
97 changes: 55 additions & 42 deletions OptimizelySDK/Odp/OdpSegmentApiManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -75,7 +74,7 @@ public OdpSegmentApiManager(ILogger logger = null, IErrorHandler errorHandler =
/// </summary>
/// <param name="apiKey">ODP public key</param>
/// <param name="apiHost">Host of ODP endpoint</param>
/// <param name="userKey">'vuid' or 'fs_user_id key'</param>
/// <param name="userKey">Either `vuid` or `fs_user_id key`</param>
/// <param name="userValue">Associated value to query for the user key</param>
/// <param name="segmentsToCheck">Audience segments to check for experiment inclusion</param>
/// <returns>Array of audience segments</returns>
Expand All @@ -95,21 +94,21 @@ public string[] FetchSegments(string apiKey, string apiHost, OdpUserKeyType user
return new string[0];
}

var endpoint = $"{apiHost}/v3/graphql";
var endpoint = $"{apiHost}{Constants.ODP_GRAPHQL_API_ENDPOINT_PATH}";
var query =
BuildGetSegmentsGraphQLQuery(userKey.ToString().ToLower(), userValue,
segmentsToCheck);

var segmentsResponseJson = QuerySegments(apiKey, endpoint, query);
if (CanBeJsonParsed(segmentsResponseJson))
if (string.IsNullOrWhiteSpace(segmentsResponseJson))
{
_logger.Log(LogLevel.ERROR,
$"{AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)");
return null;
}

var parsedSegments = ParseSegmentsResponseJson(segmentsResponseJson);
if (parsedSegments is null)
var parsedSegments = DeserializeSegmentsFromJson(segmentsResponseJson);
if (parsedSegments == null)
{
var message = $"{AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)";
_logger.Log(LogLevel.ERROR, message);
Expand All @@ -132,62 +131,85 @@ public string[] FetchSegments(string apiKey, string apiHost, OdpUserKeyType user
return null;
}

return parsedSegments.Data.Customer.Audiences.Edges.
Where(e => e.Node.State == BaseCondition.QUALIFIED).
Select(e => e.Node.Name).ToArray();
return parsedSegments.Data.Customer.Audiences.Edges
.Where(e => e.Node.State == BaseCondition.QUALIFIED)
.Select(e => e.Node.Name)
.ToArray();
}

/// <summary>
/// Build GraphQL query for getting segments
/// </summary>
/// <param name="userKey">'vuid' or 'fs_user_id key'</param>
/// <param name="userKey">Either `vuid` or `fs_user_id` key</param>
/// <param name="userValue">Associated value to query for the user key</param>
/// <param name="segmentsToCheck">Audience segments to check for experiment inclusion</param>
/// <returns>GraphQL string payload</returns>
private static string BuildGetSegmentsGraphQLQuery(string userKey, string userValue,
IEnumerable segmentsToCheck
)
{
var query =
"query($userId: String, $audiences: [String]) {customer(|userKey|: $userId){audiences(subset: $audiences) {edges {node {name state}}}}}".
Replace("|userKey|", userKey);

var variables =
"\"variables\": { \"userId\": \"|userValue|\", \"audiences\": |audiences| }".
Replace("|userValue|", userValue).
Replace("|audiences|", JsonConvert.SerializeObject(segmentsToCheck));

return $"{{ \"query\": \"{query}\", {variables} }}";
return
@"{
""query"": ""{
query($userId: String, $audiences: [String]) {
{
customer({userKey}: $userId) {
audiences(subset: $audiences) {
edges {
node {
name
< A3E2 span class='blob-code-inner blob-code-marker ' data-code-marker="+"> state
}
}
}
}
}
}
}"",
""variables"" : {
""userId"": ""{userValue}"",
""audiences"": {audiences}
}
}"
.Replace("{userKey}", userKey)
.Replace("{userValue}", userValue)
.Replace("{audiences}", JsonConvert.SerializeObject(segmentsToCheck));
}


/// <summary>
/// Synchronous handler for querying the ODP GraphQL endpoint
/// </summary>
/// <param name="apiKey">ODP public API key</param>
/// <param name="endpoint">Fully-qualified ODP GraphQL Endpoint</param>
/// <param name="query">GraphQL query string to send</param>
/// <returns>JSON response from ODP</returns>
private string QuerySegments(string apiKey, string endpoint,
string query
)
private string QuerySegments(string apiKey, string endpoint, string query)
{
HttpResponseMessage response;
HttpResponseMessage response = null;
try
{
response = QuerySegmentsAsync(apiKey, endpoint, query).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
catch (HttpRequestException ex)
{
_errorHandler.HandleError(ex);
_logger.Log(LogLevel.ERROR, $"{AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)");

var statusCode = response == null ? 0 : (int)response.StatusCode;


_logger.Log(LogLevel.ERROR,
$"{AUDIENCE_FETCH_FAILURE_MESSAGE} ({(statusCode == 0 ? Constants.NETWORK_ERROR_REASON : statusCode.ToString())})");

return default;
}

if (!response.IsSuccessStatusCode)
catch (Exception ex)
{
_errorHandler.HandleError(ex);

_logger.Log(LogLevel.ERROR,
$"{AUDIENCE_FETCH_FAILURE_MESSAGE} ({(int)response.StatusCode})");
$"{AUDIENCE_FETCH_FAILURE_MESSAGE} ({Constants.NETWORK_ERROR_REASON})");

return default;
}

Expand Down Expand Up @@ -231,28 +253,19 @@ string query
Constants.HEADER_API_KEY, apiKey
},
},
Content = new StringContent(query, Encoding.UTF8, "application/json"),
Content = new StringContent(query, Encoding.UTF8,
Constants.APPLICATION_JSON_MEDIA_TYPE),
};

return request;
}

/// <summary>
/// Ensure a string has content that can be parsed from JSON to an object
/// </summary>
/// <param name="jsonToValidate">Value containing possible JSON</param>
/// <returns>True if content could be interpreted as JSON else False</returns>
private static bool CanBeJsonParsed(string jsonToValidate)
{
return string.IsNullOrWhiteSpace(jsonToValidate);
}

/// <summary>
/// Parses JSON response
/// </summary>
/// <param name="jsonResponse">JSON response from ODP</param>
/// <returns>Strongly-typed ODP Response object</returns>
public static Response ParseSegmentsResponseJson(string jsonResponse)
public static Response DeserializeSegmentsFromJson(string jsonResponse)
{
return JsonConvert.DeserializeObject<Response>(jsonResponse);
}
Expand Down
0