Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
3 changes: 3 additions & 0 deletions OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs">
<Link>Event\WebRequestEventDispatcher35.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs">
<Link>Event\EventRetryConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\LogEvent.cs">
<Link>Event\LogEvent.cs</Link>
</Compile>
Expand Down
3 changes: 3 additions & 0 deletions OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs">
<Link>Event\WebRequestEventDispatcher35.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs">
<Link>Event\EventRetryConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\LogEvent.cs">
<Link>Event\LogEvent.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs" />
<Compile Include="..\OptimizelySDK\Event\Dispatcher\HttpClientEventDispatcher45.cs" />
<Compile Include="..\OptimizelySDK\Event\Dispatcher\IEventDispatcher.cs" />
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs" />
<Compile Include="..\OptimizelySDK\Event\LogEvent.cs" />
<Compile Include="..\OptimizelySDK\Event\ForwardingEventProcessor.cs" />
<Compile Include="..\OptimizelySDK\Exceptions\OptimizelyException.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@
<Compile Include="..\OptimizelySDK\Event\Dispatcher\WebRequestEventDispatcher35.cs">
<Link>Event\Dispatcher\WebRequestEventDispatcher35.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\EventRetryConfig.cs">
<Link>Event\EventRetryConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Event\Entity\ConversionEvent.cs">
<Link>Event\Entity\ConversionEvent.cs</Link>
</Compile>
Expand Down
214 changes: 214 additions & 0 deletions OptimizelySDK.Tests/EventTests/HttpClientEventDispatcher45Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* Copyright 2026, 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
*
* http://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.
*/

#if !NET35 && !NET40
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using OptimizelySDK.Event;
using OptimizelySDK.Event.Dispatcher;
using OptimizelySDK.Logger;

namespace OptimizelySDK.Tests.EventTests
{
[TestFixture]
public class HttpClientEventDispatcher45Test
{
[SetUp]
public void Setup()
{
_mockLogger = new Mock<ILogger>();
_mockLogger.Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
_requestTimestamps = new List<DateTime>();
}

private Mock<ILogger> _mockLogger;
private List<DateTime> _requestTimestamps;

[Test]
public void DispatchEvent_Success_SingleAttempt()
{
var handler = new MockHttpMessageHandler(HttpStatusCode.OK);
var httpClient = new HttpClient(handler);
var dispatcher = new HttpClientEventDispatcher45(httpClient)
{
Logger = _mockLogger.Object,
};
var logEvent = CreateLogEvent();

dispatcher.DispatchEvent(logEvent);
Thread.Sleep(500); // Wait for async dispatch

Assert.AreEqual(1, handler.RequestCount);
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Never);
}

[Test]
public void DispatchEvent_ServerError500_RetriesThreeTimes()
{
var handler = new MockHttpMessageHandler(HttpStatusCode.InternalServerError);
var httpClient = new HttpClient(handler);
var dispatcher = new HttpClientEventDispatcher45(httpClient)
{
Logger = _mockLogger.Object,
};
var logEvent = CreateLogEvent();

dispatcher.DispatchEvent(logEvent);
Thread.Sleep(1500);

Assert.AreEqual(3, handler.RequestCount);
_mockLogger.Verify(
l => l.Log(LogLevel.ERROR, It.Is<string>(s => s.Contains("3 attempt(s)"))),
Times.Once);
}

[Test]
public void DispatchEvent_ClientError400_NoRetry()
{
var handler = new MockHttpMessageHandler(HttpStatusCode.BadRequest);
var httpClient = new HttpClient(handler);
var dispatcher = new HttpClientEventDispatcher45(httpClient)
{
Logger = _mockLogger.Object,
};
var logEvent = CreateLogEvent();

dispatcher.DispatchEvent(logEvent);
Thread.Sleep(500);

Assert.AreEqual(1, handler.RequestCount);
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once);
}

[Test]
public void DispatchEvent_SucceedsOnSecondAttempt_StopsRetrying()
{
var handler = new MockHttpMessageHandler(new[]
{
HttpStatusCode.InternalServerError,
HttpStatusCode.OK,
});
var httpClient = new HttpClient(handler);
var dispatcher = new HttpClientEventDispatcher45(httpClient)
{
Logger = _mockLogger.Object,
};
var logEvent = CreateLogEvent();

dispatcher.DispatchEvent(logEvent);
Thread.Sleep(1000);

Assert.AreEqual(2, handler.RequestCount);
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Never);
}

[Test]
public void DispatchEvent_ExponentialBackoff_VerifyTiming()
{
var handler =
new MockHttpMessageHandler(HttpStatusCode.InternalServerError, _requestTimestamps);
var httpClient = new HttpClient(handler);
var dispatcher = new HttpClientEventDispatcher45(httpClient)
{
Logger = _mockLogger.Object,
};
var logEvent = CreateLogEvent();

dispatcher.DispatchEvent(logEvent);
Thread.Sleep(1500); // Wait for all retries

Assert.AreEqual(3, _requestTimestamps.Count);

// First retry after ~200ms
var firstDelay = (_requestTimestamps[1] - _requestTimestamps[0]).TotalMilliseconds;
Assert.That(firstDelay, Is.GreaterThanOrEqualTo(180).And.LessThan(350),
$"First retry delay was {firstDelay}ms, expected ~200ms");

// Second retry after ~400ms
var secondDelay = (_requestTimestamps[2] - _requestTimestamps[1]).TotalMilliseconds;
Assert.That(secondDelay, Is.GreaterThanOrEqualTo(380).And.LessThan(550),
$"Second retry delay was {secondDelay}ms, expected ~400ms");
}

private static LogEvent CreateLogEvent()
{
return new LogEvent(
"https://logx.optimizely.com/v1/events",
new Dictionary<string, object>
{
{ "accountId", "12345" },
{ "visitors", new object[] { } },
},
"POST",
new Dictionary<string, string>());
}

/// <summary>
/// Mock HTTP message handler for testing.
/// </summary>
private class MockHttpMessageHandler : HttpMessageHandler
{
private readonly HttpStatusCode[] _statusCodes;
private readonly List<DateTime> _timestamps;
private int _currentIndex;

public MockHttpMessageHandler(HttpStatusCode statusCode,
List<DateTime> timestamps = null
)
: this(new[] { statusCode }, timestamps) { }

public MockHttpMessageHandler(HttpStatusCode[] statusCodes,
List<DateTime> timestamps = null
)
{
_statusCodes = statusCodes;
_timestamps = timestamps;
_currentIndex = 0;
}

public int RequestCount { get; private set; }

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
)
{
RequestCount++;
_timestamps?.Add(DateTime.Now);

var statusCode = _currentIndex < _statusCodes.Length ?
_statusCodes[_currentIndex] :
_statusCodes[_statusCodes.Length - 1];

_currentIndex++;

var response = new HttpResponseMessage(statusCode)
{
Content = new StringContent("{}"),
};

return Task.FromResult(response);
}
}
}
}
#endif
5 changes: 4 additions & 1 deletion OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ public void ShouldDispatchEventsWithCorrectPayload()
[Test]
public void ShouldRetryFailedEvents()
{
// With exponential backoff: 4 events × 3 attempts each × (200ms + 400ms) delays = ~2400ms+
// Need longer timeout to account for backoff delays
const int RETRY_TEST_TIMEOUT_MS = 5000;
var cde = new CountdownEvent(12);
_mockApiManager.Setup(a =>
a.SendEvents(It.IsAny<string>(), It.IsAny<string>(),
Expand All @@ -566,7 +569,7 @@ public void ShouldRetryFailedEvents()
eventManager.SendEvent(MakeEvent(i));
}

cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS);
cde.Wait(RETRY_TEST_TIMEOUT_MS);

// retry 3x (default) 4 events (batches of 1) = 12 calls to attempt to process
_mockApiManager.Verify(
Expand Down
1 change: 1 addition & 0 deletions OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
<Compile Include="EventTests\DefaultEventDispatcherTest.cs"/>
<Compile Include="EventTests\EventBuilderTest.cs"/>
<Compile Include="EventTests\ForwardingEventProcessorTest.cs"/>
<Compile Include="EventTests\HttpClientEventDispatcher45Test.cs"/>
<Compile Include="EventTests\LogEventTest.cs"/>
<Compile Include="EventTests\TestEventDispatcher.cs"/>
<Compile Include="EventTests\TestForwardingEventDispatcher.cs"/>
Expand Down
Loading
Loading