packages feed

rollbar-client (empty) → 0.1.0

raw patch · 14 files changed

+1121/−0 lines, 14 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, directory, exceptions, hspec, mtl, process, req, rollbar-client, text, unordered-containers, yaml

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for rollbar-client++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,19 @@+Copyright 2020 Stack Builders Inc.++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,12 @@+# Rollbar Client++Core library to communicate with [Rollbar+API](https://explorer.docs.rollbar.com/).++## Getting Started++Read the instructions [here](../README.md).++## Example++For a complete example check the link [here](example/Main.hs).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ example/Main.hs view
@@ -0,0 +1,46 @@+module Main+  ( main+  ) where++import qualified Data.Text as Text+import Rollbar.Client+import System.Environment (getArgs)++-- To load this example using Cabal, run:+--+-- ROLLBAR_TOKEN=<...> cabal repl --flags example client-example+--+-- Or, if using Stack, replace the Cabal part with:+--+-- stack ghci --flag rollbar-client:example rollbar-client:exe:client-example+--+-- You can then test creating a Rollbar item. If there are no arguments, the+-- example uses the 'withRollbar' function, which automatically creates a+-- Rollbar item after catching an exception:+--+-- >>> :main+-- *** Exception: Boom+-- CallStack (from HasCallStack):+--   error, called at .../rollbar-haskell/rollbar-client/example/Main.hs:39:30 in main:Main+--+-- If there are arguments, the example uses the 'runRollbar' function, which+-- allows us to create and send our own Rollbar items. In this case, we send+-- the arguments as message and change the level to warning:+--+-- >>> :main Warning!+-- ItemId "55c3e83084d943d6af3bf940e80513e1"++main :: IO ()+main = do+  settings <- readSettings "rollbar.yaml"+  args <- getArgs+  case args of+    [] ->+      withRollbar settings $ error "Boom"+    _ -> do+      itemId <-+        runRollbar settings $ do+          let body = Text.pack (unwords args)+          item <- mkItem (PayloadMessage (Message body mempty))+          createItem item { itemLevel = Just LevelWarning }+      print itemId
+ rollbar-client.cabal view
@@ -0,0 +1,100 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: b3e835bcc225cb66a06268126c5a04b48d06fd90b55677e26219ecfe207068d3++name:           rollbar-client+version:        0.1.0+synopsis:       Core library to communicate with Rollbar API.+description:    Please see the README on GitHub at+                <https://github.com/stackbuilders/rollbar-haskell/tree/master/rollbar-client>+homepage:       https://github.com/stackbuilders/rollbar-haskell#readme+bug-reports:    https://github.com/stackbuilders/rollbar-haskell/issues+author:         Stack Builders Inc.+maintainer:     Sebastián Estrella <sestrella@stackbuilders.com>+copyright:      2020 Stack Builders Inc.+license:        MIT+license-file:   LICENSE+tested-with:    GHC ==8.6.5, GHC ==8.8.4, GHC ==8.10.2+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/stackbuilders/rollbar-haskell++flag example+  description: Build the example+  manual: False+  default: False++library+  exposed-modules:+      Rollbar.Client+  other-modules:+      Rollbar.Client.Deploy+      Rollbar.Client.Internal+      Rollbar.Client.Item+      Rollbar.Client.Ping+      Rollbar.Client.Settings+      Paths_rollbar_client+  hs-source-dirs:+      src+  ghc-options: -Wall+  build-depends:+      aeson >=1.4 && <2+    , base >=4.12 && <5+    , bytestring >=0.10 && <1+    , directory >=1.3 && <2+    , exceptions >=0.10 && <1+    , mtl >=2.2 && <3+    , process >=1.6 && <2+    , req >=2.1 && <4+    , text >=1.2 && <2+    , unordered-containers >=0.2 && <1+    , yaml >=0.11 && <1+  default-language: Haskell2010++executable client-example+  main-is: Main.hs+  other-modules:+      Paths_rollbar_client+  hs-source-dirs:+      example+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.12 && <5+    , rollbar-client+    , text+  if flag(example)+    buildable: True+  else+    buildable: False+  default-language: Haskell2010++test-suite spec+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Rollbar.ClientSpec+      Paths_rollbar_client+  hs-source-dirs:+      test+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-tool-depends:+      hspec-discover:hspec-discover >=2.7 && <3+  build-depends:+      aeson+    , base >=4.12 && <5+    , hspec >=2.7 && <3+    , mtl+    , rollbar-client+    , text+    , unordered-containers+    , yaml+  default-language: Haskell2010
+ src/Rollbar/Client.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- |+-- Module: Rollbar.Client+-- Copyright: (c) 2020 Stack Builders Inc.+-- License: MIT+-- Maintainer: Sebastián Estrella <sestrella@stackbuilders.com>+module Rollbar.Client+  (+    -- * Deploy+    module Rollbar.Client.Deploy+    -- * Item+  , module Rollbar.Client.Item+    -- * Ping+  , module Rollbar.Client.Ping+    -- * Settings+  , module Rollbar.Client.Settings+  , Rollbar(..)+  , withRollbar+  , runRollbar+  ) where++import Control.Monad (void)+import Control.Monad.Catch+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Reader (ReaderT(..), ask)+import Control.Monad.Trans (MonadTrans(..))+import Network.HTTP.Req+import Rollbar.Client.Deploy+import Rollbar.Client.Item+import Rollbar.Client.Ping+import Rollbar.Client.Settings++newtype Rollbar a = Rollbar (ReaderT Settings Req a)+  deriving+    ( Applicative+    , Functor+    , Monad+    , MonadIO+    )++instance MonadHttp Rollbar where+  handleHttpException = Rollbar . lift . handleHttpException++instance HasSettings Rollbar where+  getSettings = Rollbar ask++-- | Runs a computation, captures any 'E.SomeException' threw, and send it to+-- Rollbar.+withRollbar :: (MonadCatch m, MonadIO m) => Settings -> m a -> m a+withRollbar settings f = f `catch` \e -> do+  void $ runRollbar settings $ do+    item <- mkItem $ PayloadTrace $ Trace [] $ mkException (e :: SomeException)+    createItem item++  throwM e++-- | Run a computation in 'Rollbar' monad.+runRollbar :: MonadIO m => Settings -> Rollbar a -> m a+runRollbar settings (Rollbar f) = runReq defaultHttpConfig $+  runReaderT f settings
+ src/Rollbar/Client/Deploy.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Rollbar.Client.Deploy+  ( -- ** Requests+    Deploy(..)+  , mkDeploy+  , Status(..)+    -- ** Responses+  , DeployId(..)+    -- ** Endpoints+  , reportDeploy+  ) where++import qualified Data.Text as T++import Control.Monad.IO.Class (MonadIO(..))+import Data.Aeson+import Data.Text+import Network.HTTP.Req+import Rollbar.Client.Internal+import Rollbar.Client.Settings+import System.Environment (lookupEnv)++data Deploy = Deploy+  { deployEnvironment :: Environment+    -- ^ Environment to which the revision was deployed.+  , deployRevision :: Revision+    -- ^ Git SHA of revision being deployed.+  , deployRollbarUsername :: Maybe Text+    -- ^ Rollbar username of person who deployed.+  , deployLocalUsername :: Maybe Text+    -- ^ Local username of person who deployed. Displayed in web app if no+    -- 'deployRollbarUsername' was specified.+  , deployComment :: Maybe Text+    -- ^ Additional text to include with the deploy.+  , deployStatus :: Maybe Status+    -- ^ Status of the deployment.+  } deriving (Eq, Show)++instance ToJSON Deploy where+  toJSON Deploy{..} = object+    [ "environment" .= deployEnvironment+    , "revision" .= deployRevision+    , "rollbar_username" .= deployRollbarUsername+    , "local_username" .= deployLocalUsername+    , "comment" .= deployComment+    , "status" .= deployStatus+    ]++-- | Builds a 'Deploy' based on a 'Revision'.+--+-- __Example__+--+-- > getRevision >>= mkDeploy+mkDeploy+  :: (HasSettings m, MonadIO m)+  => Revision+  -> m Deploy+mkDeploy revision = do+  env <- settingsEnvironment <$> getSettings+  muser <- fmap T.pack <$> liftIO (lookupEnv "USER")+  return Deploy+    { deployEnvironment = env+    , deployRevision = revision+    , deployRollbarUsername = Nothing+    , deployLocalUsername = muser+    , deployComment = Nothing+    , deployStatus = Just StatusSucceeded+    }++-- | Status of the deployment.+data Status+  = StatusStarted+  | StatusSucceeded+  | StatusFailed+  | StatusTimedOut+  deriving (Eq, Show)++instance ToJSON Status where+  toJSON StatusStarted = String "started"+  toJSON StatusSucceeded = String "succeeded"+  toJSON StatusFailed = String "failed"+  toJSON StatusTimedOut = String "timed_out"++newtype DeployId = DeployId Integer+  deriving (Eq, Num, Ord, Show)++instance FromJSON DeployId where+  parseJSON = withObject "DeployId" $ \o ->+    DeployId <$> o .: "deploy_id"++-- | Tracks a deploy in Rollbar.+--+-- __Example__+--+-- > settings <- readSettings "rollbar.yaml"+-- > runRollbar settings $ do+-- >   deploy <- getRevision >>= mkDeploy+-- >   reportDeploy deploy+--+-- __Reference__+--+-- <https://explorer.docs.rollbar.com/#operation/post-deploy>+reportDeploy+  :: (HasSettings m, MonadHttp m)+  => Deploy+  -> m DeployId+reportDeploy deploy =+  fmap+    (unDataResponse . responseBody)+    (rollbar POST url (ReqBodyJson deploy) jsonResponse)+  where+    url = baseUrl /: "deploy"
+ src/Rollbar/Client/Internal.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}++module Rollbar.Client.Internal+  ( DataResponse(..)+  , ResultResponse(..)+  , rollbar+  , baseUrl+  ) where++import Data.Aeson+import Data.Proxy (Proxy)+import Network.HTTP.Req+import Rollbar.Client.Settings++newtype DataResponse a = DataResponse { unDataResponse :: a }+  deriving (Eq, Show)++instance FromJSON a => FromJSON (DataResponse a) where+  parseJSON = withObject "DataResponse" $ \o ->+    DataResponse <$> o .: "data"++data ResultResponse a = ResultResponse+  { resultResponseErr :: Integer+  , resultResponseResult :: a+  } deriving (Eq, Show)++instance FromJSON a => FromJSON (ResultResponse a) where+  parseJSON = withObject "ResultResponse" $ \o ->+    ResultResponse <$> o .: "err"+                   <*> o .: "result"++rollbar+  :: ( HasSettings m+     , HttpBody body+     , HttpBodyAllowed (AllowsBody method) (ProvidesBody body)+     , HttpMethod method+     , HttpResponse response+     , MonadHttp m+     )+  => method+  -> Url 'Https+  -> body+  -> Proxy response+  -> m response+rollbar method url body response = do+  Token token <- settingsToken <$> getSettings+  req method url body response $ header "X-Rollbar-Access-Token" token++baseUrl :: Url 'Https+baseUrl = https "api.rollbar.com" /: "api" /: "1"
+ src/Rollbar/Client/Item.hs view
@@ -0,0 +1,401 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Rollbar.Client.Item+  ( -- ** Requests+    Item(..)+  , mkItem+  , Body(..)+  , Payload(..)+  , Trace(..)+  , Frame(..)+  , Context(..)+  , Exception(..)+  , mkException+  , Message(..)+  , Level(..)+  , mkLevel+  , Request(..)+  , getRequestModifier+  , Server(..)+  , Notifier(..)+  , defaultNotifier+    -- ** Responses+  , ItemId(..)+    -- ** Endpoints+  , createItem+  ) where++import qualified Control.Exception as E+import qualified Data.HashMap.Strict as HM+import qualified Data.Text as T++import Control.Monad.IO.Class (MonadIO(..))+import Data.Aeson+import Data.Maybe (catMaybes)+import Data.Monoid (Endo(..))+import Data.Text+import Network.HTTP.Req+import Rollbar.Client.Internal+import Rollbar.Client.Settings+import System.Directory (getCurrentDirectory)+import System.Info (arch, os)++data Item = Item+  { itemEnvironment :: Environment+    -- ^ The name of the environment in which this occurrence was seen. A+    -- string up to 255 characters. For best results, use "production" or+    -- "prod" for your production environment.  You don't need to configure+    -- anything in the Rollbar UI for new environment names; we'll detect them+    -- automatically.+  , itemBody :: Body+    -- ^ The main data being sent. It can either be a message, an exception, or+    -- a crash report.+  , itemLevel :: Maybe Level+    -- ^ The severity level. One of: "critical", "error", "warning", "info",+    -- "debug" Defaults to "error" for exceptions and "info" for messages.  The+    -- level of the *first* occurrence of an item is used as the item's level.+  -- timestamp+  -- code_version+  , itemPlatform :: Maybe Text+    -- ^ The platform on which this occurred. Meaningful platform names:+    -- "browser", "android", "ios", "flash", "client", "heroku",+    -- "google-app-engine" If this is a client-side event, be sure to specify+    -- the platform and use a post_client_item access token.+  , itemLanguage :: Maybe Text+    -- ^ The name of the language your code is written in.  This can affect the+    -- order of the frames in the stack trace. The following languages set the+    -- most recent call first - 'ruby', 'javascript', 'php', 'java',+    -- 'objective-c', 'lua' It will also change the way the individual frames+    -- are displayed, with what is most consistent with users of the language.+  , itemFramework :: Maybe Text+    -- ^ The name of the framework your code uses.+  -- context+  -- request+  , itemRequest :: Maybe Request+    -- ^ Data about the request this event occurred in.+  -- person+  , itemServer :: Maybe Server+    -- ^ Data about the server related to this event.+  -- client+  -- custom+  -- fingerprint+  -- title+  -- uuid+  , itemNotifier :: Notifier+    -- ^ Describes the library used to send this event.+  } deriving (Eq, Show)++instance ToJSON Item where+  toJSON Item{..} = object+    [ "data" .= object+        [ "environment" .= itemEnvironment+        , "body" .= itemBody+        , "level" .= itemLevel+        , "platform" .= itemPlatform+        , "language" .= itemLanguage+        , "framework" .= itemFramework+        , "request" .= itemRequest+        , "server" .= itemServer+        , "notifier" .= itemNotifier+        ]+    ]++-- | Builds an 'Item' based on a 'Payload'.+mkItem+  :: (HasSettings m, MonadIO m)+  => Payload+  -> m Item+mkItem payload = do+  environment <- settingsEnvironment <$> getSettings+  root <- liftIO getCurrentDirectory+  return Item+    { itemEnvironment = environment+    , itemBody = Body+        { bodyPayload = payload+        }+    , itemLevel = Just $ mkLevel payload+    , itemPlatform = Just $ T.pack os+    , itemLanguage = Just "haskell"+    , itemFramework = Nothing+    , itemRequest = Nothing+    , itemServer = Just Server+        { serverCpu = Just $ T.pack arch+        , serverHost = Nothing+        , serverRoot = Just $ T.pack root+        , serverBranch = Nothing+        , serverCodeVersion = Nothing+        }+    , itemNotifier = defaultNotifier+    }++-- | The main data being sent. It can either be a message, an exception, or a+-- crash report.+newtype Body = Body { bodyPayload :: Payload }+  deriving (Eq, Show)++instance ToJSON Body where+  toJSON Body{..} = object+    [ case bodyPayload of+        (PayloadTrace trace) -> ("trace", toJSON trace)+        (PayloadTraceChain traceChain) -> ("trace_chain", toJSON traceChain)+        (PayloadMessage message) -> ("message", toJSON message)+    ]++data Payload+  = PayloadTrace Trace+  | PayloadTraceChain [Trace]+  | PayloadMessage Message+  deriving (Eq, Show)++data Trace = Trace+  { traceFrames :: [Frame]+    -- ^ A list of stack frames, ordered such that the most recent call is last+    -- in the list.+  , traceException :: Exception+  } deriving (Eq, Show)++instance ToJSON Trace where+  toJSON Trace{..} = object+    [ "frames" .= traceFrames+    , "exception" .= traceException+    ]++data Frame = Frame+  { frameFilename :: Text+    -- ^ The filename including its full path.+  , frameLineno :: Maybe Integer+    -- ^ The line number as an integer.+  , frameColno :: Maybe Integer+    -- ^ The column number as an integer.+  , frameMethod :: Maybe Text+    -- ^ The method or function name.+  , frameCode :: Maybe Text+    -- ^ The line of code.+  , frameClassName :: Maybe Text+    -- ^ A string containing the class name.  Used in the UI when the payload's+    -- top-level "language" key has the value "java".+  , frameContext :: Maybe Context+    -- ^ Additional code before and after the "code" line.+  } deriving (Eq, Show)++instance ToJSON Frame where+  toJSON Frame{..} = object+    [ "filename" .= frameFilename+    , "lineno" .= frameLineno+    , "colno" .= frameColno+    , "method" .= frameMethod+    , "code" .= frameCode+    , "class_name" .= frameClassName+    , "context" .= frameContext+    ]++-- | Additional code before and after the "code" line.+data Context = Context+  { contextPre :: [Text]+    -- ^ List of lines of code before the "code" line.+  , contextPost :: [Text]+    -- ^ List of lines of code after the "code" line.+  } deriving (Eq, Show)++instance ToJSON Context where+  toJSON Context{..} = object+    [ "pre" .= contextPre+    , "post" .= contextPost+    ]++-- | An object describing the exception instance.+data Exception = Exception+  { exceptionClass :: Text+    -- ^ The exception class name.+  , exceptionMessage :: Maybe Text+    -- ^ The exception message, as a string.+  , exceptionDescription :: Maybe Text+    -- ^ An alternate human-readable string describing the exception Usually+    -- the original exception message will have been machine-generated; you can+    -- use this to send something custom.+  } deriving (Eq, Show)++instance ToJSON Exception where+  toJSON Exception{..} = object+    [ "class" .= exceptionClass+    , "message" .= exceptionMessage+    , "description" .= exceptionDescription+    ]++-- | Builds a 'Exception' based on 'E.SomeException'.+mkException :: E.Exception e => e -> Exception+mkException e = Exception+  { exceptionClass = T.pack $ E.displayException e+  , exceptionMessage = Nothing+  , exceptionDescription = Nothing+  }++data Message = Message+  { messageBody :: Text+  , messageMetadata :: Object+  } deriving (Eq, Show)++instance ToJSON Message where+  toJSON Message{..} = Object $+    HM.insert "body" (toJSON messageBody) messageMetadata++-- | The severity level. One of: "critical", "error", "warning", "info",+-- "debug" Defaults to "error" for exceptions and "info" for messages. The+-- level of the *first* occurrence of an item is used as the item's level.+data Level+  = LevelCritical+  | LevelError+  | LevelWarning+  | LevelInfo+  | LevelDebug+  deriving (Eq, Show)++instance ToJSON Level where+  toJSON = \case+    LevelCritical -> "critical"+    LevelError -> "error"+    LevelWarning -> "warning"+    LevelInfo -> "info"+    LevelDebug -> "debug"++-- | Builds a 'Level' based on a 'Payload'.+mkLevel :: Payload -> Level+mkLevel (PayloadMessage _) = LevelInfo+mkLevel _ = LevelError++-- | Data about the request this event occurred in.+data Request = Request+  { requestUrl :: Text+    -- ^ Full URL where this event occurred.+  , requestMethod :: Text+    -- ^ The request method.+  , requestHeaders :: Object+    -- ^ Object containing the request headers.+  , requestParams :: Object+    -- ^ Any routing parameters (i.e. for use with Rails Routes).+  , requestGet :: Object+    -- ^ Query string params.+  , requestQueryStrings :: Text+    -- ^ The raw query string.+  , requestPost :: Object+    -- ^ POST params.+  , requestBody :: Text+    -- ^ The raw POST body.+  , requestUserIp :: Text+    -- ^ Can also be the special value "$remote_ip", which will be replaced+    -- with the source IP of the API request.  Will be indexed, as long as it+    -- is a valid IPv4 address.+  } deriving (Eq, Show)++instance ToJSON Request where+  toJSON Request{..} = object+    [ "url" .= requestUrl+    , "method" .= requestMethod+    , "headers" .= requestHeaders+    , "params" .= requestParams+    , "GET" .= requestGet+    , "query_string" .= requestQueryStrings+    , "POST" .= requestPost+    , "body" .= requestBody+    , "user_ip" .= requestUserIp+    ]++-- | Pulls 'RequestModifiers' out of 'Settings' and build a list of 'Endo+-- Request' which are folded as a single request modifier function.+getRequestModifier :: (HasSettings m, Monad m) => m (Request -> Request)+getRequestModifier = do+  RequestModifiers{..} <- settingsRequestModifiers <$> getSettings+  return $ appEndo $ mconcat $ catMaybes+    [ withHeaders . excludeNames <$> requestModifiersExcludeHeaders+    , withParams . excludeNames <$> requestModifiersExcludeParams+    , withHeaders . includeNames <$> requestModifiersIncludeHeaders+    , withParams . includeNames <$> requestModifiersIncludeParams+    ]+  where+    withHeaders f = Endo $ \request -> request+      { requestHeaders = f $ requestHeaders request }+    withParams f = Endo $ \request -> request+      { requestParams = f $ requestParams request }+    excludeNames names = HM.filterWithKey $ \name _ -> name `notElem` names+    includeNames names = HM.filterWithKey $ \name _ -> name `elem` names++-- | Data about the server related to this event.+data Server = Server+  { serverCpu :: Maybe Text+    -- ^ A string up to 255 characters.+  , serverHost :: Maybe Text+    -- ^ The server hostname. Will be indexed.+  , serverRoot :: Maybe Text+    -- ^ Path to the application code root, not including the final slash.+    -- Used to collapse non-project code when displaying tracebacks.+  , serverBranch :: Maybe Text+    -- ^ Name of the checked-out source control branch. Defaults to "master".+  , serverCodeVersion :: Maybe Text+    -- ^ String describing the running code version on the server.+  } deriving (Eq, Show)++instance ToJSON Server where+  toJSON Server{..} = object+    [ "cpu" .= serverCpu+    , "host" .= serverHost+    , "root" .= serverRoot+    , "branch" .= serverBranch+    , "code_version" .= serverCodeVersion+    ]++data Notifier = Notifier+  { notifierName :: Text+  , notifierVersion :: Text+  } deriving (Eq, Show)++instance ToJSON Notifier where+  toJSON Notifier{..} = object+    [ "name" .= notifierName+    , "version" .= notifierVersion+    ]++-- | Returns information about this package such as the name and version.+defaultNotifier :: Notifier+defaultNotifier = Notifier+  { notifierName = "rollbar-client"+  , notifierVersion = "0.1.0"+  }++newtype ItemId = ItemId Text+  deriving (Eq, Show)++instance FromJSON ItemId where+  parseJSON = withObject "ItemId" $ \o ->+    ItemId <$> o .: "uuid"++-- | Reports an occurrence (exception or message) to Rollbar.+--+-- __Example__+--+-- > settings <- readSettings "rollbar.yaml"+-- > runRollbar settings $ do+-- >   item <- mkItem $ PayloadTrace $ Trace [] $ Exception+-- >     { exceptionClass = "NameError"+-- >     , exceptionMessage = Just "global name 'foo' is not defined"+-- >     , exceptionDescription = Just "Something went wrong while trying to save the user object"+-- >     }+-- >   createItem item+--+-- __Reference__+--+-- <https://explorer.docs.rollbar.com/#operation/create-item>+createItem+  :: (HasSettings m, MonadHttp m)+  => Item+  -> m ItemId+createItem item = do+  requestModifier <- getRequestModifier+  fmap+    (resultResponseResult . responseBody)+    (rollbar POST url (body requestModifier) jsonResponse)+  where+    url = baseUrl /: "item" /: ""+    body requestModifier = ReqBodyJson $+      item { itemRequest = requestModifier <$> itemRequest item }
+ src/Rollbar/Client/Ping.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}++module Rollbar.Client.Ping+  ( -- ** Requests+    Pong(..)+    -- ** Endpoints+  , ping+  ) where++import Control.Monad (void)+import Network.HTTP.Req+import Rollbar.Client.Internal (baseUrl)++data Pong = Pong+  deriving (Eq, Show)++-- | Pings Rollbar API server, on success returns 'Pong'.+--+-- __Example__+--+-- > settings <- readSettings "rollbar.yaml"+-- > runRollbar settings ping+--+-- __Reference__+--+-- <https://explorer.docs.rollbar.com/#section/Ping>+ping :: MonadHttp m => m Pong+ping = do+  void $ req GET url NoReqBody ignoreResponse mempty+  return Pong+  where+    url = baseUrl /: "status" /: "ping"
+ src/Rollbar/Client/Settings.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Rollbar.Client.Settings+  ( HasSettings(..)+  , Settings(..)+  , readSettings+  , Token(..)+  , Environment(..)+  , Revision(..)+  , getRevision+  , getRevisionMaybe+  , RequestModifiers(..)+  , defaultRequestModifiers+  ) where++import qualified Data.Text as T+import qualified Data.Text.Encoding as T++import Control.Exception (Exception, throwIO)+import Control.Monad (forM)+import Control.Monad.IO.Class (MonadIO(..))+import Data.Aeson+import Data.ByteString (ByteString)+import Data.List.NonEmpty+import Data.Text (Text)+import Data.Yaml.Config (loadYamlSettings, requireEnv)+import System.Directory (findExecutable)+import System.Process++-- | Typeclass used to pull Rollbar 'Settings' out of a given 'Monad'.+class HasSettings m where+  getSettings :: m Settings++-- | General settings required to interact with Rollbar API.+data Settings = Settings+  { settingsToken :: Token+    -- ^ Rollbar API authentication token.+  , settingsEnvironment :: Environment+    -- ^ Environment to which the revision was deployed.+  , settingsRevision :: Maybe Revision+    -- ^ Git SHA of revision being deployed.+  , settingsRequestModifiers :: RequestModifiers+  } deriving (Eq, Show)++instance FromJSON Settings where+  parseJSON = withObject "Settings" $ \o ->+    Settings <$> o .: "token"+             <*> o .: "environment"+             <*> o .:? "revision" .!= Nothing+             <*> o .:? "request_modifiers" .!= defaultRequestModifiers++-- | Reads 'Settings' from a YAML file.+readSettings :: MonadIO m => FilePath -> m Settings+readSettings path = liftIO $ loadYamlSettings [path] [] requireEnv++newtype Token = Token ByteString+  deriving (Eq, Show)++instance FromJSON Token where+  parseJSON = withText "Token" $ pure . Token . T.encodeUtf8++-- | Environment to which the revision was deployed.+newtype Environment = Environment Text+  deriving (Eq, FromJSON, Show, ToJSON)++-- | Git SHA of revision being deployed.+newtype Revision = Revision Text+  deriving (Eq, FromJSON, Show, ToJSON)++-- | Similar to 'getRevisionMaybe', but it throws a 'RevisionNotFound' if the+-- 'Revision' is not found.+getRevision+  :: (HasSettings m, MonadIO m)+  => m Revision+getRevision = do+  mrevision <- getRevisionMaybe+  case mrevision of+    Nothing -> liftIO $ throwIO RevisionNotFound+    Just revision -> return revision++-- | Gets the 'Revision' from 'Settings' (if the value is present), otherwise+-- gets the 'Revision' from @git@ (if the executable is present) directly+-- by running the following command @git rev-parse HEAD@, if none of them are+-- present (neither the value nor the executable) returns 'Nothing'.+getRevisionMaybe+  :: (HasSettings m, MonadIO m)+  => m (Maybe Revision)+getRevisionMaybe = do+  mrevision <- settingsRevision <$> getSettings+  case mrevision of+    Nothing -> do+      mgitPath <- liftIO $ findExecutable "git"+      forM mgitPath $ \gitPath ->+        mkRevision <$> liftIO (readProcess gitPath ["rev-parse", "HEAD"] "")+    Just revision -> return $ Just revision+  where+    mkRevision = Revision . T.stripEnd . T.pack++-- | Represents a list of 'Request' modifiers that are combined by+-- 'getRequestModifier' into a single function.+data RequestModifiers = RequestModifiers+  { requestModifiersExcludeHeaders :: Maybe (NonEmpty Text)+    -- ^ A list of 'Request' header names to be excluded.+  , requestModifiersExcludeParams :: Maybe (NonEmpty Text)+    -- ^ A list of 'Request' param names to be excluded.+  , requestModifiersIncludeHeaders :: Maybe (NonEmpty Text)+    -- ^ A list of 'Request' header names to be included.+  , requestModifiersIncludeParams :: Maybe (NonEmpty Text)+    -- ^ A list of 'Request' params names to be included.+  } deriving (Eq, Show)++instance FromJSON RequestModifiers where+  parseJSON = withObject "RequestModifiers" $ \o ->+    RequestModifiers <$> o .:? "exclude_headers" .!= Nothing+                     <*> o .:? "exclude_params" .!= Nothing+                     <*> o .:? "include_headers" .!= Nothing+                     <*> o .:? "include_params" .!= Nothing++-- | Returns an empty 'RequestModifiers', the function produced by+-- 'getRequestModifier' given this values is equivalent to 'id'.+defaultRequestModifiers :: RequestModifiers+defaultRequestModifiers = RequestModifiers+  { requestModifiersExcludeHeaders = Nothing+  , requestModifiersExcludeParams = Nothing+  , requestModifiersIncludeHeaders = Nothing+  , requestModifiersIncludeParams = Nothing+  }++data RollbarError = RevisionNotFound+  deriving (Eq, Show)++instance Exception RollbarError
+ test/Rollbar/ClientSpec.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Rollbar.ClientSpec+  ( spec+  ) where++import qualified Data.HashMap.Strict as HM++import Control.Monad.Reader+import Data.Aeson+import Data.Text+import Data.Yaml.Config+import Rollbar.Client+import Test.Hspec++data Package = Package+  { packageName :: Text+  , packageVersion :: Text+  } deriving (Eq, Show)++instance FromJSON Package where+  parseJSON = withObject "Package" $ \o ->+    Package <$> o .: "name"+            <*> o .: "version"++instance HasSettings (Reader Settings) where+  getSettings = ask++spec :: Spec+spec = do+  describe "getRequestModifier" $ do+    let mkSettings modifiers = Settings+          { settingsToken = Token "invalid-token"+          , settingsEnvironment = Environment "test"+          , settingsRevision = Nothing+          , settingsRequestModifiers = modifiers+          }+        request = Request+          { requestUrl = "http://example.com"+          , requestMethod = "GET"+          , requestHeaders = HM.fromList+              [ ("Host", "example.com")+              , ("Secret", "p4ssw0rd")+              ]+          , requestParams = HM.fromList+              [ ("user", "John Doe")+              , ("password", "p4ssw0rd")+              ]+          , requestGet = mempty+          , requestQueryStrings = ""+          , requestPost = mempty+          , requestBody = ""+          , requestUserIp = ""+          }++    it "excludes the headers not matching the given names" $+      let requestModifier = runReader getRequestModifier $ mkSettings $+            defaultRequestModifiers+              { requestModifiersExcludeHeaders = Just $ pure "Secret" }+      in requestModifier request `shouldBe` request+           { requestHeaders = HM.fromList [("Host", "example.com")] }++    it "excludes the params not matching the given names" $+      let requestModifier = runReader getRequestModifier $ mkSettings $+            defaultRequestModifiers+              { requestModifiersExcludeParams = Just $ pure "password" }+      in requestModifier request `shouldBe` request+           { requestParams = HM.fromList [("user", "John Doe")] }++    it "includes only the headers matching the given names" $+      let requestModifier = runReader getRequestModifier $ mkSettings $+            defaultRequestModifiers+              { requestModifiersIncludeHeaders = Just $ pure "Host" }+      in requestModifier request `shouldBe` request+           { requestHeaders = HM.fromList [("Host", "example.com")] }++    it "includes only the params matching the given names" $+      let requestModifier = runReader getRequestModifier $ mkSettings $+            defaultRequestModifiers+              { requestModifiersIncludeParams = Just $ pure "user" }+      in requestModifier request `shouldBe` request+           { requestParams = HM.fromList [("user", "John Doe")] }++  describe "defaultNotifier" $+    it "matches the package name and version" $ do+      Package{..} <- loadYamlSettings ["package.yaml"] [] ignoreEnv+      defaultNotifier `shouldBe` Notifier+        { notifierName = packageName+        , notifierVersion = packageVersion+        }++  before (readSettings "rollbar.yaml") $ do+    describe "ping" $+      it "returns Pong" $ \settings ->+        runRollbar settings ping `shouldReturn` Pong++    describe "createItem" $ do+      context "PayloadTrace" $+        it "returns ItemId" $ \settings -> do+          itemId <- runRollbar settings $ do+            item <- mkItem $ PayloadTrace $ Trace [] $ Exception+              { exceptionClass = "NameError"+              , exceptionMessage = Just "global name 'foo' is not defined"+              , exceptionDescription = Just "Something went wrong while trying to save the user object"+              }+            createItem item++          itemId `shouldSatisfy` const True++      context "PayloadTraceChain" $+        it "returns ItemId" $ \settings -> do+          itemId <- runRollbar settings $ do+            item <- mkItem $ PayloadTraceChain $ pure $ Trace [] $ Exception+              { exceptionClass = "NameError"+              , exceptionMessage = Just "global name 'foo' is not defined"+              , exceptionDescription = Just "Something went wrong while trying to save the user object"+              }+            createItem item++          itemId `shouldSatisfy` const True++      context "PayloadMessage" $+        it "returns ItemId" $ \settings -> do+          itemId <- runRollbar settings $ do+            item <- mkItem $ PayloadMessage $ Message+              { messageBody = "Request over threshold of 10 seconds"+              , messageMetadata = HM.fromList+                  [ ("route", "home#index")+                  , ("time_elapsed", Number 15.23)+                  ]+              }+            createItem item++          itemId `shouldSatisfy` const True++    describe "reportDeploy" $+      it "returns DeployId" $ \settings -> do+        deployId <- runRollbar settings $ do+          deploy <- getRevision >>= mkDeploy+          reportDeploy deploy++        deployId `shouldSatisfy` (> 0)
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}