bugsnag-1.2.0.3: src/Network/Bugsnag/Exception.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ExistentialQuantification #-}
module Network.Bugsnag.Exception
( AsException (..)
, bugsnagExceptionFromSomeException
) where
import Prelude
import Control.Exception
( ErrorCall
, SomeException (SomeException)
, displayException
, fromException
)
import qualified Control.Exception as Exception
import Control.Exception.Annotated
( AnnotatedException (AnnotatedException)
, annotatedExceptionCallStack
)
import qualified Control.Exception.Annotated as Annotated
import Data.Bugsnag
import Data.Foldable (asum)
import Data.Function ((&))
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.Typeable (typeRep)
import GHC.Stack (CallStack, SrcLoc (..), getCallStack)
import qualified Network.Bugsnag.Exception.Context as Context
import Network.Bugsnag.Exception.Parse
import UnliftIO.Exception (StringException (StringException))
-- | Newtype over 'Exception', so it can be thrown and caught
newtype AsException = AsException
{ unAsException :: Exception
}
deriving newtype (Show)
deriving anyclass (Exception.Exception)
-- | Construct a 'Exception' from a 'SomeException'
--
-- It is possible that an exception contains multiple sources of messages and stack traces. The general
-- principles for selection are:
--
-- * When exception wrappers/context are present, prefer information from the innermost exception
-- * Prefer features from more standard and commonly-used libraries
bugsnagExceptionFromSomeException :: SomeException -> Exception
bugsnagExceptionFromSomeException ex =
bugsnagExceptionFromAsException ex
& fromMaybe
( asum
[ bugsnagExceptionFromErrorCall ex
, bugsnagExceptionFromStringException ex
, bugsnagExceptionFromSomeExceptionResemblingStringException ex
]
& fromMaybe (unrecognized ex)
& ( \be -> case exception_stacktrace be of
[] -> case findTraceAnnotation ex of
Nothing -> be
Just frames -> be {exception_stacktrace = frames}
_ -> be
)
)
-- | Respect 'AsException' as-is without modifications
--
-- If it's wrapped in 'AnnotatedException', ignore the annotations.
bugsnagExceptionFromAsException :: SomeException -> Maybe Exception
bugsnagExceptionFromAsException ex = do
AnnotatedException {exception = AsException bugsnagException} <-
fromException @(AnnotatedException AsException) ex
pure bugsnagException
findTraceAnnotation :: SomeException -> Maybe [StackFrame]
findTraceAnnotation ex =
asum
[ Context.backtraceStackFrames ex
, do
ae <- fromException @(AnnotatedException SomeException) ex
stack <- annotatedExceptionCallStack ae
pure $ callStackToStackFrames stack
]
-- | When a 'StringException' is thrown, we use its message and trace.
bugsnagExceptionFromStringException :: SomeException -> Maybe Exception
bugsnagExceptionFromStringException ex = do
AnnotatedException {exception = stringException@(StringException message stack)} <-
fromException @(AnnotatedException StringException) ex
pure
defaultException
{ exception_errorClass = T.pack $ show $ typeRep $ Just stringException
, exception_message = Just $ T.dropWhileEnd (== '\n') $ T.pack message
, exception_stacktrace = callStackToStackFrames stack
}
bugsnagExceptionFromErrorCall :: SomeException -> Maybe Exception
bugsnagExceptionFromErrorCall ex = do
AnnotatedException {exception = errorCall} <-
fromException @(AnnotatedException ErrorCall) ex
messageWithStackFrames <- either (const Nothing) Just $ parseErrorCall errorCall
pure
defaultException
{ exception_errorClass = T.pack $ show $ typeRep $ Just errorCall
, exception_message =
Just $ T.dropWhileEnd (== '\n') $ mwsfMessage messageWithStackFrames
, exception_stacktrace = mwsfStackFrames messageWithStackFrames
}
bugsnagExceptionFromSomeExceptionResemblingStringException
:: SomeException -> Maybe Exception
bugsnagExceptionFromSomeExceptionResemblingStringException ex = do
AnnotatedException {exception = wrappedException} <-
fromException @(AnnotatedException SomeException) ex
messageWithStackFrames <-
either (const Nothing) pure $ parseStringException wrappedException
pure
defaultException
{ exception_errorClass = "StringException"
, exception_message =
Just $ T.dropWhileEnd (== '\n') $ mwsfMessage messageWithStackFrames
, exception_stacktrace = mwsfStackFrames messageWithStackFrames
}
unrecognized :: SomeException -> Exception
unrecognized ex =
defaultException
{ exception_errorClass = case fromException @(AnnotatedException SomeException) ex of
Just (AnnotatedException {exception = SomeException e}) -> T.pack $ show $ typeRep $ Just e
Nothing -> T.pack $ show $ typeRep $ Just ex
, exception_message =
Just $ T.dropWhileEnd (== '\n') $ T.pack $ displayException ex
, exception_stacktrace = []
}
-- | Converts a GHC call stack to a list of stack frames suitable
-- for use as the stacktrace in a Bugsnag exception
callStackToStackFrames :: CallStack -> [StackFrame]
callStackToStackFrames = fmap callSiteToStackFrame . getCallStack
callSiteToStackFrame :: (String, SrcLoc) -> StackFrame
callSiteToStackFrame (str, loc) =
defaultStackFrame
{ stackFrame_method = T.pack str
, stackFrame_file = T.pack $ srcLocFile loc
, stackFrame_lineNumber = srcLocStartLine loc
, stackFrame_columnNumber = Just $ srcLocStartCol loc
}