diff --git a/Flagsmith.Client.Test/AnalyticsProcessorTest.cs b/Flagsmith.Client.Test/AnalyticsProcessorTest.cs
index fd4c23d..577b78a 100644
--- a/Flagsmith.Client.Test/AnalyticsProcessorTest.cs
+++ b/Flagsmith.Client.Test/AnalyticsProcessorTest.cs
@@ -13,8 +13,8 @@ internal class AnalyticsProcessorTest : AnalyticsProcessor
///
/// This class provides some extra functionality to help particularly in unit testing.
///
- public AnalyticsProcessorTest(HttpClient httpClient, string environmentKey, string baseApiUrl, int timeOut = 3)
- : base(httpClient, environmentKey, baseApiUrl, timeOut: timeOut)
+ public AnalyticsProcessorTest(HttpClient httpClient, string environmentKey, Uri analyticsUri, int timeOut = 3)
+ : base(httpClient, environmentKey, analyticsUri, timeOut: timeOut)
{
}
///
diff --git a/Flagsmith.Client.Test/AnalyticsTests.cs b/Flagsmith.Client.Test/AnalyticsTests.cs
index 2c4ebb8..e5b608d 100644
--- a/Flagsmith.Client.Test/AnalyticsTests.cs
+++ b/Flagsmith.Client.Test/AnalyticsTests.cs
@@ -14,7 +14,8 @@ namespace Flagsmith.FlagsmithClientTest
{
public class AnalyticsTests
{
- const string _defaultApiUrl = "https://edge.api.flagsmith.com/api/v1/";
+ static readonly Uri _defaultApiUri = new Uri("https://edge.api.flagsmith.com/api/v1/");
+ static readonly Uri _defaultAnalyticsUri = new Uri(_defaultApiUri, "analytics/flags/");
[Fact]
public async Task TestAnalyticsProcessorDoesNotThrowUnderLoad()
@@ -27,7 +28,7 @@ public async Task TestAnalyticsProcessorDoesNotThrowUnderLoad()
HttpMocker.PayloadsSubmitted = new System.Collections.Concurrent.ConcurrentBag();
var mockHttpClient = HttpMocker.MockHttpResponse(System.Net.HttpStatusCode.OK, null, true);
- var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, null);
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, _defaultAnalyticsUri);
var token = new CancellationToken();
await Parallel.ForEachAsync(Enumerable.Range(1, numberOfThreads), token, async (item, token) =>
{
@@ -69,7 +70,7 @@ public async Task TestAnalyticsProcessorFlushClearsAnalyticsData()
{
StatusCode = System.Net.HttpStatusCode.OK,
});
- var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, null);
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, _defaultAnalyticsUri);
await analyticsProcessor.TrackFeature("TestAnalyticsProcessorFlushClearsAnalyticsDataFeature");
await analyticsProcessor.Flush();
Assert.False(analyticsProcessor.HasTrackingItemsInCache());
@@ -81,7 +82,7 @@ public async void TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsData(
{
StatusCode = System.Net.HttpStatusCode.OK,
});
- var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, baseApiUrl: _defaultApiUrl);
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, analyticsUri: _defaultAnalyticsUri);
await analyticsProcessor.TrackFeature("TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsDataFeature1");
await analyticsProcessor.TrackFeature("TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsDataFeature2");
var jObject = JObject.Parse(analyticsProcessor.ToString());
@@ -91,13 +92,77 @@ public async void TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsData(
Assert.Equal(1, jObject["TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsDataFeature2"].Value());
}
[Fact]
+ public async Task TestAnalyticsProcessorPostsToConfiguredAnalyticsUri()
+ {
+ var mockHttpClient = HttpMocker.MockHttpResponse(new HttpResponseMessage
+ {
+ StatusCode = System.Net.HttpStatusCode.OK,
+ });
+ var analyticsUri = new Uri("https://analytics.example.com/api/v1/analytics/flags/");
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, Fixtures.ApiKey, analyticsUri);
+ await analyticsProcessor.TrackFeature("some_feature");
+ await analyticsProcessor.Flush();
+ mockHttpClient.Verify(x => x.SendAsync(
+ It.Is(req => req.Method == HttpMethod.Post && req.RequestUri == analyticsUri),
+ It.IsAny()), Times.Once);
+ }
+ [Fact]
+ public async Task TestAnalyticsProcessorBaseApiUrlConstructorPostsToAnalyticsEndpoint()
+ {
+ var mockHttpClient = HttpMocker.MockHttpResponse(new HttpResponseMessage
+ {
+ StatusCode = System.Net.HttpStatusCode.OK,
+ });
+ var analyticsProcessor = new AnalyticsProcessor(mockHttpClient.Object, Fixtures.ApiKey, _defaultApiUri.ToString());
+ await analyticsProcessor.TrackFeature("some_feature");
+ await analyticsProcessor.Flush();
+ VerifyAnalyticsPostedTo(mockHttpClient, _defaultAnalyticsUri);
+ }
+ [Fact]
+ public async Task TestFlagsmithClientPostsAnalyticsToExpectedUri()
+ {
+ var customAnalyticsUri = new Uri("https://analytics.example.com/api/v1/analytics/flags/");
+ // A client using the default analytics endpoint, relative to the API URI...
+ var (defaultUriMock, defaultUriFlags) = await GetClientFlags(analyticsUri: null);
+ // ...and a client with an analytics endpoint configured separately from the API URI
+ var (customUriMock, customUriFlags) = await GetClientFlags(customAnalyticsUri);
+ await Task.Delay(11 * 1000);
+ await defaultUriFlags.IsFeatureEnabled("some_feature");
+ await customUriFlags.IsFeatureEnabled("some_feature");
+ // Tracking a feature does not await the flush it triggers, so give the requests time to be made
+ await Task.Delay(2 * 1000);
+ VerifyAnalyticsPostedTo(defaultUriMock, _defaultAnalyticsUri);
+ VerifyAnalyticsPostedTo(customUriMock, customAnalyticsUri);
+ }
+ private static async Task<(Mock, IFlags)> GetClientFlags(Uri analyticsUri)
+ {
+ var mockHttpClient = HttpMocker.MockHttpResponse(new HttpResponseMessage
+ {
+ StatusCode = System.Net.HttpStatusCode.OK,
+ Content = new StringContent(Fixtures.ApiFlagResponse)
+ });
+ var client = new FlagsmithClient(new FlagsmithConfiguration
+ {
+ EnvironmentKey = Fixtures.ApiKey,
+ ApiUri = _defaultApiUri,
+ AnalyticsUri = analyticsUri,
+ HttpClient = mockHttpClient.Object,
+ EnableAnalytics = true
+ });
+ return (mockHttpClient, await client.GetEnvironmentFlags());
+ }
+ private static void VerifyAnalyticsPostedTo(Mock mockHttpClient, Uri expectedUri)
+ => mockHttpClient.Verify(x => x.SendAsync(
+ It.Is(req => req.Method == HttpMethod.Post && req.RequestUri == expectedUri),
+ It.IsAny()), Times.Once);
+ [Fact]
public async Task TestAnalyticsProcessorFlushEarlyExitIfAnalyticsDataIsEmpty()
{
var mockHttpClient = HttpMocker.MockHttpResponse(new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.OK,
});
- var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, baseApiUrl: _defaultApiUrl);
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, analyticsUri: _defaultAnalyticsUri);
await analyticsProcessor.Flush();
mockHttpClient.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Never);
}
@@ -108,7 +173,7 @@ public async Task TestAnalyticsProcessorCallingTrackFeatureCallsFlushWhenTimerRu
{
StatusCode = System.Net.HttpStatusCode.OK,
});
- var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, baseApiUrl: _defaultApiUrl);
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, analyticsUri: _defaultAnalyticsUri);
await Task.Delay(12 * 1000);
await analyticsProcessor.TrackFeature("TestAnalyticsProcessorCallingTrackFeatureCallsFlushWhenTimerRunsOutFeature");
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/analytics/flags/", Times.Once);
@@ -122,7 +187,7 @@ public async Task TestAnalyticsProcessorDataConcurrentAccess()
{
StatusCode = System.Net.HttpStatusCode.OK,
});
- var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, null);
+ var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, _defaultAnalyticsUri);
const int numberOfCalls = 100000;
// When
diff --git a/Flagsmith.Client.Test/Fixtures.cs b/Flagsmith.Client.Test/Fixtures.cs
index e7f4f05..f267f28 100644
--- a/Flagsmith.Client.Test/Fixtures.cs
+++ b/Flagsmith.Client.Test/Fixtures.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Net.Http;
using FlagsmithEngine.Environment.Models;
@@ -10,7 +11,8 @@ internal class Fixtures
{
public static string ApiKey => "ser.test_key";
public static string ApiUrl => "http://test_url/";
- public static AnalyticsProcessorTest GetAnalyticalProcessorTest() => new(new HttpClient(), ApiKey, ApiUrl);
+ public static Uri AnalyticsUri => new Uri(ApiUrl + "analytics/flags/");
+ public static AnalyticsProcessorTest GetAnalyticalProcessorTest() => new(new HttpClient(), ApiKey, AnalyticsUri);
public static JObject JsonObject = JObject.Parse(@"{
'api_key': 'test_key',
'name': 'Test Environment',
diff --git a/Flagsmith.Client.Test/UserAgentTest.cs b/Flagsmith.Client.Test/UserAgentTest.cs
index 14b2346..6cca703 100644
--- a/Flagsmith.Client.Test/UserAgentTest.cs
+++ b/Flagsmith.Client.Test/UserAgentTest.cs
@@ -113,7 +113,7 @@ public async Task TestUserAgentHeaderIsSentInAnalyticsFlush()
var analyticsProcessor = new AnalyticsProcessor(
httpClientMock.Object,
Fixtures.ApiKey,
- Fixtures.ApiUrl
+ Fixtures.AnalyticsUri
);
// When
diff --git a/Flagsmith.FlagsmithClient/AnalyticsProcessor.cs b/Flagsmith.FlagsmithClient/AnalyticsProcessor.cs
index 99f4201..b874c4f 100644
--- a/Flagsmith.FlagsmithClient/AnalyticsProcessor.cs
+++ b/Flagsmith.FlagsmithClient/AnalyticsProcessor.cs
@@ -18,7 +18,7 @@ public class AnalyticsProcessor : IAnalyticsProcessor
private int _FlushIntervalSeconds = 10;
private readonly DisposableLock _lock = new DisposableLock();
- private readonly string _AnalyticsEndPoint;
+ private readonly Uri _AnalyticsUri;
private readonly string _EnvironmentKey;
private readonly int _TimeOut;
private DateTime _LastFlushed;
@@ -28,9 +28,14 @@ public class AnalyticsProcessor : IAnalyticsProcessor
private ConcurrentDictionary> AnalyticsDataThreads;
public AnalyticsProcessor(HttpClient httpClient, string environmentKey, string baseApiUrl, ILogger logger = null, Dictionary customHeaders = null, int timeOut = 3, int flushIntervalSeconds = 10)
+ : this(httpClient, environmentKey, new Uri(baseApiUrl + "analytics/flags/", UriKind.RelativeOrAbsolute), logger, customHeaders, timeOut, flushIntervalSeconds)
+ {
+ }
+
+ public AnalyticsProcessor(HttpClient httpClient, string environmentKey, Uri analyticsUri, ILogger logger = null, Dictionary customHeaders = null, int timeOut = 3, int flushIntervalSeconds = 10)
{
_EnvironmentKey = environmentKey;
- _AnalyticsEndPoint = baseApiUrl + "analytics/flags/";
+ _AnalyticsUri = analyticsUri;
_TimeOut = timeOut;
_LastFlushed = DateTime.UtcNow;
_HttpClient = httpClient;
@@ -71,7 +76,7 @@ private async Task FlushWithoutLock()
try
{
var analyticsJson = JsonConvert.SerializeObject(GetAggregatedAnalyticsWithoutLock());
- var request = new HttpRequestMessage(HttpMethod.Post, _AnalyticsEndPoint)
+ var request = new HttpRequestMessage(HttpMethod.Post, _AnalyticsUri)
{
Headers =
{
diff --git a/Flagsmith.FlagsmithClient/FlagsmithClient.cs b/Flagsmith.FlagsmithClient/FlagsmithClient.cs
index 9ad6bc5..f819447 100644
--- a/Flagsmith.FlagsmithClient/FlagsmithClient.cs
+++ b/Flagsmith.FlagsmithClient/FlagsmithClient.cs
@@ -70,7 +70,7 @@ private void Initialise()
throw new Exception("ValueError: environmentKey is required");
}
if (_config.EnableAnalytics)
- _analyticsProcessor = new AnalyticsProcessor(_config.HttpClient, _config.EnvironmentKey, _config.ApiUri.ToString(), _config.Logger, _config.CustomHeaders);
+ _analyticsProcessor = new AnalyticsProcessor(_config.HttpClient, _config.EnvironmentKey, _config.AnalyticsUri ?? new Uri(_config.ApiUri, "analytics/flags/"), _config.Logger, _config.CustomHeaders);
if (_config.EnableLocalEvaluation)
{
diff --git a/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs b/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs
index 29ad442..66436ef 100644
--- a/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs
+++ b/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs
@@ -19,6 +19,14 @@ public class FlagsmithConfiguration
///
public Uri ApiUri { get; set; } = DefaultApiUri;
+ ///
+ /// URI of the Flagsmith analytics events API endpoint. Set this when analytics are not served by the same
+ /// API as flags, e.g. when using Edge Proxy. Defaults to the analytics/flags/ endpoint relative to
+ /// .
+ /// new Uri("https://flagsmith.example.com/api/v1/analytics/flags/")
+ ///
+ public Uri? AnalyticsUri { get; set; }
+
///
/// The environment key obtained from Flagsmith interface.
///