packages feed

bugsnag-haskell 0.0.1.3 → 0.0.2.0

raw patch · 12 files changed

+358/−78 lines, 12 filesdep +Globdep +containersPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: Glob, containers

API changes (from Hackage documentation)

- Network.Bugsnag.Event: [beExceptions] :: BugsnagEvent -> NonEmpty BugsnagException
- Network.Bugsnag.Event: instance GHC.Generics.Generic Network.Bugsnag.Event.BugsnagEvent
+ Network.Bugsnag.BeforeNotify: setStackFramesCode :: CodeIndex -> BeforeNotify
+ Network.Bugsnag.BeforeNotify: updateEventFromException :: (BugsnagException -> BeforeNotify) -> BeforeNotify
+ Network.Bugsnag.BeforeNotify: updateEventFromOriginalException :: Exception e => (e -> BeforeNotify) -> BeforeNotify
+ Network.Bugsnag.BeforeNotify: updateStackFrames :: (BugsnagStackFrame -> BugsnagStackFrame) -> BeforeNotify
+ Network.Bugsnag.CodeIndex: buildCodeIndex :: String -> Q Exp
+ Network.Bugsnag.CodeIndex: data CodeIndex
+ Network.Bugsnag.CodeIndex: findSourceRange :: FilePath -> (Natural, Natural) -> CodeIndex -> Maybe [(Natural, Text)]
+ Network.Bugsnag.CodeIndex: instance GHC.Show.Show Network.Bugsnag.CodeIndex.CodeIndex
+ Network.Bugsnag.CodeIndex: instance GHC.Show.Show Network.Bugsnag.CodeIndex.FileIndex
+ Network.Bugsnag.CodeIndex: instance Language.Haskell.TH.Syntax.Lift Network.Bugsnag.CodeIndex.CodeIndex
+ Network.Bugsnag.CodeIndex: instance Language.Haskell.TH.Syntax.Lift Network.Bugsnag.CodeIndex.FileIndex
+ Network.Bugsnag.Event: [beException] :: BugsnagEvent -> BugsnagException
+ Network.Bugsnag.Exception.Parse: parseStringException :: SomeException -> Either String MessageWithStackFrames
+ Network.Bugsnag.Settings: [bsCodeIndex] :: BugsnagSettings -> Maybe CodeIndex
+ Network.Bugsnag.StackFrame: attachBugsnagCode :: CodeIndex -> BugsnagStackFrame -> BugsnagStackFrame
- Network.Bugsnag.Event: BugsnagEvent :: NonEmpty BugsnagException -> Maybe [BugsnagBreadcrumb] -> Maybe BugsnagRequest -> Maybe [BugsnagThread] -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe BugsnagSeverity -> Maybe BugsnagSeverityReason -> Maybe BugsnagUser -> Maybe BugsnagApp -> Maybe BugsnagDevice -> Maybe Value -> BugsnagEvent
+ Network.Bugsnag.Event: BugsnagEvent :: BugsnagException -> Maybe [BugsnagBreadcrumb] -> Maybe BugsnagRequest -> Maybe [BugsnagThread] -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe BugsnagSeverity -> Maybe BugsnagSeverityReason -> Maybe BugsnagUser -> Maybe BugsnagApp -> Maybe BugsnagDevice -> Maybe Value -> BugsnagEvent
- Network.Bugsnag.Event: bugsnagEvent :: NonEmpty BugsnagException -> BugsnagEvent
+ Network.Bugsnag.Event: bugsnagEvent :: BugsnagException -> BugsnagEvent
- Network.Bugsnag.Settings: BugsnagSettings :: BugsnagApiKey -> Maybe Text -> BugsnagReleaseStage -> [BugsnagReleaseStage] -> BeforeNotify -> BugsnagException -> Bool -> BugsnagEvent -> Maybe Text -> FilePath -> Bool -> BugsnagStackFrame -> Bool -> Manager -> BugsnagSettings
+ Network.Bugsnag.Settings: BugsnagSettings :: BugsnagApiKey -> Maybe Text -> BugsnagReleaseStage -> [BugsnagReleaseStage] -> BeforeNotify -> BugsnagException -> Bool -> BugsnagEvent -> Maybe Text -> FilePath -> Bool -> BugsnagStackFrame -> Bool -> Manager -> Maybe CodeIndex -> BugsnagSettings

Files

bugsnag-haskell.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 36962ecc46dfa7d5ab30da6797310d06145ecdd5d2d487ab6f1ec237b259e721+-- hash: 01ab548c1cccbe4991f8fcdae7003425379acff61d3a52d709841c2d174fd35c  name:           bugsnag-haskell-version:        0.0.1.3+version:        0.0.2.0 synopsis:       Bugsnag error reporter for Haskell description:    Please see README.md category:       Web@@ -27,10 +27,12 @@       src   ghc-options: -Wall   build-depends:-      aeson >=1.3.0.0+      Glob >=0.9.0+    , aeson >=1.3.0.0     , base >=4.8.0 && <5     , bytestring     , case-insensitive+    , containers     , http-client     , http-client-tls     , http-conduit@@ -50,6 +52,7 @@       Network.Bugsnag.App       Network.Bugsnag.BeforeNotify       Network.Bugsnag.Breadcrumb+      Network.Bugsnag.CodeIndex       Network.Bugsnag.Device       Network.Bugsnag.Event       Network.Bugsnag.Exception@@ -160,7 +163,9 @@     , hspec     , text     , time+    , unliftio   other-modules:+      Network.Bugsnag.CodeIndexSpec       Network.Bugsnag.ReportSpec       Network.BugsnagSpec       Paths_bugsnag_haskell
src/Network/Bugsnag/BeforeNotify.hs view
@@ -6,12 +6,16 @@      -- * Modifying the Exception     , updateException+    , updateStackFrames     , filterStackFrames+    , setStackFramesCode     , setStackFramesInProject     , setGroupingHash     , setGroupingHashBy      -- * Modifying the Event+    , updateEventFromException+    , updateEventFromOriginalException     , updateEventFromSession     , updateEventFromWaiRequest @@ -29,7 +33,10 @@     , setInfoSeverity     ) where +import Control.Exception (Exception, fromException)+import Data.Maybe (fromMaybe) import Data.Text (Text)+import Network.Bugsnag.CodeIndex import Network.Bugsnag.Device import Network.Bugsnag.Event import Network.Bugsnag.Exception@@ -63,9 +70,6 @@  -- | Modify just the Exception part of an Event ----- Technically this will modify all exceptions in the Event, but if you're using--- this library normally, there will be only one.--- -- This may be used to set more specific information for exception types in -- scope in your application: --@@ -81,20 +85,26 @@ -- >         _ -> ex -- updateException :: (BugsnagException -> BugsnagException) -> BeforeNotify-updateException f event = event { beExceptions = f <$> beExceptions event }+updateException f event = event { beException = f $ beException event } +-- | Apply a function to each @'BugsnagStackFrame'@ in the Exception+updateStackFrames :: (BugsnagStackFrame -> BugsnagStackFrame) -> BeforeNotify+updateStackFrames f =+    updateException $ \ex -> ex { beStacktrace = map f $ beStacktrace ex }+ -- | Filter out StackFrames matching a predicate filterStackFrames :: (BugsnagStackFrame -> Bool) -> BeforeNotify filterStackFrames p =     updateException $ \ex -> ex { beStacktrace = filter p $ beStacktrace ex } +-- | Set @'bsfCode'@ using the given index+setStackFramesCode :: CodeIndex -> BeforeNotify+setStackFramesCode = updateStackFrames . attachBugsnagCode+ -- | Set @'bsIsInProject'@ using the given predicate, applied to the Filename setStackFramesInProject :: (FilePath -> Bool) -> BeforeNotify-setStackFramesInProject p = updateException-    $ \ex -> ex { beStacktrace = map updateStackFrames $ beStacktrace ex }-  where-    updateStackFrames :: BugsnagStackFrame -> BugsnagStackFrame-    updateStackFrames sf = sf { bsfInProject = Just $ p $ bsfFile sf }+setStackFramesInProject p =+    updateStackFrames $ \sf -> sf { bsfInProject = Just $ p $ bsfFile sf }  -- | Set @'beGroupingHash'@ setGroupingHash :: Text -> BeforeNotify@@ -103,6 +113,45 @@ -- | Set @'beGroupingHash'@ based on the Event setGroupingHashBy :: (BugsnagEvent -> Maybe Text) -> BeforeNotify setGroupingHashBy f event = event { beGroupingHash = f event }++-- | Update the @'BugsnagEvent'@ based on its @'BugsnagException'@+--+-- Use this instead of @'updateException'@ if you want to do other things to the+-- Event, such as set its @'beGroupingHash'@ based on the Exception.+--+updateEventFromException :: (BugsnagException -> BeforeNotify) -> BeforeNotify+updateEventFromException f event = f (beException event) event++-- | Update the @'BugsnagEvent'@ based on the original exception+--+-- This allows updating the Event after casting to an exception type that this+-- library doesn't know about (e.g. @SqlError@). Because the result of your+-- function is itself a @'BeforeNotify'@, you can (and should) use other+-- helpers:+--+-- @+-- myBeforeNotify =+--     'defaultBeforeNotify'+--         . 'updateEventFromOriginalException' asSqlError+--         . 'updateEventFromOriginalException' asHttpError+--         . -- ...+--+-- asSqlError :: SqlError -> BeforeNotify+-- asSqlError SqlError{..} =+--     'setGroupingHash' sqlErrorCode . 'updateException' $ \ex -> ex+--         { beErrorClass = sqlErrorCode+--         , beMessage = sqlErrorMessage+--         }+-- @+--+-- If there is no original exception, or the cast fails, the event is unchanged.+--+updateEventFromOriginalException+    :: Exception e => (e -> BeforeNotify) -> BeforeNotify+updateEventFromOriginalException f event = fromMaybe event $ do+    someException <- beOriginalException $ beException event+    yourException <- fromException someException+    pure $ f yourException event  -- | Set the events @'BugsnagEvent'@ and @'BugsnagDevice'@ updateEventFromWaiRequest :: Request -> BeforeNotify
+ src/Network/Bugsnag/CodeIndex.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+module Network.Bugsnag.CodeIndex+    ( CodeIndex+    , buildCodeIndex+    , findSourceRange+    ) where++import Data.List (genericLength)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import Data.Traversable (for)+import Instances.TH.Lift ()+import Language.Haskell.TH.Syntax+import Numeric.Natural (Natural)+import System.FilePath.Glob (glob)++newtype CodeIndex = CodeIndex+    { unCodeIndex :: Map FilePath FileIndex }+    deriving (Lift, Show)++buildCodeIndex :: String -> Q Exp+buildCodeIndex p = do+    index <- qRunIO $ buildCodeIndex' p+    [|index|]++buildCodeIndex' :: String -> IO CodeIndex+buildCodeIndex' p = do+    paths <- glob p+    CodeIndex . Map.fromList <$> traverse indexPath paths+  where+    indexPath :: FilePath -> IO (FilePath, FileIndex)+    indexPath fp = (fp, ) <$> buildFileIndex fp++data FileIndex = FileIndex+    { fiSourceLines :: Map Natural Text+    , fiLastLine :: Natural+    }+    deriving (Lift, Show)++buildFileIndex :: FilePath -> IO FileIndex+buildFileIndex path = do+    lns <- T.lines <$> T.readFile path++    pure FileIndex+        { fiSourceLines = Map.fromList $ zip [0 ..] lns+        , fiLastLine = genericLength lns - 1+        }++findSourceRange+    :: FilePath -> (Natural, Natural) -> CodeIndex -> Maybe [(Natural, Text)]+findSourceRange path (begin, end) index = do+    FileIndex {..} <- Map.lookup path $ unCodeIndex index++    for [begin .. min end fiLastLine]+        $ \n -> (n, ) <$> Map.lookup n fiSourceLines
src/Network/Bugsnag/Event.hs view
@@ -1,15 +1,13 @@-{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} module Network.Bugsnag.Event     ( BugsnagEvent(..)     , bugsnagEvent     ) where  import Data.Aeson-import Data.Aeson.Ext-import Data.List.NonEmpty (NonEmpty)+import Data.Aeson.Types import Data.Text (Text)-import GHC.Generics import Network.Bugsnag.App import Network.Bugsnag.Breadcrumb import Network.Bugsnag.Device@@ -20,7 +18,7 @@ import Network.Bugsnag.User  data BugsnagEvent = BugsnagEvent-    { beExceptions :: NonEmpty BugsnagException+    { beException :: BugsnagException     , beBreadcrumbs :: Maybe [BugsnagBreadcrumb]     , beRequest :: Maybe BugsnagRequest     , beThreads :: Maybe [BugsnagThread]@@ -37,15 +35,33 @@     -- and I'm not sure yet how to resolve the naming clash with BugsnagSession.     , beMetaData :: Maybe Value     }-    deriving Generic  instance ToJSON BugsnagEvent where-    toJSON = genericToJSON $ bsAesonOptions "be"-    toEncoding = genericToEncoding $ bsAesonOptions "be"+    -- | Explicit instance needed to send @'beException'@ as @exceptions@+    toJSON BugsnagEvent {..} = object+        $ "exceptions" .= [beException]+        : concat+            [ "breadcrumbs" .=? beBreadcrumbs+            , "request" .=? beRequest+            , "threads" .=? beThreads+            , "context" .=? beContext+            , "groupingHash" .=? beGroupingHash+            , "unhandled" .=? beUnhandled+            , "severity" .=? beSeverity+            , "severityReason" .=? beSeverityReason+            , "user" .=? beUser+            , "app" .=? beApp+            , "device" .=? beDevice+            , "metaData" .=? beMetaData+            ]+      where+        -- For implementing "omit Nothing fields"+        (.=?) :: ToJSON v => Text -> Maybe v -> [Pair]+        (.=?) k = maybe [] (pure . (k .=)) -bugsnagEvent :: NonEmpty BugsnagException -> BugsnagEvent-bugsnagEvent exceptions = BugsnagEvent-    { beExceptions = exceptions+bugsnagEvent :: BugsnagException -> BugsnagEvent+bugsnagEvent exception = BugsnagEvent+    { beException = exception     , beBreadcrumbs = Nothing     , beRequest = Nothing     , beThreads = Nothing
src/Network/Bugsnag/Exception.hs view
@@ -74,7 +74,7 @@ -- bugsnagExceptionFromSomeException :: SomeException -> BugsnagException bugsnagExceptionFromSomeException ex =-    foldr go (bugsnagExceptionFromException ex) exCasters+    foldr go (bugsnagExceptionWithParser parseStringException ex) exCasters   where     go :: Caster -> BugsnagException -> BugsnagException     go (Caster caster) res = maybe res caster $ fromException ex@@ -82,7 +82,7 @@ exCasters :: [Caster] exCasters =     [ Caster id-    , Caster bugsnagExceptionFromErrorCall+    , Caster $ bugsnagExceptionWithParser parseErrorCall     , Caster $ bugsnagExceptionFromException @IOException     , Caster $ bugsnagExceptionFromException @ArithException     , Caster $ bugsnagExceptionFromException @ArrayException@@ -103,13 +103,13 @@     , Caster $ bugsnagExceptionFromException @TypeError     ] --- | Construct a @'BugsnagException'@ from an @'ErrorCall'@------ This type of exception may have @'HasCallStack'@ information.----bugsnagExceptionFromErrorCall :: ErrorCall -> BugsnagException-bugsnagExceptionFromErrorCall ex =-    case parseErrorCall ex of+bugsnagExceptionWithParser+    :: Exception e+    => (e -> Either String MessageWithStackFrames)+    -> e+    -> BugsnagException+bugsnagExceptionWithParser p ex =+    case p ex of         Left _ -> bugsnagExceptionFromException ex         Right (MessageWithStackFrames message stacktrace) ->             bugsnagException (exErrorClass ex) message stacktrace
src/Network/Bugsnag/Exception/Parse.hs view
@@ -6,14 +6,16 @@ module Network.Bugsnag.Exception.Parse     ( MessageWithStackFrames(..)     , parseErrorCall+    , parseStringException     ) where -import Control.Exception (ErrorCall)+import Control.Exception (ErrorCall, Exception, SomeException) import Control.Monad (void) import Data.Bifunctor (first) import Data.Text (Text) import qualified Data.Text as T import Network.Bugsnag.StackFrame+import Numeric.Natural import Text.Parsec import Text.Parsec.String @@ -24,35 +26,89 @@  -- | Parse an @'ErrorCall'@ for @'HasCallStack'@ information parseErrorCall :: ErrorCall -> Either String MessageWithStackFrames-parseErrorCall = first show . parse (errorCallParser <* eof) "<error>" . show+parseErrorCall = parse' errorCallParser +-- | Parse a @'StringException'@ for @'HasCallStack'@ information+--+-- We accept this as @'SomeException'@ so that this library doesn't depend on+-- any one concrete library that has @'throwString'@ (there are two right now,+-- sigh.)+--+parseStringException :: SomeException -> Either String MessageWithStackFrames+parseStringException = parse' stringExceptionParser+ errorCallParser :: Parser MessageWithStackFrames errorCallParser = MessageWithStackFrames-    <$> (T.pack <$> manyTill anyChar eol)-    <*> (csHeader *> manyTill stackFrameParser eof)+    <$> messageParser+    <*> manyTill stackFrameParser eof+  where+    messageParser :: Parser Text+    messageParser = do+        msg <- T.pack <$> manyTill anyChar eol+        msg <$ (string "CallStack (from HasCallStack):" *> eol) -csHeader :: Parser ()-csHeader = string "CallStack (from HasCallStack):" *> eol+    stackFrameParser :: Parser BugsnagStackFrame+    stackFrameParser = do+        func <- stackFrameFunctionTill $ string ", called at "+        (path, ln, cl) <- stackFrameLocationTill $ eol <|> eof -stackFrameParser :: Parser BugsnagStackFrame-stackFrameParser = do-    func <- spaces *> (T.pack <$> manyTill anyChar (char ','))-    path <- string " called at " *> manyTill anyChar (char ':')-    ln <- read <$> manyTill digit (char ':')-    cl <- read <$> manyTill digit (char ' ')+        pure BugsnagStackFrame+            { bsfFile = path+            , bsfLineNumber = ln+            , bsfColumnNumber = Just cl+            , bsfMethod = func+            , bsfInProject = Just True+            , bsfCode = Nothing+            } -    void $ string "in " -- package:module+stringExceptionParser :: Parser MessageWithStackFrames+stringExceptionParser = MessageWithStackFrames+    <$> messageParser+    <*> manyTill stackFrameParser eof+  where+    messageParser :: Parser Text+    messageParser = do+        manyTill anyChar (try $ string "throwString called with:") *> eol *> eol+        msg <- T.pack <$> manyTill anyChar eol+        msg <$ (string "Called from:" *> eol)++    stackFrameParser :: Parser BugsnagStackFrame+    stackFrameParser = do+        func <- stackFrameFunctionTill $ string " ("+        (path, ln, cl) <- stackFrameLocationTill $ char ')' *> eol <|> eof++        pure BugsnagStackFrame+            { bsfFile = path+            , bsfLineNumber = ln+            , bsfColumnNumber = Just cl+            , bsfMethod = func+            , bsfInProject = Just True+            , bsfCode = Nothing+            }++stackFrameFunctionTill :: Parser a -> Parser Text+stackFrameFunctionTill p = spaces *> (T.pack <$> manyTill anyChar p)++stackFrameLocationTill :: Parser a -> Parser (FilePath, Natural, Natural)+stackFrameLocationTill p = do+    result <- (,,)+        <$> manyTill anyChar (char ':')+        <*> (read <$> manyTill digit (char ':'))+        <*> (read <$> manyTill digit (char ' '))++    -- Ignore the "in package:module" part. TODO: we could use this to set+    -- bsfInProject if we had some more knowledge about project packages.+    void $ string "in "     void $ manyTill anyChar $ char ':'-    void $ manyTill anyChar $ eol <|> eof+    void $ manyTill anyChar p+    pure result -    pure BugsnagStackFrame-        { bsfFile = path-        , bsfLineNumber = ln-        , bsfColumnNumber = Just cl-        , bsfMethod = func-        , bsfInProject = Just True-        , bsfCode = Nothing-        }+parse'+    :: Exception e+    => Parser MessageWithStackFrames+    -> e+    -> Either String MessageWithStackFrames+parse' p = first show . parse (p <* eof) "<error>" . show  eol :: Parser () eol = void endOfLine
src/Network/Bugsnag/Notify.hs view
@@ -31,11 +31,11 @@             f                 . bsBeforeNotify settings                 . setGroupingHashBy (bsGroupingHash settings)+                . maybe id setStackFramesCode (bsCodeIndex settings)                 . setStackFramesInProject (bsIsInProject settings)                 . filterStackFrames (bsFilterStackFrames settings)                 . createApp settings                 . bugsnagEvent-                . pure                 $ bugsnagExceptionFromSomeException ex          manager = bsHttpManager settings
src/Network/Bugsnag/Settings.hs view
@@ -12,11 +12,11 @@     ) where  import Data.Aeson (FromJSON)-import qualified Data.List.NonEmpty as NE import Data.String import Data.Text (Text) import qualified Data.Text as T import Network.Bugsnag.BeforeNotify+import Network.Bugsnag.CodeIndex import Network.Bugsnag.Event import Network.Bugsnag.Exception import Network.Bugsnag.ReleaseStage@@ -87,6 +87,13 @@     -- It's more efficient, and ensures proper resource cleanup, to share a     -- single manager across an application. Must be TLS-enabled.     --+    , bsCodeIndex :: Maybe CodeIndex+    -- ^ A @'CodeIndex'@ built at compile-time from project sources+    --+    -- If set, this will be used to update StackFrames to include lines of+    -- source code context as read out of this value. N.B. using this means+    -- loading and keeping the source code for the entire project in memory.+    --     }  {-# DEPRECATED bsGroupingHash "use setGroupingHashBy with bsBeforeNotify" #-}@@ -106,13 +113,14 @@     , bsIsInProject = const True     , bsFilterStackFrames = const True     , bsHttpManager = manager+    , bsCodeIndex = Nothing     }  -- | Should this @'BugsnagEvent'@ trigger notification? -- -- >>> :set -XOverloadedStrings -- >>> settings <- newBugsnagSettings ""--- >>> let event = bugsnagEvent $ pure $ bugsnagException "" "" []+-- >>> let event = bugsnagEvent $ bugsnagException "" "" [] -- >>> bugsnagShouldNotify settings event -- True --@@ -125,7 +133,7 @@ -- -- >>> let ignore = (== "IgnoreMe") . beErrorClass -- >>> let ignoreSettings = settings { bsIgnoreException = ignore }--- >>> let ignoreEvent = bugsnagEvent $ pure $ bugsnagException "IgnoreMe" "" []+-- >>> let ignoreEvent = bugsnagEvent $ bugsnagException "IgnoreMe" "" [] -- >>> bugsnagShouldNotify ignoreSettings event -- True --@@ -135,16 +143,8 @@ bugsnagShouldNotify :: BugsnagSettings -> BugsnagEvent -> Bool bugsnagShouldNotify settings event     | bsReleaseStage settings `notElem` bsNotifyReleaseStages settings = False-    | bsIgnoreException settings exception = False+    | bsIgnoreException settings $ beException event = False     | otherwise = True-  where-    exception-        | NE.length (beExceptions event) == 1 = NE.head $ beExceptions event-        | otherwise = error $ unlines-            [ "Library misused. Event must have exactly one Exception when"-            , " going through our notifyBugsnag functions. To send more than"-            , " one Exception, drop down to reportError."-            ]  -- | Construct settings with a new, TLS-enabled @'Manager'@ --
src/Network/Bugsnag/StackFrame.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE TemplateHaskell #-} module Network.Bugsnag.StackFrame     ( BugsnagCode(..)+    , attachBugsnagCode     , BugsnagStackFrame(..)     , bugsnagStackFrame     , currentStackFrame@@ -11,20 +12,40 @@  import Data.Aeson import Data.Aeson.Ext+import Data.Maybe (fromMaybe) import Data.Text (Text) import GHC.Generics import Instances.TH.Lift () import Language.Haskell.TH.Syntax+import Network.Bugsnag.CodeIndex import Numeric.Natural (Natural)  -- | Lines of code surrounding the error ----- Pairs of @(line-number, line-of-code)@, up to 3 on either side. There's no--- real way to support this in Haskell, so we always send @Nothing@.+-- Pairs of @(line-number, line-of-code)@, up to 3 on either side. -- newtype BugsnagCode = BugsnagCode [(Natural, Text)]     deriving (Show, ToJSON) +-- | Attempt to attach a @'BugsnagCode'@ to a @'BugsnagStackFrame'@+--+-- Looks up the content in the Index by File/LineNumber and, if found, sets it+-- on the record. N.B. this will only operate on in-project StackFrames.+--+attachBugsnagCode :: CodeIndex -> BugsnagStackFrame -> BugsnagStackFrame+attachBugsnagCode index sf = sf+    { bsfCode = if fromMaybe True $ bsfInProject sf+        then findBugsnagCode (bsfFile sf) (bsfLineNumber sf) index+        else Nothing+    }++findBugsnagCode :: FilePath -> Natural -> CodeIndex -> Maybe BugsnagCode+findBugsnagCode path n = fmap BugsnagCode . findSourceRange path (begin, n + 3)+  where+    begin+        | n < 3 = 0+        | otherwise = n - 3+ data BugsnagStackFrame = BugsnagStackFrame     { bsfFile :: FilePath     , bsfLineNumber :: Natural@@ -61,6 +82,8 @@ currentStackFrame :: Q Exp currentStackFrame = [|locStackFrame $(qLocation >>= liftLoc)|] +-- brittany-disable-next-binding+ locStackFrame :: Loc -> Text -> BugsnagStackFrame locStackFrame (Loc path _ _ (ls, cs) _) func =     BugsnagStackFrame@@ -74,6 +97,8 @@         -- assumed to be in end-user code.         , bsfCode = Nothing         }++-- brittany-disable-next-binding  -- Taken from monad-logger liftLoc :: Loc -> Q Exp
+ test/Network/Bugsnag/CodeIndexSpec.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+module Network.Bugsnag.CodeIndexSpec+    ( spec+    ) where++import Test.Hspec++import Network.Bugsnag.CodeIndex++spec :: Spec+spec = do+    describe "CodeIndex" $ do+        let index = $(buildCodeIndex "test/fixtures/index-project/**/*.hs")++        it "can find ranges within the file" $ do+            let path = "test/fixtures/index-project/Foo.hs"+                range = (0, 3)+                sourceLines =+                    [ (0, "module Foo where")+                    , (1, "")+                    , (2, "data What = What")+                    , (3, "    deriving Show")+                    ]++            findSourceRange path range index `shouldBe` Just sourceLines++        it "handles ranges that extend beyond bounds" $ do+            let path = "test/fixtures/index-project/Foo.hs"+                range = (6, 10)+                sourceLines =+                    [(6, "what 0 = Just What"), (7, "what _ = Nothing")]++            findSourceRange path range index `shouldBe` Just sourceLines++        it "handles ranges that are totally beyond bounds" $ do+            let path = "test/fixtures/index-project/Foo.hs"+                range = (10, 12)++            findSourceRange path range index `shouldBe` Just []++        it "handles missing files" $ do+            findSourceRange "nope.hs" (0, 3) index `shouldBe` Nothing
test/Network/Bugsnag/ReportSpec.hs view
@@ -18,12 +18,11 @@         it "is right for a minimally-specified report" $ do             let report =                     bugsnagReport-                        [ bugsnagEvent $ pure-                            $ bugsnagException-                                "errorClass"-                                "message"-                                [ bugsnagStackFrame "src/Foo/Bar.hs" 10 "myFunction"-                                ]+                        [ bugsnagEvent $ bugsnagException+                            "errorClass"+                            "message"+                            [ bugsnagStackFrame "src/Foo/Bar.hs" 10 "myFunction"+                            ]                         ]              -- N.B. we don't worry about the notifier object since it would need@@ -52,7 +51,7 @@                         { brNotifier = bugsnagNotifier                         , brEvents =                             [ BugsnagEvent-                                { beExceptions = pure BugsnagException+                                { beException = BugsnagException                                     { beErrorClass = "errorClass"                                     , beMessage = Just "message"                                     , beStacktrace =
test/Network/BugsnagSpec.hs view
@@ -10,6 +10,7 @@  import Control.Exception import Network.Bugsnag+import UnliftIO.Exception (throwString)  brokenFunctionIO :: IO a brokenFunctionIO = throw $ bugsnagException@@ -22,6 +23,13 @@ sillyHead (x:_) = x sillyHead _ = error "empty list" +brokenFunction' :: HasCallStack => IO a+brokenFunction' = sillyHead' []++sillyHead' :: HasCallStack => [a] -> IO a+sillyHead' (x:_) = pure x+sillyHead' _ = throwString "empty list"+ spec :: Spec spec = do     describe "BugsnagException" $ do@@ -34,7 +42,7 @@              let frame = head $ beStacktrace ex             bsfFile frame `shouldBe` "test/Network/BugsnagSpec.hs"-            bsfLineNumber frame `shouldBe` 16+            bsfLineNumber frame `shouldBe` 17             bsfColumnNumber frame `shouldBe` Just 43             bsfMethod frame `shouldBe` "brokenFunctionIO"             bsfInProject frame `shouldBe` Just True@@ -50,9 +58,26 @@                  let frame = head $ beStacktrace ex                 bsfFile frame `shouldBe` "test/Network/BugsnagSpec.hs"-                bsfLineNumber frame `shouldBe` 23+                bsfLineNumber frame `shouldBe` 24                 bsfColumnNumber frame `shouldBe` Just 15                 bsfMethod frame `shouldBe` "error"                  map bsfMethod (beStacktrace ex)                     `shouldBe` ["error", "sillyHead", "brokenFunction"]++            it "also parses StringException" $ do+                e <- brokenFunction' `catch` pure++                let ex = bugsnagExceptionFromSomeException e+                beErrorClass ex `shouldBe` "SomeException"+                beMessage ex `shouldBe` Just "empty list"+                beStacktrace ex `shouldSatisfy` ((== 3) . length)++                let frame = head $ beStacktrace ex+                bsfFile frame `shouldBe` "test/Network/BugsnagSpec.hs"+                bsfLineNumber frame `shouldBe` 31+                bsfColumnNumber frame `shouldBe` Just 16+                bsfMethod frame `shouldBe` "throwString"++                map bsfMethod (beStacktrace ex)+                    `shouldBe` ["throwString", "sillyHead'", "brokenFunction'"]