Skip to content

Commit bd28db6

Browse files
Add request telemetry (#434)
* Add recording of telemetry to ApiClient * Allow reading of RecordTelemetry from .json config * Formatting * Add test to confirm configuring RecordTelemetry * Readonly CodeQL fixes * Readme spelling mistakes * Change constant variables to const
1 parent 7458826 commit bd28db6

13 files changed

+220
-17
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,29 @@ The execution of integration tests require the following environment variables s
240240
* For Default account systems (OAuth): `CHECKOUT_DEFAULT_OAUTH_CLIENT_ID` & `CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET`
241241
* For Previous account systems (ABC): `CHECKOUT_PREVIOUS_PUBLIC_KEY` & `CHECKOUT_PREVIOUS_SECRET_KEY`
242242

243+
## Telemetry
244+
Request telemetry is enabled by default in the .NET SDK. Request latency is included in the telemetry data. Recording the request latency allows Checkout.com to continuously monitor and improve the merchant experience.
245+
246+
Request telemetry can be disabled by opting out during CheckoutSdk builder step:
247+
```
248+
ICheckoutApi api = CheckoutSdk.Builder().StaticKeys()
249+
.SecretKey("secret_key")
250+
.RecordTelemetry(false)
251+
.Environment(Environment.Sandbox)
252+
.Build();
253+
```
254+
255+
Or when using `CheckoutSDK.Extensions.Microsoft`:
256+
```
257+
{
258+
"Checkout": {
259+
...
260+
"RecordTelemetry": false,
261+
...
262+
}
263+
}
264+
```
265+
243266
## Code of Conduct
244267

245268
Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)

src/CheckoutSdk.Extensions/CheckoutOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class CheckoutOptions
2323
public PlatformType? PlatformType { get; set; }
2424

2525
public IHttpClientFactory HttpClientFactory { get; set; }
26-
26+
27+
public bool RecordTelemetry { get; set; } = true;
2728
}
2829
}

src/CheckoutSdk.Extensions/CheckoutServiceCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ private static void SetCommonAttributes<TB, TC>(
135135
{
136136
builder.HttpClientFactory(httpClientFactory);
137137
}
138+
builder.RecordTelemetry(options.RecordTelemetry);
138139
}
139140
}
140141
}

src/CheckoutSdk/AbstractCheckoutSdkBuilder.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace Checkout
99
public abstract class AbstractCheckoutSdkBuilder<T>
1010
{
1111
protected Environment Env = Checkout.Environment.Sandbox;
12+
13+
private bool _recordTelemetry = true;
1214
private EnvironmentSubdomain _envSubdomain;
1315
protected IHttpClientFactory ClientFactory = new DefaultHttpClientFactory();
1416

@@ -24,6 +26,12 @@ public AbstractCheckoutSdkBuilder<T> EnvironmentSubdomain(string subdomain)
2426
return this;
2527
}
2628

29+
public AbstractCheckoutSdkBuilder<T> RecordTelemetry(bool recordTelemetry)
30+
{
31+
_recordTelemetry = recordTelemetry;
32+
return this;
33+
}
34+
2735
#if (NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER)
2836
public AbstractCheckoutSdkBuilder<T> LogProvider(ILoggerFactory loggerFactory)
2937
{
@@ -40,7 +48,7 @@ public AbstractCheckoutSdkBuilder<T> HttpClientFactory(IHttpClientFactory httpCl
4048

4149
protected CheckoutConfiguration GetCheckoutConfiguration()
4250
{
43-
return new CheckoutConfiguration(GetSdkCredentials(), Env, _envSubdomain, ClientFactory);
51+
return new CheckoutConfiguration(GetSdkCredentials(), Env, _envSubdomain, ClientFactory, _recordTelemetry);
4452
}
4553

4654
protected abstract SdkCredentials GetSdkCredentials();

src/CheckoutSdk/ApiClient.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
using System.Web;
44
#endif
55
using System;
6+
using System.Collections.Concurrent;
67
using System.Collections.Generic;
8+
using System.Diagnostics;
79
using System.Linq;
810
using System.Net;
911
using System.Net.Http;
@@ -19,16 +21,21 @@ public class ApiClient : IApiClient
1921
private readonly ILogger _log = LogProvider.GetLogger(typeof(ApiClient));
2022
#endif
2123

24+
private const string sdkTelemetryHeader = "cko-sdk-telemetry";
25+
private const int maxCountInTelemetryQueue = 10;
2226
private readonly HttpClient _httpClient;
2327
private readonly Uri _baseUri;
2428
private readonly ISerializer _serializer = new JsonSerializer();
29+
private readonly ConcurrentQueue<RequestMetrics> requestMetricsQueue = new ConcurrentQueue<RequestMetrics>();
30+
private readonly bool _enableTelemetry;
2531

26-
public ApiClient(IHttpClientFactory httpClientFactory, Uri baseUri)
32+
public ApiClient(IHttpClientFactory httpClientFactory, Uri baseUri, bool enableTelemetry)
2733
{
2834
CheckoutUtils.ValidateParams("httpClientFactory", httpClientFactory, "baseUri", baseUri);
2935
var httpClient = httpClientFactory.CreateClient();
3036
_baseUri = baseUri;
3137
_httpClient = httpClient;
38+
_enableTelemetry = enableTelemetry;
3239
}
3340

3441
public async Task<TResult> Get<TResult>(
@@ -170,7 +177,7 @@ public async Task<TResult> Query<TResult>(
170177
$"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
171178
#endif
172179

173-
path = $"{path}?{queryString}";
180+
path = $"{path}?{queryString}";
174181
}
175182
}
176183

@@ -246,8 +253,37 @@ private async Task<HttpResponseMessage> Invoke(
246253
{
247254
httpRequest.Headers.Add("Cko-Idempotency-Key", idempotencyKey);
248255
}
256+
257+
if (_enableTelemetry)
258+
{
259+
var currentRequestId = Guid.NewGuid().ToString();
260+
if (requestMetricsQueue.TryDequeue(out var lastRequestMetric))
261+
{
262+
lastRequestMetric.RequestId = currentRequestId;
263+
httpRequest.Headers.TryAddWithoutValidation(sdkTelemetryHeader, _serializer.Serialize(lastRequestMetric));
264+
}
249265

250-
return await _httpClient.SendAsync(httpRequest, cancellationToken);
266+
Stopwatch stopwatch = new Stopwatch();
267+
stopwatch.Start();
268+
var result = await _httpClient.SendAsync(httpRequest, cancellationToken);
269+
stopwatch.Stop();
270+
271+
if (requestMetricsQueue.Count < maxCountInTelemetryQueue)
272+
{
273+
lastRequestMetric.PrevRequestDuration = stopwatch.ElapsedMilliseconds;
274+
requestMetricsQueue.Enqueue(new RequestMetrics()
275+
{
276+
PrevRequestDuration = stopwatch.ElapsedMilliseconds,
277+
PrevRequestId = currentRequestId
278+
});
279+
}
280+
281+
return result;
282+
}
283+
else
284+
{
285+
return await _httpClient.SendAsync(httpRequest, cancellationToken);
286+
}
251287
}
252288

253289
private async Task ValidateResponseAsync(HttpResponseMessage httpResponse)

src/CheckoutSdk/CheckoutApi.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,25 +79,29 @@ private static ApiClient BaseApiClient(CheckoutConfiguration configuration)
7979
return new ApiClient(configuration.HttpClientFactory,
8080
configuration.EnvironmentSubdomain != null
8181
? configuration.EnvironmentSubdomain.ApiUri
82-
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri);
82+
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri,
83+
configuration.RecordTelemetry);
8384
}
8485

8586
private static ApiClient FilesApiClient(CheckoutConfiguration configuration)
8687
{
8788
return new ApiClient(configuration.HttpClientFactory,
88-
configuration.Environment.GetAttribute<EnvironmentAttribute>().FilesApiUri);
89+
configuration.Environment.GetAttribute<EnvironmentAttribute>().FilesApiUri,
90+
configuration.RecordTelemetry);
8991
}
9092

9193
private static ApiClient TransfersApiClient(CheckoutConfiguration configuration)
9294
{
9395
return new ApiClient(configuration.HttpClientFactory,
94-
configuration.Environment.GetAttribute<EnvironmentAttribute>().TransfersApiUri);
96+
configuration.Environment.GetAttribute<EnvironmentAttribute>().TransfersApiUri,
97+
configuration.RecordTelemetry);
9598
}
9699

97100
private static ApiClient BalancesApiClient(CheckoutConfiguration configuration)
98101
{
99102
return new ApiClient(configuration.HttpClientFactory,
100-
configuration.Environment.GetAttribute<EnvironmentAttribute>().BalancesApiUri);
103+
configuration.Environment.GetAttribute<EnvironmentAttribute>().BalancesApiUri,
104+
configuration.RecordTelemetry);
101105
}
102106

103107

src/CheckoutSdk/CheckoutConfiguration.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public class CheckoutConfiguration
1010

1111
public IHttpClientFactory HttpClientFactory { get; }
1212

13+
public bool RecordTelemetry { get; }
14+
1315
public CheckoutConfiguration(
1416
SdkCredentials sdkCredentials,
1517
Environment environment,
@@ -28,7 +30,8 @@ public CheckoutConfiguration(
2830
SdkCredentials sdkCredentials,
2931
Environment environment,
3032
EnvironmentSubdomain environmentSubdomain,
31-
IHttpClientFactory httpClientFactory)
33+
IHttpClientFactory httpClientFactory,
34+
bool recordTelemetry)
3235
{
3336
CheckoutUtils.ValidateParams(
3437
"sdkCredentials", sdkCredentials,
@@ -38,6 +41,7 @@ public CheckoutConfiguration(
3841
Environment = environment;
3942
EnvironmentSubdomain = environmentSubdomain;
4043
HttpClientFactory = httpClientFactory;
44+
RecordTelemetry = recordTelemetry;
4145
}
4246
}
4347
}

src/CheckoutSdk/Previous/AbstractCheckoutApmApi.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ protected AbstractCheckoutApmApi(CheckoutConfiguration configuration)
1515
var apiClient = new ApiClient(configuration.HttpClientFactory,
1616
configuration.EnvironmentSubdomain != null
1717
? configuration.EnvironmentSubdomain.ApiUri
18-
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri);
18+
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri,
19+
configuration.RecordTelemetry);
1920
_idealClient = new IdealClient(apiClient, configuration);
2021
_klarnaClient = new KlarnaClient(apiClient, configuration);
2122
_sepaClient = new SepaClient(apiClient, configuration);

src/CheckoutSdk/Previous/CheckoutApi.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public CheckoutApi(CheckoutConfiguration configuration) : base(configuration)
3333
var apiClient = new ApiClient(configuration.HttpClientFactory,
3434
configuration.EnvironmentSubdomain != null
3535
? configuration.EnvironmentSubdomain.ApiUri
36-
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri);
36+
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri,
37+
configuration.RecordTelemetry);
3738
_tokensClient = new TokensClient(apiClient, configuration);
3839
_customersClient = new CustomersClient(apiClient, configuration);
3940
_sourcesClient = new SourcesClient(apiClient, configuration);

src/CheckoutSdk/RequestMetrics.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Checkout
2+
{
3+
public struct RequestMetrics
4+
{
5+
public string PrevRequestId { get; set; }
6+
public string RequestId { get; set; }
7+
public long PrevRequestDuration { get; set; }
8+
}
9+
}

0 commit comments

Comments
 (0)