bugsnag-1.2.0.1: src/Network/Bugsnag/Exception.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ExistentialQuantification #-}
module Network.Bugsnag.Exception
( AsException (..)
, bugsnagExceptionFromSomeException
) where
import Prelude
import Control.Exception
( 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.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Typeable (Proxy (..), Typeable, typeRep)
import GHC.Stack (CallStack, SrcLoc (..), getCallStack)
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'
bugsnagExceptionFromSomeException :: SomeException -> Exception
bugsnagExceptionFromSomeException ex =
fromMaybe defaultException $
asum
[ bugsnagExceptionFromAnnotatedAsException <$> fromException ex
, bugsnagExceptionFromStringException <$> fromException ex
, bugsnagExceptionFromAnnotatedStringException <$> fromException ex
, bugsnagExceptionFromAnnotatedException <$> fromException ex
]
-- | Respect 'AsException' as-is without modifications.
-- If it's wrapped in 'AnnotatedException', ignore the annotations.
bugsnagExceptionFromAnnotatedAsException
:: AnnotatedException AsException -> Exception
bugsnagExceptionFromAnnotatedAsException = unAsException . Annotated.exception
-- | When a 'StringException' is thrown, we use its message and trace.
bugsnagExceptionFromStringException :: StringException -> Exception
bugsnagExceptionFromStringException (StringException message stack) =
(mkException $ Just $ T.pack message)
{ exception_errorClass = typeName @StringException
, exception_stacktrace = callStackToStackFrames stack
}
-- | When 'StringException' is wrapped in 'AnnotatedException',
-- there are two possible sources of a 'CallStack'.
-- Prefer the one from 'AnnotatedException', falling back to the
-- 'StringException' trace if no 'CallStack' annotation is present.
bugsnagExceptionFromAnnotatedStringException
:: AnnotatedException StringException -> Exception
bugsnagExceptionFromAnnotatedStringException ae@AnnotatedException {exception = StringException message stringExceptionStack} =
(mkException $ Just $ T.pack message)
{ exception_errorClass = typeName @StringException
, exception_stacktrace =
maybe
(callStackToStackFrames stringExceptionStack)
callStackToStackFrames
$ annotatedExceptionCallStack ae
}
-- | For an 'AnnotatedException' exception, derive the error class and message
-- from the wrapped exception.
-- If a 'CallStack' annotation is present, use that as the stacetrace.
-- Otherwise, attempt to parse a trace from the underlying exception.
bugsnagExceptionFromAnnotatedException
:: AnnotatedException SomeException -> Exception
bugsnagExceptionFromAnnotatedException ae =
case annotatedExceptionCallStack ae of
Just stack ->
(mkException $ Just $ T.pack $ displayException $ Annotated.exception ae)
{ exception_errorClass = exErrorClass $ Annotated.exception ae
, exception_stacktrace = callStackToStackFrames stack
}
Nothing ->
let
parseResult =
asum
[ fromException (Annotated.exception ae)
>>= (either (const Nothing) Just . parseExceptionWithContext)
, fromException (Annotated.exception ae)
>>= (either (const Nothing) Just . parseErrorCall)
, either (const Nothing) Just $
parseStringException (Annotated.exception ae)
]
mmessage =
asum
[ mwsfMessage <$> parseResult
, Just $
T.pack $
displayException $
Annotated.exception
ae
]
in
(mkException mmessage)
{ exception_errorClass = exErrorClass $ Annotated.exception ae
, exception_stacktrace = foldMap mwsfStackFrames parseResult
}
mkException :: Maybe Text -> Exception
mkException mmsg =
defaultException
{ exception_message = T.dropWhileEnd (== '\n') <$> mmsg
}
-- | Unwrap the 'SomeException' newtype to get the actual underlying type name
exErrorClass :: SomeException -> Text
exErrorClass (SomeException (_ :: e)) = typeName @e
typeName :: forall a. Typeable a => Text
typeName = T.pack $ show $ typeRep $ Proxy @a
-- | 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
}