Skip to content

Latest commit

 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scientist

Hackage Stackage Nightly Stackage LTS CI

Haskell port of github/scientist.

Usage

The following extensions are recommended:

{-# LANGUAGE OverloadedStrings #-}

Most usage will only require the top-level library:

import Scientist
  1. Define a new Experiment m c a b with,

    ex0 :: Functor m => Experiment m c a b
    ex0 = newExperiment "some name" theOriginalCode
  2. The type variables capture the following details:

    • m: Some Monad to operate in, e.g. IO.
    • c: Any context value you attach with setExperimentContext. It will then be available in the Result c a b you publish.
    • a: The result type of your original (aka "control") code, this is what is always returned and so is the return type of experimentRun.
    • b: The result type of your experimental (aka "candidate") code. It probably won't differ (and must not to use a comparison like (==)), but it can (provided you implement a comparison between a and b).
  3. Configure the Experiment as desired

    ex1 :: (Functor m, Eq a) => Experiment m c a a
    ex1 =
      setExperimentPublish publish0
        $ setExperimentCompare experimentCompareEq
        $ setExperimentTry theExperimentalCode
        $ newExperiment "some name" theOriginalCode
    
    -- Increment Statsd, Log, store in Redis, whatever
    publish0 :: Result c a b -> m ()
    publish0 = undefined
  4. Run the experiment

    run0 :: (MonadUnliftIO m, Eq a) => m a
    run0 =
      experimentRun
        $ setExperimentPublish publish0
        $ setExperimentCompare experimentCompareEq
        $ setExperimentTry theExperimentalCode
        $ newExperiment "some name" theOriginalCode
  5. Explore things like setExperimentIgnore, setExperimentEnabled, etc.


The rest of this README matches section-by-section to the ported project and shows only the differences in syntax for those features. Please follow the header links for additional details, motivation, etc.

myWidgetAllows :: MonadUnliftIO m => Model -> User -> m Bool
myWidgetAllows model user = do
  experimentRun
    $ setExperimentTry
        (userCanRead user model) -- new way
    $ newExperiment "widget-permissions"
        (modelCheckUserValid model user) -- old way
run1 :: MonadUnliftIO m => m a
run1 =
  experimentRun
    $ setExperimentEnabled (pure True)
    $ setExperimentOnException onScientistException
    $ setExperimentPublish (liftIO . putStrLn . formatResult)
    $ setExperimentTry theExperimentalCode
    $ newExperiment "some name" theOriginalCode

onScientistException :: MonadIO m => SomeException -> m ()
onScientistException ex = do
  liftIO $ putStrLn "..."

  -- To re-raise
  throwIO ex

formatResult :: Result c a b -> String
formatResult = undefined
run2 :: MonadUnliftIO m => m [User]
run2 =
  experimentRun
    $ setExperimentCompare (experimentCompareOn $ map userLogin)
    $ setExperimentTry userServiceFetch
    $ newExperiment "users" fetchAllUsers

When using experimentCompareOn, By, or Eq, if a candidate branch raises an exception, that will never compare equally.

See setExperimentContext.

Just do it ahead of time.

run3 :: MonadUnliftIO m => m a
run3 = do
  x <- expensiveSetup

  experimentRun
    $ setExperimentTry (theExperimentalCodeWith x)
    $ newExperiment "expensive" (theOriginalCodeWith x)

Not supported at this time. Format the value(s) as necessary when publishing.

See setExperimentIgnore.

See setExperimentRunIf.

run4 :: MonadUnliftIO m => m a
run4 =
  experimentRun
    $ setExperimentEnabled (experimentEnabledPercent 30)
    $ setExperimentTry theExperimentalCode
    $ newExperiment "some name" theOriginalCode
run5 :: MonadUnliftIO m => m User
run5 =
  experimentRun
    $ setExperimentPublish publish1
    $ setExperimentTry theExperimentalCode
    $ newExperiment "some name" theOriginalCode

publish1 :: MonadIO m => Result MyContext User User -> m ()
publish1 result = do
  -- Details are present unless it's a ResultSkipped, which we'll ignore
  for_ (resultDetails result) $ \details -> do
    let eName = resultDetailsExperimentName details

    -- Store the timing for the control value,
    statsdTiming ("science." <> eName <> ".control")
      $ resultControlDuration
      $ resultDetailsControl details

    -- for the candidate (only the first, see "Breaking the rules" below,
    statsdTiming ("science." <> eName <> ".candidate")
      $ resultCandidateDuration
      $ resultDetailsCandidate details

    -- and counts for match/ignore/mismatch:
    case result of
      ResultSkipped{} -> pure ()
      ResultMatched{} -> do
        statsdIncrement $ "science." <> eName <> ".matched"
      ResultIgnored{} -> do
        statsdIncrement $ "science." <> eName <> ".ignored"
      ResultMismatched{} -> do
        statsdIncrement $ "science." <> eName <> ".mismatched"
        -- Finally, store mismatches in redis so they can be retrieved and
        -- examined later on, for debugging and research.
        storeMismatchData details

storeMismatchData :: Monad m => ResultDetails MyContext User User -> m ()
storeMismatchData details = do
  let
    eName = resultDetailsExperimentName details
    eContext = resultDetailsExperimentContext details

    payload = MyPayload
      { name = eName
      , context = eContext
      , control = controlObservationPayload $ resultDetailsControl details
      , candidate = candidateObservationPayload $ resultDetailsCandidate details
      , execution_order = resultDetailsExecutionOrder details
      }

    key = "science." <> eName <> ".mismatch"

  redisLpush key $ toJSON payload
  redisLtrim key 0 1000

controlObservationPayload :: ResultControl User -> Value
controlObservationPayload rc =
  object ["value" .= cleanValue (resultControlValue rc)]

candidateObservationPayload :: ResultCandidate User -> Value
candidateObservationPayload rc = case resultCandidateValue rc of
  Left ex -> object ["exception" .= displayException ex]
  Right user -> object ["value" .= cleanValue user]

-- See "Keeping it clean" above
cleanValue :: User -> Text
cleanValue = userLogin

See Result, ResultDetails, ResultControl and ResultCandidate for all the available data you can publish.

TODO: raise_on_mismatches

TODO: raise_with

Candidate code is wrapped in tryAny, resulting in Either SomeException values in the result candidates list. We use the safer UnliftIO.Exception module.

See setExperimentOnException.

nope0 :: Experiment m c a b -> Experiment m c a b
nope0 = setExperimentIgnore (\_ _ -> True)

Or, more efficiently:

nope1 :: Experiment m c a b -> Experiment m c a b
nope1 = setExperimentCompare (\_ _ -> True)

If you call setExperimentTry more than once, it will append (not overwrite) candidate branches. If any candidate is deemed ignored or a mismatch, the overall result will be.

setExperimentTryNamed can be used to give branches explicit names (otherwise, they are "control", "candidate", "candidate-{n}"). These names are visible in ResultControl, ResultCandidate, and resultDetailsExecutionOrder.

Not supported.

Supporting the lack of a Control branch in the types would ultimately lead to a runtime error if you attempt to run such an Experiment without having and naming a Candidate to use instead, or severely complicate the types to account for that safely. In our opinion, this feature is not worth either of those.


LICENSE | CHANGELOG

About

Haskell port of github/scientist

Topics

Resources

Stars

4 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages