diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
index 971d3c7..5257232 100644
--- a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
@@ -153,10 +153,12 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke
_initCompletion.TrySetResult(true);
}
- if (_client.DataSourceStatusProvider.Status.State == DataSourceState.Off)
+ var dataSourceStatus = _client.DataSourceStatusProvider.Status;
+ if (dataSourceStatus.State == DataSourceState.Off)
{
- _statusProvider.SetStatus(ProviderStatus.Error, ProviderShutdownMessage);
- _initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
+ var message = DescribeLastError(dataSourceStatus, ProviderShutdownMessage);
+ _statusProvider.SetStatus(ProviderStatus.Error, message);
+ _initCompletion.TrySetException(new LaunchDarklyProviderInitException(message));
}
return _initCompletion.Task;
@@ -210,18 +212,29 @@ private void StatusChangeHandler(object sender, DataSourceStatus status)
// The "ProviderStatus.Error" state says it is unable to evaluate flags. We can always evaluate
// flags.
_statusProvider.SetStatus(ProviderStatus.Stale,
- status.LastError?.Message ?? "encountered an unknown error");
+ DescribeLastError(status, "encountered an unknown error"));
break;
case DataSourceState.Off:
default:
// The status is an error, and not fatal, because the LaunchDarkly client can continue to
// evaluate flags using the data it already has.
- _statusProvider.SetStatus(ProviderStatus.Error, ProviderShutdownMessage);
- _initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
+ var message = DescribeLastError(status, ProviderShutdownMessage);
+ _statusProvider.SetStatus(ProviderStatus.Error, message);
+ _initCompletion.TrySetException(new LaunchDarklyProviderInitException(message));
break;
}
}
+ private static string DescribeLastError(DataSourceStatus status, string fallback)
+ {
+ if (status.LastError is DataSourceStatus.ErrorInfo lastError)
+ {
+ return lastError.Message ?? lastError.ToString();
+ }
+
+ return fallback;
+ }
+
///
public override void Track(string trackingEventName, EvaluationContext evaluationContext = null, TrackingEventDetails trackingEventDetails = default)
{
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
index ec642dd..8df398c 100644
--- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
@@ -6,6 +6,7 @@
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Interfaces;
using Moq;
+using OpenFeature.Constant;
using OpenFeature.Model;
using Xunit;
using LaunchDarkly.Sdk.Server.Integrations;
@@ -273,6 +274,78 @@ public async Task ItEmitsConfigurationChangedEvents()
Assert.NotEqual(eventPayloadA?.FlagsChanged[0], eventPayloadB?.FlagsChanged[0]);
}
+ private static async Task EmitStatusAndReadEvent(DataSourceStatus status)
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(l => l.GetLogger())
+ .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
+ mockClient.Setup(l => l.Initialized).Returns(true);
+
+ var mockDataSourceStatus = new Mock();
+ mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
+ {
+ State = DataSourceState.Valid
+ });
+ mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object);
+
+ var mockFlagTracker = new Mock();
+ mockClient.Setup(l => l.FlagTracker).Returns(mockFlagTracker.Object);
+
+ var provider = new Provider(mockClient.Object);
+ await provider.InitializeAsync(EvaluationContext.Empty);
+
+ mockDataSourceStatus.Raise(e => e.StatusChanged += null, mockDataSourceStatus.Object, status);
+
+ return await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload;
+ }
+
+ [Fact(Timeout = 5000)]
+ public async Task ItIncludesTheLastErrorMessageInErrorEvents()
+ {
+ var errorEvent = await EmitStatusAndReadEvent(new DataSourceStatus
+ {
+ State = DataSourceState.Off,
+ LastError = DataSourceStatus.ErrorInfo.FromException(new System.Exception("the key was invalid"))
+ });
+ Assert.Equal(ProviderEventTypes.ProviderError, errorEvent?.Type);
+ Assert.Equal("the key was invalid", errorEvent?.Message);
+ }
+
+ [Fact(Timeout = 5000)]
+ public async Task ItDescribesHttpErrorsWithoutAMessageInErrorEvents()
+ {
+ var lastError = DataSourceStatus.ErrorInfo.FromHttpError(401);
+ var errorEvent = await EmitStatusAndReadEvent(new DataSourceStatus
+ {
+ State = DataSourceState.Off,
+ LastError = lastError
+ });
+ Assert.Equal(ProviderEventTypes.ProviderError, errorEvent?.Type);
+ Assert.Equal(lastError.ToString(), errorEvent?.Message);
+ Assert.Contains("401", errorEvent?.Message);
+ }
+
+ [Fact(Timeout = 5000)]
+ public async Task ItUsesADefaultMessageInErrorEventsWithoutALastError()
+ {
+ var errorEvent = await EmitStatusAndReadEvent(new DataSourceStatus { State = DataSourceState.Off });
+ Assert.Equal(ProviderEventTypes.ProviderError, errorEvent?.Type);
+ Assert.Equal("the provider has encountered a permanent error or been shutdown", errorEvent?.Message);
+ }
+
+ [Fact(Timeout = 5000)]
+ public async Task ItDescribesHttpErrorsWithoutAMessageInStaleEvents()
+ {
+ var lastError = DataSourceStatus.ErrorInfo.FromHttpError(503);
+ var staleEvent = await EmitStatusAndReadEvent(new DataSourceStatus
+ {
+ State = DataSourceState.Interrupted,
+ LastError = lastError
+ });
+ Assert.Equal(ProviderEventTypes.ProviderStale, staleEvent?.Type);
+ Assert.Equal(lastError.ToString(), staleEvent?.Message);
+ }
+
[Fact(Timeout = 5000)]
public void ItTracksCustomEvents()
{