packages feed

launchdarkly-server-sdk 4.1.0 → 4.2.0

raw patch · 10 files changed

+98/−22 lines, 10 filessetup-changedPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ LaunchDarkly.Server.Config: configSetOmitAnonymousContexts :: Bool -> Config -> Config

Files

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.2.0](https://github.com/launchdarkly/haskell-server-sdk/compare/4.1.0...4.2.0) (2024-08-23)+++### Features++* Add option to omit anonymous users from index and identify events ([#87](https://github.com/launchdarkly/haskell-server-sdk/issues/87)) ([85e512a](https://github.com/launchdarkly/haskell-server-sdk/commit/85e512a80dff0e8814afb0f6e6ca334c30afe9cd))+ ## [4.1.0](https://github.com/launchdarkly/haskell-server-sdk/compare/4.0.4...4.1.0) (2024-03-18)  
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
launchdarkly-server-sdk.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           launchdarkly-server-sdk-version:        4.1.0+version:        4.2.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
src/LaunchDarkly/Server/Client.hs view
@@ -55,7 +55,7 @@ import LaunchDarkly.Server.Config.HttpConfiguration (HttpConfiguration (..)) import LaunchDarkly.Server.Config.Internal (ApplicationInfo, Config, getApplicationInfoHeader, shouldSendEvents) import LaunchDarkly.Server.Context (getValue)-import LaunchDarkly.Server.Context.Internal (Context (Invalid), getCanonicalKey, getKey, getKeys, redactContext)+import LaunchDarkly.Server.Context.Internal (Context (Invalid), getCanonicalKey, getKey, getKeys, optionallyRedactAnonymous, redactContext) import LaunchDarkly.Server.DataSource.Internal (DataSource (..), DataSourceFactory, DataSourceUpdates (..), defaultDataSourceUpdates, nullDataSourceFactory) import LaunchDarkly.Server.Details (EvalErrorKind (..), EvaluationDetail (..), EvaluationReason (..)) import LaunchDarkly.Server.Evaluate (evaluateDetail, evaluateTyped)@@ -129,7 +129,7 @@     clientContext <- makeClientContext config      let dataSourceUpdates = defaultDataSourceUpdates status store-    dataSource <- dataSourceFactory config clientContext dataSourceUpdates+    dataSource <- getDataSourceFactory config clientContext dataSourceUpdates     eventThreadPair <-         if not (shouldSendEvents config)             then pure Nothing@@ -142,8 +142,8 @@      pure $ Client {..} -dataSourceFactory :: Config -> DataSourceFactory-dataSourceFactory config =+getDataSourceFactory :: Config -> DataSourceFactory+getDataSourceFactory config =     if getField @"offline" config || getField @"useLdd" config         then nullDataSourceFactory         else case getField @"dataSourceFactory" config of@@ -266,11 +266,15 @@ identify client (Invalid err) = clientRunLogger client $ $(logWarn) $ "identify called with an invalid context: " <> err identify client context = case (getValue "key" context) of     (String "") -> clientRunLogger client $ $(logWarn) "identify called with empty key"-    _ -> do-        let redacted = redactContext (getField @"config" client) context-        x <- makeBaseEvent $ IdentifyEvent {key = getKey context, context = redacted}-        _ <- noticeContext (getField @"events" client) context-        queueEvent (getField @"config" client) (getField @"events" client) (EventTypeIdentify x)+    _anyValidKey -> do+        let identifyContext = optionallyRedactAnonymous (getField @"config" client) context+        case identifyContext of+            (Invalid _) -> pure ()+            _anyValidContext -> do+                let redacted = redactContext (getField @"config" client) identifyContext+                x <- makeBaseEvent $ IdentifyEvent {key = getKey context, context = redacted}+                _ <- noticeContext (getField @"events" client) context+                queueEvent (getField @"config" client) (getField @"events" client) (EventTypeIdentify x)  -- | -- Track reports that a context has performed an event. Custom data can be
src/LaunchDarkly/Server/Client/Internal.hs view
@@ -21,7 +21,7 @@  -- | The version string for this library. clientVersion :: Text-clientVersion = "4.1.0" -- x-release-please-version+clientVersion = "4.2.0" -- x-release-please-version  -- | -- Client is the LaunchDarkly client. Client instances are thread-safe.
src/LaunchDarkly/Server/Config.hs view
@@ -27,6 +27,7 @@     , configSetUseLdd     , configSetDataSourceFactory     , configSetApplicationInfo+    , configSetOmitAnonymousContexts     , ApplicationInfo     , makeApplicationInfo     , withApplicationValue@@ -70,6 +71,7 @@         , dataSourceFactory = Nothing         , manager = Nothing         , applicationInfo = Nothing+        , omitAnonymousContexts = False         }  -- | Set the SDK key used to authenticate with LaunchDarkly.@@ -221,3 +223,10 @@ -- appropriately configured dict to the 'Config' object. configSetApplicationInfo :: ApplicationInfo -> Config -> Config configSetApplicationInfo = setField @"applicationInfo" . Just++-- |+-- Sets whether anonymous contexts should be omitted from index and identify events.+--+-- By default, anonymous contexts are included in index and identify events.+configSetOmitAnonymousContexts :: Bool -> Config -> Config+configSetOmitAnonymousContexts = setField @"omitAnonymousContexts"
src/LaunchDarkly/Server/Config/Internal.hs view
@@ -49,6 +49,7 @@     , dataSourceFactory :: !(Maybe DataSourceFactory)     , manager :: !(Maybe Manager)     , applicationInfo :: !(Maybe ApplicationInfo)+    , omitAnonymousContexts :: !Bool     }     deriving (Generic) 
src/LaunchDarkly/Server/Context/Internal.hs view
@@ -33,6 +33,8 @@     , getKinds     , redactContext     , redactContextRedactAnonymous+    , optionallyRedactAnonymous+    , withoutAnonymousContexts     ) where @@ -48,8 +50,8 @@ import Data.Text (Text, intercalate, replace, unpack) import qualified GHC.Exts as Exts (fromList) import GHC.Generics (Generic)-import LaunchDarkly.AesonCompat (KeyMap, deleteKey, emptyObject, foldrWithKey, fromList, insertKey, keyMapUnion, lookupKey, mapValues, objectKeys, singleton, toList)-import LaunchDarkly.Server.Config (Config)+import LaunchDarkly.AesonCompat (KeyMap, deleteKey, emptyObject, foldrWithKey, fromList, insertKey, keyMapUnion, lookupKey, mapValues, objectKeys, objectValues, singleton, toList)+import LaunchDarkly.Server.Config.Internal (Config (..)) import LaunchDarkly.Server.Reference (Reference) import qualified LaunchDarkly.Server.Reference as R @@ -157,7 +159,7 @@             _ ->                 Multi                     MultiContext-                        { fullKey = intercalate ":" $ map (\c -> canonicalizeKey (key c) (kind c)) sorted+                        { fullKey = intercalate ":" $ map (\c -> canonicalizeKey (getField @"key" c) (kind c)) sorted                         , contexts = fromList $ map (\c -> ((kind c), c)) singleContexts                         } @@ -268,7 +270,7 @@ -- This method is functionally equivalent to @fromMaybe "" $ getValue "key"@, -- it's just nicer to use. getKey :: Context -> Text-getKey (Single c) = key c+getKey (Single c) = getField @"key" c getKey _ = ""  -- Internally used convenience function for retrieving all context keys,@@ -278,8 +280,8 @@ -- and key. Multi-kind contexts will return a map of kind / key pairs for each -- of its sub-contexts. An invalid context will return the empty map. getKeys :: Context -> KeyMap Text-getKeys (Single c) = singleton (kind c) (key c)-getKeys (Multi (MultiContext {contexts})) = mapValues key contexts+getKeys (Single c) = singleton (kind c) (getField @"key" c)+getKeys (Multi (MultiContext {contexts})) = mapValues (getField @"key") contexts getKeys _ = emptyObject  -- Internally used convenience function to retrieve a context's fully qualified@@ -520,3 +522,32 @@         let substate@(RedactState {context = subcontext}) = redactComponents xs (level + 1) (state {context = o})          in substate {context = insertKey x (Object $ subcontext) context}     _ -> state++-- |+-- Internally used only.+--+-- If the config has omitAnonymousContexts set to True, this method will return a new context with+-- all anonymous contexts removed. If the config does not have omitAnonymousContexts set to True,+-- this method will return the context as is.+optionallyRedactAnonymous :: Config -> Context -> Context+optionallyRedactAnonymous Config {omitAnonymousContexts = True} c = withoutAnonymousContexts c+optionallyRedactAnonymous _ c = c++-- |+-- Internally used only.+--+-- For a multi-kind context:+--+-- A multi-kind context is made up of two or more single-kind contexts. This method will first discard any+-- single-kind contexts which are anonymous. It will then create a new multi-kind context from the remaining+-- single-kind contexts. This may result in an invalid context (e.g. all single-kind contexts are anonymous).+--+-- For a single-kind context:+--+-- If the context is not anonymous, this method will return the current context as is and unmodified.+--+-- If the context is anonymous, this method will return an invalid context.+withoutAnonymousContexts :: Context -> Context+withoutAnonymousContexts (Single SingleContext {anonymous = True}) = makeMultiContext []+withoutAnonymousContexts (Multi MultiContext {contexts}) = makeMultiContext $ map Single $ filter (not . anonymous) $ objectValues contexts+withoutAnonymousContexts c = c
src/LaunchDarkly/Server/Events.hs view
@@ -19,7 +19,7 @@ import LaunchDarkly.AesonCompat (KeyMap, insertKey, keyMapUnion, lookupKey, objectValues) import LaunchDarkly.Server.Config.Internal (Config, shouldSendEvents) import LaunchDarkly.Server.Context (Context)-import LaunchDarkly.Server.Context.Internal (getCanonicalKey, getKinds, redactContext, redactContextRedactAnonymous)+import LaunchDarkly.Server.Context.Internal (Context (Invalid), getCanonicalKey, getKinds, optionallyRedactAnonymous, redactContext, redactContextRedactAnonymous) import LaunchDarkly.Server.Details (EvaluationReason (..)) import LaunchDarkly.Server.Features (Flag) @@ -375,9 +375,12 @@  maybeIndexContext :: Natural -> Config -> Context -> EventState -> IO () maybeIndexContext now config context state = do-    noticedContext <- noticeContext state context-    when noticedContext $-        queueEvent config state (EventTypeIndex $ BaseEvent now $ IndexEvent {context = redactContext config context})+    case optionallyRedactAnonymous config context of+        (Invalid _) -> pure ()+        ctx -> do+            noticedContext <- noticeContext state ctx+            when noticedContext $+                queueEvent config state (EventTypeIndex $ BaseEvent now $ IndexEvent {context = redactContext config ctx})  noticeContext :: EventState -> Context -> IO Bool noticeContext state context = modifyMVar (getField @"contextKeyLRU" state) $ \cache -> do
test/Spec/Context.hs view
@@ -12,7 +12,7 @@ import LaunchDarkly.AesonCompat (lookupKey) import LaunchDarkly.Server.Config (configSetAllAttributesPrivate, makeConfig) import LaunchDarkly.Server.Context-import LaunchDarkly.Server.Context.Internal (redactContext, redactContextRedactAnonymous)+import LaunchDarkly.Server.Context.Internal (redactContext, redactContextRedactAnonymous, withoutAnonymousContexts) import qualified LaunchDarkly.Server.Reference as R  confirmInvalidContext :: Context -> Text -> Assertion@@ -397,6 +397,25 @@      orgObj = case lookupKey "org" decodedIntoMap of (Just (Object o)) -> o; _decodeFailure -> error "expected object" +canRedactAnonymousContextsAsExpected :: Test+canRedactAnonymousContextsAsExpected =+    TestCase $+        let anonymousUser = makeContext "user-key" "user" & withAnonymous True+            anonymousOrg = makeContext "org-key" "org" & withAnonymous True+            device = makeContext "device-key" "device"+            mc = makeMultiContext [anonymousUser, anonymousOrg, device]+            anonMc = makeMultiContext [anonymousUser, anonymousOrg]+         in ( do+                -- Redacting an anonymous context should result in an invalid context+                assertEqual "" False $ isValid $ withoutAnonymousContexts anonymousUser+                -- Redacting a non-anonymous context should result in the same context+                assertEqual "" device $ withoutAnonymousContexts device+                -- Redacting a multi-context should result in a multi-context with only the non-anonymous contexts+                assertEqual "" device $ withoutAnonymousContexts mc+                -- Redacting a multi-context with only anonymous contexts should result in an invalid context+                assertEqual "" False $ isValid $ withoutAnonymousContexts anonMc+            )+ allTests :: Test allTests =     TestList@@ -419,4 +438,5 @@         , canRedactAllAttributesCorrectly         , canRedactSingleKindAnonymousContextAttributesCorrectly         , canRedactMultiKindAnonymousContextAttributesCorrectly+        , canRedactAnonymousContextsAsExpected         ]