launchdarkly-server-sdk 4.5.1 → 4.6.0
raw patch · 6 files changed
+108/−26 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +7/−0
- launchdarkly-server-sdk.cabal +2/−1
- src/LaunchDarkly/Server/Client.hs +2/−23
- src/LaunchDarkly/Server/Client/Internal.hs +38/−2
- test/Spec.hs +2/−0
- test/Spec/HttpConfiguration.hs +57/−0
CHANGELOG.md view
@@ -2,6 +2,13 @@ All notable changes to the LaunchDarkly Haskell Server-side SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [4.6.0](https://github.com/launchdarkly/haskell-server-sdk/compare/4.5.1...4.6.0) (2026-05-28)+++### Features++* add X-LaunchDarkly-Instance-Id header (SDK-2355) ([#111](https://github.com/launchdarkly/haskell-server-sdk/issues/111)) ([758c304](https://github.com/launchdarkly/haskell-server-sdk/commit/758c3046d1e505782392ee3c017e1e4dc6a4ff04))+ ## [4.5.1](https://github.com/launchdarkly/haskell-server-sdk/compare/4.5.0...4.5.1) (2025-12-16)
launchdarkly-server-sdk.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: launchdarkly-server-sdk-version: 4.5.1+version: 4.6.0 synopsis: Server-side SDK for integrating with LaunchDarkly description: Please see the README on GitHub at <https://github.com/launchdarkly/haskell-server-sdk#readme> category: Web@@ -140,6 +140,7 @@ Spec.DataSource Spec.Evaluate Spec.Features+ Spec.HttpConfiguration Spec.Integrations.FileData Spec.Integrations.TestData Spec.Operators
src/LaunchDarkly/Server/Client.hs view
@@ -44,16 +44,14 @@ import GHC.Generics (Generic) import GHC.Natural (Natural) import Network.HTTP.Client (newManager)-import qualified Network.HTTP.Client as Http import Network.HTTP.Client.TLS (tlsManagerSettings) import System.Clock (TimeSpec (..)) import LaunchDarkly.AesonCompat (KeyMap, emptyObject, filterObject, insertKey, mapValues)-import LaunchDarkly.Server.Client.Internal (Client (..), clientVersion, getStatusI)+import LaunchDarkly.Server.Client.Internal (Client (..), clientVersion, getStatusI, makeHttpConfiguration) import LaunchDarkly.Server.Client.Status (Status (..)) import LaunchDarkly.Server.Config.ClientContext (ClientContext (..))-import LaunchDarkly.Server.Config.HttpConfiguration (HttpConfiguration (..))-import LaunchDarkly.Server.Config.Internal (ApplicationInfo, Config, getApplicationInfoHeader, shouldSendEvents)+import LaunchDarkly.Server.Config.Internal (Config, shouldSendEvents) import LaunchDarkly.Server.Context (getValue) import LaunchDarkly.Server.Context.Internal (Context (Invalid), getCanonicalKey, getKey, optionallyRedactAnonymous, redactContext, redactContextRedactAnonymous) import LaunchDarkly.Server.DataSource.Internal (DataSource (..), DataSourceFactory, DataSourceUpdates (..), defaultDataSourceUpdates, nullDataSourceFactory)@@ -69,9 +67,7 @@ import Crypto.Hash.SHA256 (hash) import Crypto.MAC.HMAC (hmac) import Data.ByteArray.Encoding (Base (Base16), convertToBase)-import Data.ByteString (ByteString) import Data.Text.Encoding (decodeUtf8)-import Network.HTTP.Types (HeaderName) networkDataSourceFactory :: (ClientContext -> DataSourceUpdates -> LoggingT IO ()) -> DataSourceFactory networkDataSourceFactory threadF clientContext dataSourceUpdates = do@@ -92,23 +88,6 @@ liftIO $ void $ takeMVar sync pure $ DataSource {..}--makeHttpConfiguration :: Config -> IO HttpConfiguration-makeHttpConfiguration config = do- tlsManager <- newManager tlsManagerSettings- let headers =- [ ("Authorization", encodeUtf8 $ getField @"key" config)- , ("User-Agent", "HaskellServerClient/" <> encodeUtf8 clientVersion)- ]- defaultRequestHeaders = addTagsHeader headers (getField @"applicationInfo" config)- defaultRequestTimeout = Http.responseTimeoutMicro $ fromIntegral $ getField @"requestTimeoutSeconds" config * 1000000- pure $ HttpConfiguration {..}- where- addTagsHeader :: [(HeaderName, ByteString)] -> Maybe ApplicationInfo -> [(HeaderName, ByteString)]- addTagsHeader headers Nothing = headers- addTagsHeader headers (Just info) = case getApplicationInfoHeader info of- Nothing -> headers- Just header -> ("X-LaunchDarkly-Tags", encodeUtf8 header) : headers makeClientContext :: Config -> IO ClientContext makeClientContext config = do
src/LaunchDarkly/Server/Client/Internal.hs view
@@ -2,26 +2,36 @@ ( Client (..) , Status (..) , clientVersion+ , makeHttpConfiguration , setStatus , getStatusI ) where import Control.Concurrent (ThreadId) import Control.Concurrent.MVar (MVar)+import Data.ByteString (ByteString) import Data.Generics.Product (getField) import Data.IORef (IORef, atomicModifyIORef', readIORef) import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import qualified Data.UUID as UUID+import qualified Data.UUID.V4 as UUIDv4 import GHC.Generics (Generic)+import Network.HTTP.Client (newManager)+import qualified Network.HTTP.Client as Http+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types (HeaderName) import LaunchDarkly.Server.Client.Status (Status (..), transitionStatus)-import LaunchDarkly.Server.Config.Internal (Config)+import LaunchDarkly.Server.Config.HttpConfiguration (HttpConfiguration (..))+import LaunchDarkly.Server.Config.Internal (ApplicationInfo, Config, getApplicationInfoHeader) import LaunchDarkly.Server.DataSource.Internal (DataSource) import LaunchDarkly.Server.Events (EventState) import LaunchDarkly.Server.Store.Internal (StoreHandle, getInitializedC) -- | The version string for this library. clientVersion :: Text-clientVersion = "4.5.1" -- x-release-please-version+clientVersion = "4.6.0" -- x-release-please-version -- | -- Client is the LaunchDarkly client. Client instances are thread-safe.@@ -50,3 +60,29 @@ getInitializedC (getField @"store" client) >>= \case Right True -> pure Initialized _ -> pure Uninitialized++-- |+-- Build an 'HttpConfiguration' for the given 'Config'. A fresh TLS manager is created and a new+-- per-instance GUID v4 is generated for the 'X-LaunchDarkly-Instance-Id' header. Because the+-- returned 'defaultRequestHeaders' is shared by the polling, streaming, and event clients, every+-- outbound request carries the same stable per-instance identifier without per-channel plumbing.+--+-- The SDK key, version banner, and (optional) application tags header are also attached here.+makeHttpConfiguration :: Config -> IO HttpConfiguration+makeHttpConfiguration config = do+ tlsManager <- newManager tlsManagerSettings+ instanceId <- UUIDv4.nextRandom+ let baseHeaders =+ [ ("Authorization", encodeUtf8 $ getField @"key" config)+ , ("User-Agent", "HaskellServerClient/" <> encodeUtf8 clientVersion)+ , ("X-LaunchDarkly-Instance-Id", encodeUtf8 $ UUID.toText instanceId)+ ]+ defaultRequestHeaders = addTagsHeader baseHeaders (getField @"applicationInfo" config)+ defaultRequestTimeout = Http.responseTimeoutMicro $ fromIntegral $ getField @"requestTimeoutSeconds" config * 1000000+ pure $ HttpConfiguration {..}+ where+ addTagsHeader :: [(HeaderName, ByteString)] -> Maybe ApplicationInfo -> [(HeaderName, ByteString)]+ addTagsHeader headers Nothing = headers+ addTagsHeader headers (Just info) = case getApplicationInfoHeader info of+ Nothing -> headers+ Just header -> ("X-LaunchDarkly-Tags", encodeUtf8 header) : headers
test/Spec.hs view
@@ -10,6 +10,7 @@ import qualified Spec.DataSource import qualified Spec.Evaluate import qualified Spec.Features+import qualified Spec.HttpConfiguration import qualified Spec.Integrations.FileData import qualified Spec.Integrations.TestData import qualified Spec.Operators@@ -32,6 +33,7 @@ , Spec.DataSource.allTests , Spec.Evaluate.allTests , Spec.Features.allTests+ , Spec.HttpConfiguration.allTests , Spec.Operators.allTests , Spec.Reference.allTests , Spec.Segment.allTests
+ test/Spec/HttpConfiguration.hs view
@@ -0,0 +1,57 @@+module Spec.HttpConfiguration (allTests) where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BSC+import Data.Maybe (isJust)+import qualified Data.Text.Encoding as TE+import qualified Data.UUID as UUID+import Network.HTTP.Types (HeaderName)+import Test.HUnit++import LaunchDarkly.Server.Client.Internal (makeHttpConfiguration)+import LaunchDarkly.Server.Config (makeConfig)+import LaunchDarkly.Server.Config.HttpConfiguration (HttpConfiguration (..))++instanceIdHeaderName :: HeaderName+instanceIdHeaderName = "X-LaunchDarkly-Instance-Id"++parseUUID :: BS.ByteString -> Maybe UUID.UUID+parseUUID = UUID.fromText . TE.decodeUtf8++testMakeHttpConfigurationAttachesInstanceIdHeader :: Test+testMakeHttpConfigurationAttachesInstanceIdHeader = TestCase $ do+ httpConfig <- makeHttpConfiguration (makeConfig "sdk-key")+ let headers = defaultRequestHeaders httpConfig+ case lookup instanceIdHeaderName headers of+ Nothing -> assertFailure "X-LaunchDarkly-Instance-Id header missing from defaultRequestHeaders"+ Just value -> case parseUUID value of+ Nothing -> assertFailure $ "instance id is not a parseable UUID: " <> BSC.unpack value+ Just uuid ->+ let (_, w2, _, _) = UUID.toWords uuid+ versionNibble = (w2 `div` 0x1000) `mod` 0x10+ in assertEqual ("instance id " <> show uuid <> " must be UUID v4") 4 versionNibble++testMakeHttpConfigurationSetsExactlyOneInstanceIdHeader :: Test+testMakeHttpConfigurationSetsExactlyOneInstanceIdHeader = TestCase $ do+ httpConfig <- makeHttpConfiguration (makeConfig "sdk-key")+ let headers = defaultRequestHeaders httpConfig+ matches = filter ((== instanceIdHeaderName) . fst) headers+ assertEqual "exactly one X-LaunchDarkly-Instance-Id header should be set" 1 (length matches)++testMakeHttpConfigurationGeneratesDistinctInstanceIds :: Test+testMakeHttpConfigurationGeneratesDistinctInstanceIds = TestCase $ do+ h1 <- makeHttpConfiguration (makeConfig "sdk-key")+ h2 <- makeHttpConfiguration (makeConfig "sdk-key")+ let id1 = lookup instanceIdHeaderName (defaultRequestHeaders h1)+ id2 = lookup instanceIdHeaderName (defaultRequestHeaders h2)+ assertBool "first instance id present" (isJust id1)+ assertBool "second instance id present" (isJust id2)+ assertBool "each SDK instance should generate its own instance id" (id1 /= id2)++allTests :: Test+allTests =+ TestList+ [ testMakeHttpConfigurationAttachesInstanceIdHeader+ , testMakeHttpConfigurationSetsExactlyOneInstanceIdHeader+ , testMakeHttpConfigurationGeneratesDistinctInstanceIds+ ]