diff --git a/Analytics-CSharp/Segment/Analytics/Configuration.cs b/Analytics-CSharp/Segment/Analytics/Configuration.cs
index 79de436..c2aa395 100644
--- a/Analytics-CSharp/Segment/Analytics/Configuration.cs
+++ b/Analytics-CSharp/Segment/Analytics/Configuration.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Segment.Analytics.Policies;
+using Segment.Analytics.Retry;
using Segment.Analytics.Utilities;
using Segment.Concurrent;
using Segment.Serialization;
@@ -47,6 +48,21 @@ private set
public IEventPipelineProvider EventPipelineProvider { get; }
+ ///
+ /// HTTP retry configuration for rate limiting and exponential backoff. Defaults to
+ /// null. Set it before constructing Analytics, e.g.
+ /// new Configuration("writeKey") { HttpConfig = new HttpConfig(...) }.
+ /// Mirrors analytics-kotlin's mutable Configuration.httpConfig.
+ ///
+ /// This sets the pipeline's starting configuration only. CDN settings take precedence:
+ /// any settings payload carrying an httpConfig key replaces this value, and a CDN
+ /// payload is treated as enabling a subsystem unless it says "enabled": "false".
+ /// A payload with no httpConfig key leaves this value in effect. This matches the
+ /// behaviour of analytics-kotlin and analytics-swift.
+ ///
+ ///
+ public HttpConfig HttpConfig { get; set; }
+
///
/// Configuration that analytics can use
///
diff --git a/Analytics-CSharp/Segment/Analytics/Retry/RetryConfig.cs b/Analytics-CSharp/Segment/Analytics/Retry/RetryConfig.cs
index 5be1331..3f83581 100644
--- a/Analytics-CSharp/Segment/Analytics/Retry/RetryConfig.cs
+++ b/Analytics-CSharp/Segment/Analytics/Retry/RetryConfig.cs
@@ -3,7 +3,7 @@
namespace Segment.Analytics.Retry
{
- internal class RateLimitConfig
+ public class RateLimitConfig
{
public bool Enabled { get; }
public int MaxRetryCount { get; }
@@ -23,7 +23,7 @@ public RateLimitConfig(bool enabled = false, int maxRetryCount = 100, int maxRet
);
}
- internal class BackoffConfig
+ public class BackoffConfig
{
public bool Enabled { get; }
public int MaxRetryCount { get; }
@@ -57,7 +57,10 @@ public BackoffConfig(
Default4xxBehavior = default4xxBehavior;
Default5xxBehavior = default5xxBehavior;
UnknownCodeBehavior = unknownCodeBehavior;
- StatusCodeOverrides = statusCodeOverrides ?? DefaultStatusCodeOverrides;
+ // Copy: the property is public, and sharing the static default would let one
+ // caller's mutation corrupt every BackoffConfig built afterwards in the process.
+ StatusCodeOverrides = new Dictionary(
+ statusCodeOverrides ?? DefaultStatusCodeOverrides);
}
public BackoffConfig Validated() => new BackoffConfig(
@@ -109,7 +112,7 @@ public RetryConfig(RateLimitConfig rateLimitConfig = null, BackoffConfig backoff
}
}
- internal class HttpConfig
+ public class HttpConfig
{
public RateLimitConfig RateLimitConfig { get; }
public BackoffConfig BackoffConfig { get; }
diff --git a/Analytics-CSharp/Segment/Analytics/Retry/RetryTypes.cs b/Analytics-CSharp/Segment/Analytics/Retry/RetryTypes.cs
index 7a87348..5884cb3 100644
--- a/Analytics-CSharp/Segment/Analytics/Retry/RetryTypes.cs
+++ b/Analytics-CSharp/Segment/Analytics/Retry/RetryTypes.cs
@@ -6,7 +6,7 @@ internal enum PipelineState
RateLimited
}
- internal enum RetryBehavior
+ public enum RetryBehavior
{
Retry,
Drop
diff --git a/Analytics-CSharp/Segment/Analytics/Utilities/EventPipeline.cs b/Analytics-CSharp/Segment/Analytics/Utilities/EventPipeline.cs
index 3056e8a..98c14bd 100644
--- a/Analytics-CSharp/Segment/Analytics/Utilities/EventPipeline.cs
+++ b/Analytics-CSharp/Segment/Analytics/Utilities/EventPipeline.cs
@@ -48,7 +48,7 @@ public EventPipeline(
string apiHost = HTTPClient.DefaultAPIHost)
: this(analytics, logTag, apiKey, flushPolicies, apiHost, (HttpConfig)null) { }
- internal EventPipeline(
+ public EventPipeline(
Analytics analytics,
string logTag,
string apiKey,
@@ -69,7 +69,9 @@ internal EventPipeline(
Running = false;
var retryConfig = httpConfig != null
- ? new RetryConfig(httpConfig.RateLimitConfig, httpConfig.BackoffConfig)
+ // Validated(): user-supplied config reaches us unclamped, unlike the
+ // CDN path which HttpConfigParser already validates.
+ ? new RetryConfig(httpConfig.RateLimitConfig.Validated(), httpConfig.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
_retryState = RetryStateStorage.LoadRetryState(_storage);
@@ -78,7 +80,7 @@ internal EventPipeline(
internal void UpdateHttpConfig(HttpConfig config)
{
var retryConfig = config != null
- ? new RetryConfig(config.RateLimitConfig, config.BackoffConfig)
+ ? new RetryConfig(config.RateLimitConfig.Validated(), config.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
}
diff --git a/Analytics-CSharp/Segment/Analytics/Utilities/EventPipelineProvider.cs b/Analytics-CSharp/Segment/Analytics/Utilities/EventPipelineProvider.cs
index abd376c..137780c 100644
--- a/Analytics-CSharp/Segment/Analytics/Utilities/EventPipelineProvider.cs
+++ b/Analytics-CSharp/Segment/Analytics/Utilities/EventPipelineProvider.cs
@@ -11,7 +11,8 @@ public IEventPipeline Create(Analytics analytics, string key)
return new EventPipeline(analytics, key,
analytics.Configuration.WriteKey,
analytics.Configuration.FlushPolicies,
- analytics.Configuration.ApiHost);
+ analytics.Configuration.ApiHost,
+ analytics.Configuration.HttpConfig);
}
}
}
\ No newline at end of file
diff --git a/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipeline.cs b/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipeline.cs
index 4657be9..9c283f3 100644
--- a/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipeline.cs
+++ b/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipeline.cs
@@ -61,7 +61,7 @@ public SyncEventPipeline(
CancellationToken? flushCancellationToken = null)
: this(analytics, logTag, apiKey, flushPolicies, apiHost, flushTimeout, flushCancellationToken, null) { }
- internal SyncEventPipeline(
+ public SyncEventPipeline(
Analytics analytics,
string logTag,
string apiKey,
@@ -86,7 +86,9 @@ internal SyncEventPipeline(
_flushCancellationToken = flushCancellationToken ?? CancellationToken.None;
var retryConfig = httpConfig != null
- ? new RetryConfig(httpConfig.RateLimitConfig, httpConfig.BackoffConfig)
+ // Validated(): user-supplied config reaches us unclamped, unlike the
+ // CDN path which HttpConfigParser already validates.
+ ? new RetryConfig(httpConfig.RateLimitConfig.Validated(), httpConfig.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
_retryState = RetryStateStorage.LoadRetryState(_storage);
@@ -95,7 +97,7 @@ internal SyncEventPipeline(
internal void UpdateHttpConfig(HttpConfig config)
{
var retryConfig = config != null
- ? new RetryConfig(config.RateLimitConfig, config.BackoffConfig)
+ ? new RetryConfig(config.RateLimitConfig.Validated(), config.BackoffConfig.Validated())
: new RetryConfig();
_retryStateMachine = new RetryStateMachine(retryConfig);
}
diff --git a/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipelineProvider.cs b/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipelineProvider.cs
index 5794677..931a10b 100644
--- a/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipelineProvider.cs
+++ b/Analytics-CSharp/Segment/Analytics/Utilities/SyncEventPipelineProvider.cs
@@ -22,7 +22,8 @@ public IEventPipeline Create(Analytics analytics, string key)
analytics.Configuration.FlushPolicies,
analytics.Configuration.ApiHost,
_flushTimeout,
- _flushCancellationToken);
+ _flushCancellationToken,
+ analytics.Configuration.HttpConfig);
}
}
}
\ No newline at end of file
diff --git a/Tests/Retry/ConfigurationHttpConfigTest.cs b/Tests/Retry/ConfigurationHttpConfigTest.cs
new file mode 100644
index 0000000..6e15dc5
--- /dev/null
+++ b/Tests/Retry/ConfigurationHttpConfigTest.cs
@@ -0,0 +1,130 @@
+using Moq;
+using Segment.Analytics;
+using Segment.Analytics.Retry;
+using Segment.Analytics.Utilities;
+using Segment.Serialization;
+using Tests.Utils;
+using Xunit;
+
+namespace Tests.Retry
+{
+ ///
+ /// Configuration.HttpConfig is the user-facing entry point for retry settings,
+ /// mirroring Kotlin's Configuration.httpConfig and Swift's .httpConfig(_:).
+ /// These cover that a config supplied there actually reaches the pipeline's
+ /// retry state machine; CDN settings still override it later via UpdateHttpConfig.
+ ///
+ public class ConfigurationHttpConfigTest
+ {
+ private static Analytics CreateAnalytics(HttpConfig httpConfig)
+ {
+ Settings? settings = JsonUtility.FromJson(
+ "{\"integrations\":{\"Segment.io\":{\"apiKey\":\"k\"}},\"plan\":{},\"edgeFunction\":{}}");
+
+ var mockHttpClient = new Mock(null, null, null);
+ mockHttpClient.Setup(c => c.Settings()).ReturnsAsync(settings);
+
+ var config = new Configuration(
+ writeKey: "123",
+ autoAddSegmentDestination: false,
+ useSynchronizeDispatcher: true,
+ flushInterval: 0,
+ flushAt: 2,
+ httpClientProvider: new MockHttpClientProvider(mockHttpClient),
+ storageProvider: new MockStorageProvider(new Mock())
+ )
+ {
+ HttpConfig = httpConfig
+ };
+ return new Analytics(config);
+ }
+
+ [Fact]
+ public void Configuration_ExposesHttpConfig()
+ {
+ var httpConfig = new HttpConfig(backoffConfig: new BackoffConfig(enabled: true, maxRetryCount: 7));
+ Analytics analytics = CreateAnalytics(httpConfig);
+
+ Assert.Same(httpConfig, analytics.Configuration.HttpConfig);
+ }
+
+ [Fact]
+ public void Configuration_HttpConfigDefaultsToNull()
+ {
+ Analytics analytics = CreateAnalytics(null);
+
+ Assert.Null(analytics.Configuration.HttpConfig);
+ }
+
+ [Fact]
+ public void EventPipeline_WithoutHttpConfig_IsLegacyMode()
+ {
+ Analytics analytics = CreateAnalytics(null);
+
+ var pipeline = (EventPipeline)new EventPipelineProvider().Create(analytics, "key");
+
+ Assert.True(pipeline._retryStateMachine.IsLegacyMode);
+ }
+
+ [Fact]
+ public void EventPipeline_WithHttpConfig_LeavesLegacyMode()
+ {
+ Analytics analytics = CreateAnalytics(
+ new HttpConfig(backoffConfig: new BackoffConfig(enabled: true)));
+
+ var pipeline = (EventPipeline)new EventPipelineProvider().Create(analytics, "key");
+
+ Assert.False(pipeline._retryStateMachine.IsLegacyMode);
+ }
+
+ [Fact]
+ public void SyncEventPipeline_WithoutHttpConfig_IsLegacyMode()
+ {
+ Analytics analytics = CreateAnalytics(null);
+
+ var pipeline = (SyncEventPipeline)new SyncEventPipelineProvider().Create(analytics, "key");
+
+ Assert.True(pipeline._retryStateMachine.IsLegacyMode);
+ }
+
+ [Fact]
+ public void BackoffConfig_DoesNotShareTheDefaultOverrideMap()
+ {
+ var first = new BackoffConfig(enabled: true);
+ first.StatusCodeOverrides[500] = RetryBehavior.Drop;
+
+ var second = new BackoffConfig(enabled: true);
+
+ Assert.False(second.StatusCodeOverrides.ContainsKey(500));
+ Assert.NotSame(first.StatusCodeOverrides, second.StatusCodeOverrides);
+ }
+
+ [Fact]
+ public void UserSuppliedHttpConfig_IsValidatedOnTheWayIn()
+ {
+ // maxRetryInterval: 0 is out of range and must clamp to 1 second, exactly as the
+ // CDN path does via HttpConfigParser. Unvalidated it would schedule the retry at
+ // currentTime, i.e. no wait at all.
+ Analytics analytics = CreateAnalytics(
+ new HttpConfig(rateLimitConfig: new RateLimitConfig(enabled: true, maxRetryInterval: 0)));
+
+ var pipeline = (EventPipeline)new EventPipelineProvider().Create(analytics, "key");
+ RetryState state = pipeline._retryStateMachine.HandleResponse(
+ new RetryState(),
+ new ResponseInfo(429, retryAfterSeconds: null, batchFile: "b.json", currentTime: 1000));
+
+ Assert.Equal(2000, state.WaitUntilTime);
+ }
+
+ [Fact]
+ public void SyncEventPipeline_WithHttpConfig_LeavesLegacyMode()
+ {
+ Analytics analytics = CreateAnalytics(
+ new HttpConfig(rateLimitConfig: new RateLimitConfig(enabled: true)));
+
+ var pipeline = (SyncEventPipeline)new SyncEventPipelineProvider().Create(analytics, "key");
+
+ Assert.False(pipeline._retryStateMachine.IsLegacyMode);
+ }
+ }
+}