packages feed

wai-middleware-rollbar 0.2.0 → 0.3.0

raw patch · 11 files changed

+497/−80 lines, 11 filesdep +QuickCheckdep +containersdep +lensdep ~aesondep ~basedep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, containers, lens, lens-aeson, wai-middleware-rollbar

Dependency ranges changed: aeson, base, bytestring, case-insensitive, text

API changes (from Hackage documentation)

- Rollbar.Item: Headers :: RequestHeaders -> Headers
- Rollbar.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Rollbar.Item.Item a)
- Rollbar.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Rollbar.Item.Item a)
- Rollbar.Item: instance GHC.Generics.Generic (Rollbar.Item.Item a)
- Rollbar.Item: instance GHC.Show.Show a => GHC.Show.Show (Rollbar.Item.Item a)
- Rollbar.Item: newtype Headers
- Rollbar.Item.Data: instance Data.Aeson.Types.ToJSON.ToJSON body => Data.Aeson.Types.ToJSON.ToJSON (Rollbar.Item.Data.Data body)
- Rollbar.Item.Data: instance GHC.Classes.Eq body => GHC.Classes.Eq (Rollbar.Item.Data.Data body)
- Rollbar.Item.Data: instance GHC.Generics.Generic (Rollbar.Item.Data.Data body)
- Rollbar.Item.Data: instance GHC.Show.Show body => GHC.Show.Show (Rollbar.Item.Data.Data body)
- Rollbar.Item.Request: Headers :: RequestHeaders -> Headers
- Rollbar.Item.Request: instance Data.Aeson.Types.ToJSON.ToJSON Rollbar.Item.Request.Headers
- Rollbar.Item.Request: instance Data.Aeson.Types.ToJSON.ToJSON Rollbar.Item.Request.Request
- Rollbar.Item.Request: instance GHC.Classes.Eq Rollbar.Item.Request.Headers
- Rollbar.Item.Request: instance GHC.Classes.Eq Rollbar.Item.Request.Request
- Rollbar.Item.Request: instance GHC.Generics.Generic Rollbar.Item.Request.Headers
- Rollbar.Item.Request: instance GHC.Generics.Generic Rollbar.Item.Request.Request
- Rollbar.Item.Request: instance GHC.Show.Show Rollbar.Item.Request.Headers
- Rollbar.Item.Request: instance GHC.Show.Show Rollbar.Item.Request.Request
- Rollbar.Item.Request: newtype Headers
+ Network.Wai.Middleware.Rollbar: Settings :: AccessToken -> Environment -> Settings
+ Network.Wai.Middleware.Rollbar: [accessToken] :: Settings -> AccessToken
+ Network.Wai.Middleware.Rollbar: [environment] :: Settings -> Environment
+ Network.Wai.Middleware.Rollbar: data Settings (headers :: [Symbol])
+ Network.Wai.Middleware.Rollbar: exceptions :: RemoveHeaders headers => Settings headers -> Middleware
+ Rollbar.Item: MissingHeaders :: RequestHeaders -> MissingHeaders
+ Rollbar.Item: instance (Rollbar.Item.MissingHeaders.RemoveHeaders headers, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Rollbar.Item.Item a headers)
+ Rollbar.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Rollbar.Item.Item a headers)
+ Rollbar.Item: instance GHC.Generics.Generic (Rollbar.Item.Item a headers)
+ Rollbar.Item: instance GHC.Show.Show a => GHC.Show.Show (Rollbar.Item.Item a headers)
+ Rollbar.Item: newtype MissingHeaders (headers :: [Symbol])
+ Rollbar.Item.Data: class RemoveHeaders (headers :: [Symbol])
+ Rollbar.Item.Data: instance (Rollbar.Item.MissingHeaders.RemoveHeaders headers, Data.Aeson.Types.ToJSON.ToJSON body) => Data.Aeson.Types.ToJSON.ToJSON (Rollbar.Item.Data.Data body headers)
+ Rollbar.Item.Data: instance GHC.Classes.Eq body => GHC.Classes.Eq (Rollbar.Item.Data.Data body headers)
+ Rollbar.Item.Data: instance GHC.Generics.Generic (Rollbar.Item.Data.Data body headers)
+ Rollbar.Item.Data: instance GHC.Show.Show body => GHC.Show.Show (Rollbar.Item.Data.Data body headers)
+ Rollbar.Item.MissingHeaders: MissingHeaders :: RequestHeaders -> MissingHeaders
+ Rollbar.Item.MissingHeaders: class RemoveHeaders (headers :: [Symbol])
+ Rollbar.Item.MissingHeaders: instance (GHC.TypeLits.KnownSymbol header, Rollbar.Item.MissingHeaders.RemoveHeaders headers) => Rollbar.Item.MissingHeaders.RemoveHeaders (header : headers)
+ Rollbar.Item.MissingHeaders: instance GHC.Classes.Eq (Rollbar.Item.MissingHeaders.MissingHeaders headers)
+ Rollbar.Item.MissingHeaders: instance GHC.Show.Show (Rollbar.Item.MissingHeaders.MissingHeaders headers)
+ Rollbar.Item.MissingHeaders: instance Rollbar.Item.MissingHeaders.RemoveHeaders '[]
+ Rollbar.Item.MissingHeaders: instance Rollbar.Item.MissingHeaders.RemoveHeaders headers => Data.Aeson.Types.ToJSON.ToJSON (Rollbar.Item.MissingHeaders.MissingHeaders headers)
+ Rollbar.Item.MissingHeaders: newtype MissingHeaders (headers :: [Symbol])
+ Rollbar.Item.Request: MissingHeaders :: RequestHeaders -> MissingHeaders
+ Rollbar.Item.Request: class RemoveHeaders (headers :: [Symbol])
+ Rollbar.Item.Request: instance GHC.Classes.Eq (Rollbar.Item.Request.Request headers)
+ Rollbar.Item.Request: instance GHC.Generics.Generic (Rollbar.Item.Request.Request headers)
+ Rollbar.Item.Request: instance GHC.Show.Show (Rollbar.Item.Request.Request headers)
+ Rollbar.Item.Request: instance Rollbar.Item.MissingHeaders.RemoveHeaders headers => Data.Aeson.Types.ToJSON.ToJSON (Rollbar.Item.Request.Request headers)
+ Rollbar.Item.Request: newtype MissingHeaders (headers :: [Symbol])
- Network.Wai.Middleware.Rollbar: requests :: AccessToken -> Environment -> Middleware
+ Network.Wai.Middleware.Rollbar: requests :: RemoveHeaders headers => Settings headers -> Middleware
- Rollbar.Item: Data :: Body body -> CodeVersion -> Maybe Context -> Maybe (HashMap Text Value) -> Environment -> Maybe Fingerprint -> Maybe Framework -> Hardcoded "haskell" -> Level -> Notifier -> Maybe Person -> Platform -> Maybe Request -> Maybe Server -> Maybe UTCTime -> Maybe Title -> Maybe UUID4 -> Data body
+ Rollbar.Item: Data :: Body body -> CodeVersion -> Maybe Context -> Maybe (HashMap Text Value) -> Environment -> Maybe Fingerprint -> Maybe Framework -> Hardcoded "haskell" -> Level -> Notifier -> Maybe Person -> Platform -> Maybe (Request headers) -> Maybe Server -> Maybe UTCTime -> Maybe Title -> Maybe UUID4 -> Data body headers
- Rollbar.Item: Item :: AccessToken -> Data a -> Item a
+ Rollbar.Item: Item :: AccessToken -> Data a headers -> Item a headers
- Rollbar.Item: [accessToken] :: Item a -> AccessToken
+ Rollbar.Item: [accessToken] :: Item a headers -> AccessToken
- Rollbar.Item: [body] :: Data body -> Body body
+ Rollbar.Item: [body] :: Data body headers -> Body body
- Rollbar.Item: [codeVersion] :: Data body -> CodeVersion
+ Rollbar.Item: [codeVersion] :: Data body headers -> CodeVersion
- Rollbar.Item: [context] :: Data body -> Maybe Context
+ Rollbar.Item: [context] :: Data body headers -> Maybe Context
- Rollbar.Item: [custom] :: Data body -> Maybe (HashMap Text Value)
+ Rollbar.Item: [custom] :: Data body headers -> Maybe (HashMap Text Value)
- Rollbar.Item: [environment] :: Data body -> Environment
+ Rollbar.Item: [environment] :: Data body headers -> Environment
- Rollbar.Item: [fingerprint] :: Data body -> Maybe Fingerprint
+ Rollbar.Item: [fingerprint] :: Data body headers -> Maybe Fingerprint
- Rollbar.Item: [framework] :: Data body -> Maybe Framework
+ Rollbar.Item: [framework] :: Data body headers -> Maybe Framework
- Rollbar.Item: [itemData] :: Item a -> Data a
+ Rollbar.Item: [itemData] :: Item a headers -> Data a headers
- Rollbar.Item: [language] :: Data body -> Hardcoded "haskell"
+ Rollbar.Item: [language] :: Data body headers -> Hardcoded "haskell"
- Rollbar.Item: [level] :: Data body -> Level
+ Rollbar.Item: [level] :: Data body headers -> Level
- Rollbar.Item: [notifier] :: Data body -> Notifier
+ Rollbar.Item: [notifier] :: Data body headers -> Notifier
- Rollbar.Item: [person] :: Data body -> Maybe Person
+ Rollbar.Item: [person] :: Data body headers -> Maybe Person
- Rollbar.Item: [platform] :: Data body -> Platform
+ Rollbar.Item: [platform] :: Data body headers -> Platform
- Rollbar.Item: [request] :: Data body -> Maybe Request
+ Rollbar.Item: [request] :: Data body headers -> Maybe (Request headers)
- Rollbar.Item: [server] :: Data body -> Maybe Server
+ Rollbar.Item: [server] :: Data body headers -> Maybe Server
- Rollbar.Item: [timestamp] :: Data body -> Maybe UTCTime
+ Rollbar.Item: [timestamp] :: Data body headers -> Maybe UTCTime
- Rollbar.Item: [title] :: Data body -> Maybe Title
+ Rollbar.Item: [title] :: Data body headers -> Maybe Title
- Rollbar.Item: [uuid] :: Data body -> Maybe UUID4
+ Rollbar.Item: [uuid] :: Data body headers -> Maybe UUID4
- Rollbar.Item: critical :: Environment -> Maybe MessageBody -> payload -> Data payload
+ Rollbar.Item: critical :: Environment -> Maybe MessageBody -> payload -> Data payload ("Authorization" : headers)
- Rollbar.Item: data Data body
+ Rollbar.Item: data Data body headers
- Rollbar.Item: data Item a
+ Rollbar.Item: data Item a headers
- Rollbar.Item: debug :: Environment -> Maybe MessageBody -> payload -> Data payload
+ Rollbar.Item: debug :: Environment -> Maybe MessageBody -> payload -> Data payload ("Authorization" : headers)
- Rollbar.Item: error :: Environment -> Maybe MessageBody -> payload -> Data payload
+ Rollbar.Item: error :: Environment -> Maybe MessageBody -> payload -> Data payload ("Authorization" : headers)
- Rollbar.Item: info :: Environment -> Maybe MessageBody -> payload -> Data payload
+ Rollbar.Item: info :: Environment -> Maybe MessageBody -> payload -> Data payload ("Authorization" : headers)
- Rollbar.Item: warning :: Environment -> Maybe MessageBody -> payload -> Data payload
+ Rollbar.Item: warning :: Environment -> Maybe MessageBody -> payload -> Data payload ("Authorization" : headers)
- Rollbar.Item.Data: Data :: Body body -> CodeVersion -> Maybe Context -> Maybe (HashMap Text Value) -> Environment -> Maybe Fingerprint -> Maybe Framework -> Hardcoded "haskell" -> Level -> Notifier -> Maybe Person -> Platform -> Maybe Request -> Maybe Server -> Maybe UTCTime -> Maybe Title -> Maybe UUID4 -> Data body
+ Rollbar.Item.Data: Data :: Body body -> CodeVersion -> Maybe Context -> Maybe (HashMap Text Value) -> Environment -> Maybe Fingerprint -> Maybe Framework -> Hardcoded "haskell" -> Level -> Notifier -> Maybe Person -> Platform -> Maybe (Request headers) -> Maybe Server -> Maybe UTCTime -> Maybe Title -> Maybe UUID4 -> Data body headers
- Rollbar.Item.Data: [body] :: Data body -> Body body
+ Rollbar.Item.Data: [body] :: Data body headers -> Body body
- Rollbar.Item.Data: [codeVersion] :: Data body -> CodeVersion
+ Rollbar.Item.Data: [codeVersion] :: Data body headers -> CodeVersion
- Rollbar.Item.Data: [context] :: Data body -> Maybe Context
+ Rollbar.Item.Data: [context] :: Data body headers -> Maybe Context
- Rollbar.Item.Data: [custom] :: Data body -> Maybe (HashMap Text Value)
+ Rollbar.Item.Data: [custom] :: Data body headers -> Maybe (HashMap Text Value)
- Rollbar.Item.Data: [environment] :: Data body -> Environment
+ Rollbar.Item.Data: [environment] :: Data body headers -> Environment
- Rollbar.Item.Data: [fingerprint] :: Data body -> Maybe Fingerprint
+ Rollbar.Item.Data: [fingerprint] :: Data body headers -> Maybe Fingerprint
- Rollbar.Item.Data: [framework] :: Data body -> Maybe Framework
+ Rollbar.Item.Data: [framework] :: Data body headers -> Maybe Framework
- Rollbar.Item.Data: [language] :: Data body -> Hardcoded "haskell"
+ Rollbar.Item.Data: [language] :: Data body headers -> Hardcoded "haskell"
- Rollbar.Item.Data: [level] :: Data body -> Level
+ Rollbar.Item.Data: [level] :: Data body headers -> Level
- Rollbar.Item.Data: [notifier] :: Data body -> Notifier
+ Rollbar.Item.Data: [notifier] :: Data body headers -> Notifier
- Rollbar.Item.Data: [person] :: Data body -> Maybe Person
+ Rollbar.Item.Data: [person] :: Data body headers -> Maybe Person
- Rollbar.Item.Data: [platform] :: Data body -> Platform
+ Rollbar.Item.Data: [platform] :: Data body headers -> Platform
- Rollbar.Item.Data: [request] :: Data body -> Maybe Request
+ Rollbar.Item.Data: [request] :: Data body headers -> Maybe (Request headers)
- Rollbar.Item.Data: [server] :: Data body -> Maybe Server
+ Rollbar.Item.Data: [server] :: Data body headers -> Maybe Server
- Rollbar.Item.Data: [timestamp] :: Data body -> Maybe UTCTime
+ Rollbar.Item.Data: [timestamp] :: Data body headers -> Maybe UTCTime
- Rollbar.Item.Data: [title] :: Data body -> Maybe Title
+ Rollbar.Item.Data: [title] :: Data body headers -> Maybe Title
- Rollbar.Item.Data: [uuid] :: Data body -> Maybe UUID4
+ Rollbar.Item.Data: [uuid] :: Data body headers -> Maybe UUID4
- Rollbar.Item.Data: data Data body
+ Rollbar.Item.Data: data Data body headers
- Rollbar.Item.Request: Request :: RawBody -> Get -> Headers -> Method -> QueryString -> URL -> IP -> Request
+ Rollbar.Item.Request: Request :: RawBody -> Get -> MissingHeaders headers -> Method -> QueryString -> URL -> IP -> Request headers
- Rollbar.Item.Request: [get] :: Request -> Get
+ Rollbar.Item.Request: [get] :: Request headers -> Get
- Rollbar.Item.Request: [headers] :: Request -> Headers
+ Rollbar.Item.Request: [headers] :: Request headers -> MissingHeaders headers
- Rollbar.Item.Request: [method] :: Request -> Method
+ Rollbar.Item.Request: [method] :: Request headers -> Method
- Rollbar.Item.Request: [queryString] :: Request -> QueryString
+ Rollbar.Item.Request: [queryString] :: Request headers -> QueryString
- Rollbar.Item.Request: [rawBody] :: Request -> RawBody
+ Rollbar.Item.Request: [rawBody] :: Request headers -> RawBody
- Rollbar.Item.Request: [url] :: Request -> URL
+ Rollbar.Item.Request: [url] :: Request headers -> URL
- Rollbar.Item.Request: [userIP] :: Request -> IP
+ Rollbar.Item.Request: [userIP] :: Request headers -> IP
- Rollbar.Item.Request: data Request
+ Rollbar.Item.Request: data Request headers

Files

src/Network/Wai/Middleware/Rollbar.hs view
@@ -1,7 +1,10 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE KindSignatures #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}  {-|     Module      : Network.Wai.Middleware.Rollbar@@ -17,10 +20,11 @@     More to come shortly. -} -module Network.Wai.Middleware.Rollbar (requests) where+module Network.Wai.Middleware.Rollbar (Settings(..), exceptions, requests) where  import Control.Concurrent (forkIO)-import Control.Exception  (Handler(Handler), catches, displayException)+import Control.Exception+    (Handler(Handler), SomeException, catches, displayException, throwIO) import Control.Monad      (when)  import Data.Aeson   (ToJSON, defaultOptions, genericToEncoding, toEncoding)@@ -30,6 +34,7 @@ import Data.UUID.V4 (nextRandom)  import GHC.Generics (Generic)+import GHC.TypeLits (Symbol)  import Network.HostName          (getHostName) import Network.HTTP.Client       (HttpException)@@ -53,64 +58,80 @@ import System.Environment (getExecutablePath) import System.IO          (hPutStrLn, stderr) +import qualified Data.ByteString    as BS import qualified Data.Text          as T import qualified Data.Text.Encoding as TE import qualified Network.Wai        as NW import qualified Rollbar.Item       as RI +-- | Set up the middleware properly+--  The `headers` are  what you want removed from+--  the request headers sent to Rollbar.+data Settings (headers :: [Symbol])+    = Settings+        { accessToken :: RI.AccessToken+        -- ^ Should have a scope "post_server_item".+        , environment :: RI.Environment+        -- ^ Should be something meaningful to your program+        --+        -- E.g. "development" or "production"+        }++-- | Middleware that catches exceptions, sends an item to Rollbar,+--  and rethrows the exception.+--+--  Sends additional metadata including the request information.+exceptions :: RI.RemoveHeaders headers => Settings headers -> Middleware+exceptions settings app req handler = app req handler `catches`+    [ Handler $ handleSomeException settings req+    ]+ -- | Middleware that watches responses --  and sends an item to Rollbar if it is a server error (5xx). -- --  Sends additional metadata including the request information.-requests :: RI.AccessToken -> RI.Environment -> Middleware-requests accessToken environment app req handler' = app req handler+requests :: RI.RemoveHeaders headers => Settings headers -> Middleware+requests settings app req handler' = app req handler     where     handler :: NW.Response -> IO ResponseReceived     handler res = do-        _ <- forkIO $ handle500 accessToken environment req res+        _ <- forkIO $ handle500 settings req res         handler' res -handle500 :: RI.AccessToken -> RI.Environment -> NW.Request -> NW.Response -> IO ()-handle500 accessToken environment req res =-    when (statusIsServerError $ NW.responseStatus res) (send accessToken environment req res)+handle500+    :: RI.RemoveHeaders headers+    => Settings headers+    -> NW.Request+    -> NW.Response+    -> IO ()+handle500 settings req res =+    when (statusIsServerError $ NW.responseStatus res) (send settings req res)         `catches`             [ Handler handleHttpException             , Handler handleJSONException             ] -send :: RI.AccessToken -> RI.Environment -> NW.Request -> NW.Response -> IO ()-send accessToken environment req res = do-    uuid <- Just . RI.UUID4 <$> nextRandom-    timestamp <- Just <$> getCurrentTime-    host <- Just <$> getHostName-    root <- Just . RI.Root . T.pack <$> getExecutablePath-    let request = Just RI.Request {..}-    let server = Just RI.Server { RI.branch = Nothing, RI.serverCodeVersion = Nothing, .. }-    let itemData = (RI.error environment messageBody payload)-            { RI.request, RI.server, RI.timestamp, RI.uuid }-    let rReq = rollbarRequest RI.Item{..}+send+    :: forall headers+    . RI.RemoveHeaders headers+    => Settings headers+    -> NW.Request+    -> NW.Response+    -> IO ()+send settings req res = do+    rReq <- mkRollbarRequest settings req payload messageBody     void $ httpNoBody rReq     where     Status{..} = NW.responseStatus res-    headers = RI.Headers $ NW.requestHeaders req     messageBody = RI.MessageBody <$> myDecodeUtf8 statusMessage-    rawBody = ""-    get = RI.Get $ NW.queryString req-    method = RI.Method $ NW.requestMethod req-    queryString = RI.QueryString $ NW.rawQueryString req-    url = RI.URL (NW.requestHeaderHost req, NW.pathInfo req)-    userIP = RI.IP $ NW.remoteHost req     referer = myDecodeUtf8 =<< NW.requestHeaderReferer req     range = myDecodeUtf8 =<< NW.requestHeaderRange req     userAgent = myDecodeUtf8 =<< NW.requestHeaderUserAgent req-    payload = Payload+    payload = RequestPayload         { statusMessage = myDecodeUtf8' statusMessage         , ..         } -    myDecodeUtf8 = either (const Nothing) Just . TE.decodeUtf8'-    myDecodeUtf8' = fromMaybe "" . myDecodeUtf8- handleHttpException :: HttpException -> IO () handleHttpException e = do     hPutStrLn stderr "Ran into an exception while sending a request to Rollbar:"@@ -121,7 +142,68 @@     hPutStrLn stderr "Ran into an exception while parsing JSON response from Rollbar:"     hPutStrLn stderr $ displayException e -rollbarRequest :: ToJSON a => RI.Item a -> Network.HTTP.Simple.Request+handleSomeException+    :: forall a headers+    . RI.RemoveHeaders headers+    => Settings headers+    -> NW.Request+    -> SomeException+    -> IO a+handleSomeException settings req e = do+    rReq <- mkRollbarRequest settings req payload messageBody+    _ <- httpNoBody rReq+    throwIO e+    where+    exception = T.pack $ displayException e+    messageBody = Just $ RI.MessageBody $+        T.intercalate " "+            [ "Uncaught exception at:"+            , myDecodeUtf8' $ NW.requestMethod req+            , T.intercalate "/" $ NW.pathInfo req+            ]+    referer = myDecodeUtf8 =<< NW.requestHeaderReferer req+    range = myDecodeUtf8 =<< NW.requestHeaderRange req+    userAgent = myDecodeUtf8 =<< NW.requestHeaderUserAgent req+    payload = ExceptionPayload {..}++mkRollbarRequest+    :: forall headers+    . RI.RemoveHeaders headers+    => Settings headers+    -> NW.Request+    -> Payload+    -> Maybe RI.MessageBody+    -> IO Request+mkRollbarRequest Settings{..} req payload messageBody = do+    uuid <- Just . RI.UUID4 <$> nextRandom+    timestamp <- Just <$> getCurrentTime+    host <- Just <$> getHostName+    root <- Just . RI.Root . T.pack <$> getExecutablePath+    let request = Just RI.Request {..}+    let server = Just RI.Server { RI.branch = Nothing, RI.serverCodeVersion = Nothing, .. }+    let itemData = (RI.error environment messageBody payload)+            { RI.request, RI.server, RI.timestamp, RI.uuid }+    pure $ rollbarRequest RI.Item{..}+    where+    headers :: RI.MissingHeaders headers+    headers = RI.MissingHeaders $ NW.requestHeaders req+    rawBody = ""+    get = RI.Get $ NW.queryString req+    method = RI.Method $ NW.requestMethod req+    queryString = RI.QueryString $ NW.rawQueryString req+    url = RI.URL (NW.requestHeaderHost req, NW.pathInfo req)+    userIP = RI.IP $ NW.remoteHost req++myDecodeUtf8 :: BS.ByteString -> Maybe T.Text+myDecodeUtf8 = either (const Nothing) Just . TE.decodeUtf8'++myDecodeUtf8' :: BS.ByteString -> T.Text+myDecodeUtf8' = fromMaybe "" . myDecodeUtf8++rollbarRequest+    :: (RI.RemoveHeaders headers, ToJSON a)+    => RI.Item a headers+    -> Network.HTTP.Simple.Request rollbarRequest payload =     setRequestMethod "POST"     . setRequestSecure True@@ -133,12 +215,18 @@     $ defaultRequest  data Payload-    = Payload+    = RequestPayload         { statusCode    :: Int         , statusMessage :: T.Text         , userAgent     :: Maybe T.Text         , range         :: Maybe T.Text         , referer       :: Maybe T.Text+        }+    | ExceptionPayload+        { exception :: T.Text+        , userAgent :: Maybe T.Text+        , range     :: Maybe T.Text+        , referer   :: Maybe T.Text         }     deriving (Generic, Show) 
src/Rollbar/Item.hs view
@@ -1,6 +1,8 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeOperators #-}  {-|     Module      : Rollbar.Item@@ -59,6 +61,9 @@     , module Rollbar.Item.Hardcoded     , Hardcoded(..) +    , module Rollbar.Item.MissingHeaders+    , MissingHeaders(..)+     , module Rollbar.Item.Person     , Person(..)     , Email(..)@@ -67,9 +72,9 @@      , module Rollbar.Item.Request     , Get(..)-    , Headers(..)     , IP(..)     , Method(..)+    , MissingHeaders(..)     , QueryString(..)     , RawBody(..)     , URL(..)@@ -95,6 +100,7 @@ import Rollbar.Item.Environment import Rollbar.Item.Hardcoded import Rollbar.Item.Level+import Rollbar.Item.MissingHeaders import Rollbar.Item.Person import Rollbar.Item.Request import Rollbar.Item.Server@@ -108,7 +114,11 @@ import qualified Paths_wai_middleware_rollbar  -- | Creates 'Data' with the level set to 'Debug'.-debug :: Environment -> Maybe MessageBody -> payload -> Data payload+debug+    :: Environment+    -> Maybe MessageBody+    -> payload+    -> Data payload ("Authorization" ': headers) debug environment messageBody payload =     Data         { body = Message (fromMaybe "" messageBody) payload@@ -132,40 +142,59 @@         }  -- | Creates 'Data' with the level set to 'Info'.-info :: Environment -> Maybe MessageBody -> payload -> Data payload+info+    :: Environment+    -> Maybe MessageBody+    -> payload+    -> Data payload ("Authorization" ': headers) info environment messageBody payload =     (debug environment messageBody payload) { level = Info }  -- | Creates 'Data' with the level set to 'Warning'.-warning :: Environment -> Maybe MessageBody -> payload -> Data payload+warning+    :: Environment+    -> Maybe MessageBody+    -> payload+    -> Data payload ("Authorization" ': headers) warning environment messageBody payload =     (debug environment messageBody payload) { level = Warning }  -- | Creates 'Data' with the level set to 'Error'.-error :: Environment -> Maybe MessageBody -> payload -> Data payload+error+    :: Environment+    -> Maybe MessageBody+    -> payload+    -> Data payload ("Authorization" ': headers) error environment messageBody payload =     (debug environment messageBody payload) { level = Error }  -- | Creates 'Data' with the level set to 'Critical'.-critical :: Environment -> Maybe MessageBody -> payload -> Data payload+critical+    :: Environment+    -> Maybe MessageBody+    -> payload+    -> Data payload ("Authorization" ': headers) critical environment messageBody payload =     (debug environment messageBody payload) { level = Critical }  -- | The thing we actually give to Rollbar.-data Item a+data Item a headers     = Item         { accessToken :: AccessToken         -- ^ Should have a scope "post_server_item".-        , itemData    :: Data a+        , itemData    :: Data a headers         }     deriving (Eq, Generic, Show) -itemKVs :: (KeyValue kv, ToJSON v) => Item v -> [kv]+itemKVs+    :: (KeyValue kv, RemoveHeaders headers, ToJSON v)+    => Item v headers+    -> [kv] itemKVs Item{..} =     [ "access_token" .= accessToken     , "data" .= itemData     ] -instance ToJSON a => ToJSON (Item a) where+instance (RemoveHeaders headers, ToJSON a) => ToJSON (Item a headers) where     toJSON = object . itemKVs     toEncoding = pairs . mconcat . itemKVs
src/Rollbar/Item/Body.hs view
@@ -19,8 +19,10 @@     , MessageBody(..)     ) where -import Data.Aeson  (KeyValue, ToJSON, object, pairs, toEncoding, toJSON, (.=))-import Data.String (IsString)+import Data.Aeson+    (KeyValue, ToJSON, object, pairs, toEncoding, toJSON, (.=))+import Data.Aeson.Encoding (pair)+import Data.String         (IsString)  import GHC.Generics (Generic) @@ -45,8 +47,8 @@     ]  instance ToJSON arbitrary => ToJSON (Body arbitrary) where-    toJSON = object . bodyKVs-    toEncoding = pairs . mconcat . bodyKVs+    toJSON x = object ["message" .= object (bodyKVs x)]+    toEncoding = pairs . pair "message" . pairs . mconcat . bodyKVs  -- | The primary message text to send to Rollbar. newtype MessageBody
src/Rollbar/Item/Data.hs view
@@ -22,6 +22,8 @@     , Framework(..)     , Title(..)     , UUID4(..)++    , RemoveHeaders     ) where  import Data.Aeson@@ -46,7 +48,7 @@ import Rollbar.Item.Hardcoded   (Hardcoded) import Rollbar.Item.Level       (Level) import Rollbar.Item.Person      (Person)-import Rollbar.Item.Request     (Request)+import Rollbar.Item.Request     (RemoveHeaders, Request) import Rollbar.Item.Server      (Server)  import Rollbar.Item.Internal.Notifier (Notifier)@@ -60,7 +62,7 @@ -- --  N.B. While it's entirely possible for you to create one of these yourself, --  it's usually easier to use helpers like 'info' and 'error'.-data Data body+data Data body headers     = Data         { body        :: Body body         , codeVersion :: CodeVersion@@ -77,7 +79,7 @@         , person      :: Maybe Person         , platform    :: Platform         -- ^ Metadata about this package. You probably shouldn't create this yourself.-        , request     :: Maybe Request+        , request     :: Maybe (Request headers)         , server      :: Maybe Server         , timestamp   :: Maybe UTCTime         , title       :: Maybe Title@@ -85,7 +87,7 @@         }     deriving (Eq, Generic, Show) -instance ToJSON body => ToJSON (Data body) where+instance (RemoveHeaders headers, ToJSON body) => ToJSON (Data body headers) where     toJSON = genericToJSON defaultOptions         { fieldLabelModifier = codeVersionModifier         , omitNothingFields = True
+ src/Rollbar/Item/MissingHeaders.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeOperators #-}++{-|+    Module      : Rollbar.Item.MissingHeaders+    Description : Headers with some missing from the JSON instance.+    Copyright   : (c) Hardy Jones, 2017+    License     : BSD3+    Maintainer  : jones3.hardy@gmail.com+    Stability   : experimental+-}++module Rollbar.Item.MissingHeaders+    ( MissingHeaders(..)+    , RemoveHeaders+    ) where++import Data.Aeson           (KeyValue, ToJSON, object, toJSON, (.=))+import Data.CaseInsensitive (mk, original)+import Data.Maybe           (catMaybes)+import Data.Proxy           (Proxy(Proxy))++import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)++import Network.HTTP.Types (Header, RequestHeaders)++import qualified Data.ByteString       as BS+import qualified Data.ByteString.Char8 as BSC8+import qualified Data.Text             as T+import qualified Data.Text.Encoding    as TE++-- | The request headers with some missing+--+--  This is useful for removing sensitive information+--  like the `Authorization` header.+newtype MissingHeaders (headers :: [Symbol])+    = MissingHeaders RequestHeaders+    deriving (Eq, Show)++-- | Remove the headers given from the underlying request headers.+class RemoveHeaders (headers :: [Symbol]) where+    removeHeaders :: MissingHeaders headers -> RequestHeaders++instance RemoveHeaders '[] where+    removeHeaders (MissingHeaders rhs) = rhs++instance (KnownSymbol header, RemoveHeaders headers)+    => RemoveHeaders (header ': headers) where+    removeHeaders (MissingHeaders rhs) =+        removeHeaders (MissingHeaders $ filter go rhs :: MissingHeaders headers)+        where+        go (rh, _) =+            rh /= (mk . BSC8.pack $ symbolVal (Proxy :: Proxy header))++instance RemoveHeaders headers => ToJSON (MissingHeaders headers) where+    toJSON = object . catMaybes . requestHeadersKVs . removeHeaders++requestHeadersKVs :: forall kv. KeyValue kv => RequestHeaders -> [Maybe kv]+requestHeadersKVs = fmap go+    where+    go :: Header -> Maybe kv+    go (key', val') = do+        key <- myDecodeUtf8 $ original key'+        val <- myDecodeUtf8 val'+        pure (key .= val)++myDecodeUtf8 :: BS.ByteString -> Maybe T.Text+myDecodeUtf8 = either (const Nothing) Just . TE.decodeUtf8'
src/Rollbar/Item/Request.hs view
@@ -16,36 +16,37 @@ module Rollbar.Item.Request     ( Request(..)     , Get(..)-    , Headers(..)     , IP(..)     , Method(..)+    , MissingHeaders(..)     , QueryString(..)     , RawBody(..)     , URL(..)+    , RemoveHeaders     ) where -import Data.Aeson-    (KeyValue, ToJSON, object, pairs, toEncoding, toJSON, (.=))-import Data.CaseInsensitive (original)-import Data.Maybe           (catMaybes, fromMaybe)-import Data.String          (IsString)+import Data.Aeson  (KeyValue, ToJSON, object, pairs, toEncoding, toJSON, (.=))+import Data.Maybe  (catMaybes, fromMaybe)+import Data.String (IsString)  import GHC.Generics (Generic) -import Network.HTTP.Types (Header, Query, RequestHeaders)+import Network.HTTP.Types (Query) import Network.Socket     (SockAddr) +import Rollbar.Item.MissingHeaders+ import qualified Data.ByteString    as BS import qualified Data.Text          as T import qualified Data.Text.Encoding as TE  -- | Data sent to the server-data Request+data Request headers     = Request         { rawBody     :: RawBody         , get         :: Get         -- ^ Query parameters-        , headers     :: Headers+        , headers     :: MissingHeaders headers         , method      :: Method         , queryString :: QueryString         -- ^ The entire query string@@ -82,24 +83,6 @@         let val = val' >>= myDecodeUtf8         pure (key .= val) --- | The request headers-newtype Headers-    = Headers RequestHeaders-    deriving (Eq, Generic, Show)--instance ToJSON Headers where-    toJSON (Headers hs) = object . catMaybes . requestHeadersKVs $ hs-    toEncoding (Headers hs) = pairs . mconcat . catMaybes . requestHeadersKVs $ hs--requestHeadersKVs :: forall kv. KeyValue kv => RequestHeaders -> [Maybe kv]-requestHeadersKVs = fmap go-    where-    go :: Header -> Maybe kv-    go (key', val') = do-        key <- myDecodeUtf8 $ original key'-        val <- myDecodeUtf8 val'-        pure (T.pack (show key) .= val)- -- | The HTTP Verb newtype Method     = Method BS.ByteString@@ -127,7 +110,7 @@     toJSON (IP ip) = toJSON (show ip)     toEncoding (IP ip) = toEncoding (show ip) -requestKVs :: KeyValue kv => Request -> [kv]+requestKVs :: (KeyValue kv, RemoveHeaders headers) => Request headers -> [kv] requestKVs Request{..} =     [ "body" .= rawBody     , "GET" .= get@@ -138,7 +121,7 @@     , "user_ip" .= userIP     ] -instance ToJSON Request where+instance (RemoveHeaders headers) => ToJSON (Request headers) where     toJSON = object . requestKVs     toEncoding = pairs . mconcat . requestKVs 
+ test/Main.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Test.QuickCheck (conjoin, quickCheck)++import qualified Rollbar.Item.Data.Test+import qualified Rollbar.Item.MissingHeaders.Test+import qualified Rollbar.Item.Request.Test++main :: IO ()+main = do+    Rollbar.Item.Data.Test.props+    Rollbar.Item.MissingHeaders.Test.props+    Rollbar.Item.Request.Test.props
+ test/Rollbar/Item/Data/Test.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+module Rollbar.Item.Data.Test where++import Control.Lens ((^@..))++import Data.Aeson      (encode, toJSON)+import Data.Aeson.Lens (key, members)+import Data.Text       (Text, pack)++import Prelude hiding (error)++import Rollbar.Item++import Test.QuickCheck+    (Arbitrary, Property, arbitrary, conjoin, elements, quickCheck)++props :: IO ()+props =+    quickCheck $ conjoin+        [ prop_valueDataBodyHasRequiredKey+        , prop_encodingDataBodyHasRequiredKey+        ]++prop_valueDataBodyHasRequiredKey :: Data () '["Authorization"] -> Bool+prop_valueDataBodyHasRequiredKey x =+    length ms == 1 && fst (head ms) `elem` requiredBodyKeys+    where+    ms = toJSON x ^@.. key "body" . members++prop_encodingDataBodyHasRequiredKey :: Data () '["Authorization"] -> Bool+prop_encodingDataBodyHasRequiredKey x =+    length ms == 1 && fst (head ms) `elem` requiredBodyKeys+    where+    ms = encode x ^@.. key "body" . members++instance Arbitrary a => Arbitrary (Data a '["Authorization"]) where+    arbitrary = do+        env <- Environment . pack <$> arbitrary+        message <- fmap (MessageBody . pack) <$> arbitrary+        payload <- arbitrary+        elements $ (\f -> f env message payload) <$> datas++datas :: [Environment -> Maybe MessageBody -> a -> Data a '["Authorization"]]+datas = [debug, info, warning, error, critical]++requiredBodyKeys :: [Text]+requiredBodyKeys = ["trace", "trace_chain", "message", "crash_report"]
+ test/Rollbar/Item/MissingHeaders/Test.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeOperators #-}+module Rollbar.Item.MissingHeaders.Test where++import Control.Lens ((&), (^@..))++import Data.Aeson           (encode, toJSON)+import Data.Aeson.Lens      (key, members)+import Data.Bifunctor       (bimap)+import Data.CaseInsensitive (mk, original)+import Data.Proxy           (Proxy(Proxy))+import Data.Text            (Text, pack)++import GHC.TypeLits (KnownSymbol, symbolVal)++import Prelude hiding (error)++import Rollbar.Item.MissingHeaders (MissingHeaders(..))++import Test.QuickCheck+    (Arbitrary, Gen, Property, arbitrary, conjoin, elements, quickCheck)++import Data.ByteString.Char8 as BSC8+import Data.Set              as S+import Data.Text.Encoding    as TE++props :: IO ()+props = do+    quickCheck $ conjoin+        [ prop_valueAuthorizationIsRemoved+        , prop_encodingAuthorizationIsRemoved+        ]++    quickCheck $ conjoin+        [ prop_valueX_AccessTokenIsRemoved+        , prop_encodingX_AccessTokenIsRemoved+        ]++    quickCheck $ conjoin+        [ prop_valueAllHeadersAreRemoved+        , prop_encodingAllHeadersAreRemoved+        ]++prop_valueAuthorizationIsRemoved :: MissingHeaders '["Authorization"] -> Bool+prop_valueAuthorizationIsRemoved hs@(MissingHeaders rhs) =+    "Authorization" `S.notMember` actual+    where+    actual = toJSON hs ^@.. members & fmap fst & S.fromList++prop_encodingAuthorizationIsRemoved :: MissingHeaders '["Authorization"] -> Bool+prop_encodingAuthorizationIsRemoved hs@(MissingHeaders rhs) =+    "Authorization" `S.notMember` actual+    where+    actual = encode hs ^@.. members & fmap fst & S.fromList++prop_valueX_AccessTokenIsRemoved :: MissingHeaders '["X-AccessToken"] -> Bool+prop_valueX_AccessTokenIsRemoved hs@(MissingHeaders rhs) =+    "X-AccessToken" `S.notMember` actual+    where+    actual = toJSON hs ^@.. members & fmap fst & S.fromList++prop_encodingX_AccessTokenIsRemoved :: MissingHeaders '["X-AccessToken"] -> Bool+prop_encodingX_AccessTokenIsRemoved hs@(MissingHeaders rhs) =+    "X-AccessToken" `S.notMember` actual+    where+    actual = encode hs ^@.. members & fmap fst & S.fromList++prop_valueAllHeadersAreRemoved+    :: MissingHeaders+        '["Authorization", "this is made up", "Server", "X-AccessToken"]+    -> Bool+prop_valueAllHeadersAreRemoved hs@(MissingHeaders rhs) =+    "Authorization" `S.notMember` actual+        && "this is made up" `S.notMember` actual+        && "Server" `S.notMember` actual+        && "X-AccessToken" `S.notMember` actual+    where+    actual = toJSON hs ^@.. members & fmap fst & S.fromList++prop_encodingAllHeadersAreRemoved+    :: MissingHeaders+        '["Authorization", "this is made up", "Server", "X-AccessToken"]+    -> Bool+prop_encodingAllHeadersAreRemoved hs@(MissingHeaders rhs) =+    "Authorization" `S.notMember` actual+        && "this is made up" `S.notMember` actual+        && "Server" `S.notMember` actual+        && "X-AccessToken" `S.notMember` actual+    where+    actual = encode hs ^@.. members & fmap fst & S.fromList++instance Arbitrary (MissingHeaders '[]) where+    arbitrary = do+        xs <- arbitrary+        let hs = bimap (mk . BSC8.pack) BSC8.pack <$> xs+        pure . MissingHeaders $ hs++instance (KnownSymbol header, Arbitrary (MissingHeaders headers))+    => Arbitrary (MissingHeaders (header ': headers)) where+    arbitrary = do+        MissingHeaders hs <- arbitrary :: Gen (MissingHeaders headers)+        let name = mk . BSC8.pack $ symbolVal (Proxy :: Proxy header)+        value <- BSC8.pack <$> arbitrary+        pure . MissingHeaders $ (name, value) : hs
+ test/Rollbar/Item/Request/Test.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeOperators #-}+module Rollbar.Item.Request.Test where++import Control.Lens ((&), (^@..))++import Data.Aeson           (encode, toJSON)+import Data.Aeson.Lens      (key, members)+import Data.Bifunctor       (bimap)+import Data.CaseInsensitive (mk, original)+import Data.Text            (Text, pack)++import Prelude hiding (error)++import Rollbar.Item.Request (MissingHeaders(..), RemoveHeaders)++import Test.QuickCheck+    (Arbitrary, Property, arbitrary, conjoin, elements, quickCheck)++import Data.ByteString.Char8 as BSC8+import Data.Set              as S+import Data.Text.Encoding    as TE++props :: IO ()+props =+    quickCheck $ conjoin+        [ prop_valueHeadersArentWrapped+        , prop_encodingHeadersArentWrapped+        ]++prop_valueHeadersArentWrapped :: MissingHeaders '["Authorization"] -> Bool+prop_valueHeadersArentWrapped hs@(MissingHeaders rhs) =+    actual `S.isSubsetOf` expected+    where+    actual = toJSON hs ^@.. members & fmap fst & S.fromList+    expected = S.fromList $ either (const "") id . TE.decodeUtf8' . original . fst <$> rhs++prop_encodingHeadersArentWrapped :: MissingHeaders '["Authorization"] -> Bool+prop_encodingHeadersArentWrapped hs@(MissingHeaders rhs) =+    actual `S.isSubsetOf` expected+    where+    actual = encode hs ^@.. members & fmap fst & S.fromList+    expected = S.fromList $ either (const "") id . TE.decodeUtf8' . original . fst <$> rhs++instance Arbitrary (MissingHeaders ("Authorization" ': headers)) where+    arbitrary = do+        xs <- arbitrary+        pure . MissingHeaders $ bimap (mk . BSC8.pack) BSC8.pack <$> xs
wai-middleware-rollbar.cabal view
@@ -1,5 +1,5 @@ name:                wai-middleware-rollbar-version:             0.2.0+version:             0.3.0 synopsis:            Middleware that communicates to Rollbar. description:         Middleware that communicates to Rollbar. homepage:            https://github.com/joneshf/wai-middleware-rollbar#readme@@ -25,6 +25,7 @@                      , Rollbar.Item.Environment                      , Rollbar.Item.Hardcoded                      , Rollbar.Item.Level+                     , Rollbar.Item.MissingHeaders                      , Rollbar.Item.Person                      , Rollbar.Item.Request                      , Rollbar.Item.Server@@ -48,6 +49,26 @@                      , wai >= 3.2 && < 3.3   default-language:    Haskell2010   ghc-options:        -Wall++test-suite doc-test+  default-language:    Haskell2010+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Main.hs+  other-modules:       Rollbar.Item.Data.Test+                     , Rollbar.Item.MissingHeaders.Test+                     , Rollbar.Item.Request.Test+  build-depends:       base+                     , aeson+                     , bytestring+                     , case-insensitive+                     , containers >= 0.5 && < 0.6+                     , lens >= 4.15 && < 4.16+                     , lens-aeson >= 1.0 && < 1.1+                     , QuickCheck >= 2.9 && < 2.10+                     , text+                     , wai-middleware-rollbar+  default-language:    Haskell2010  source-repository head   type:     git