Guide for integrating `AnnotatedException`
parsonsmatt opened this issue · 2 comments
It's not entirely clear the best way to integrate AnnotatedException
into a code base.
As Shea notices, this stuff is all one-way. Once an exception has been Annotated
, the regular try/catch/handle
etc functions won't be able to see it anymore. If your library throws AnnotatedException e
, you're essentially locking your users into this library. Beyond just loudly documenting this, I wonder if there's a good way to have AnnotatedException
link to it's own package - maybe the Show
instance should say something like catch me with Control.Exception.Annotated
.
So, a first step, is to probably import Control.Exception.Annotated
everywhere in place of Control.Exception{,.Safe}
or Control.Monad.Catch
. If you're using a custom Prelude, this is easy - use hlint
to ban Control.Exception
and friends, ensure that try/catch
etc come from this package, and you should be good.
But then - just to be safe - you should probably start by incorporating this at the top-level of your program. Ensure that all places that need to catch/handle exceptions are doing so with AnnotatedException
. This shouldn't be totally necessary - try
and catch
from this library will work fine to catch the underlying e
from an AnnotatedException e
.
Once you've started with that, you can start adding checkpoint
s on code and get neat information back out.
Then you have to figure out how you want to ensure you're actually getting all the information you dig out of the Annotation
. I'm not entirely sure the best way to do this. But, yeah, writing your own checkpoint
that takes some more specific type is probably a good idea.
OK, so here's what I'm working with in the work codebase.
We're defining:
data OurAnnotation where
OurAnnotation :: (OurRequirements a) => a -> OurAnnotation
ourCheckpoint :: (OurRequirements a, MonadUnliftIO m) => a -> m r -> mr
ourCheckpoint a
checkpoint (Annotation (OurAnnotation a))
Then, when we're digging through our [Annotation]
, we do something like this:
castToOurAnnotation :: Annotation -> Either OurAnnotation Annotation
castToOurAnnotation ann@(Annotation a) =
case cast a of
Just (OurAnnotation b) -> Left (OurAnnotation b)
Nothing -> Right ann
handleAnnotations :: [Annotation] -> IO ()
handleAnnotations anns = do
let (ours, unknown) = partitionEithers $ map castToOurAnnotation anns
-- ...
For our codebase, this ends up doing BugsnagEvent modifications, and our core class is basically class MercuryAnnotation a where mercuryAnnotationBugsnagModifier :: a -> BugsnagEvent -> BugsnagEvent
.
(though, i am now realizing that there's a lot of the Data.Annotation
API that I'm recreating here)
For unknown annotations, we just show
the underlying thing and report it as a string. Which isn't ideal, but we also control almost all of the annotations.
Bugsnag Integration
I wrote a brief issue comment on integrating Bugsnag and annotated-exception
, which should apply to other error reporting services too.
Stripping Annotation
at the reporting site
In bugsnag-haskell
there's a field on BugsnagSettings
called bsBeforeNotify :: BugsnagEvent -> BugsnagEvent
. When initializing a BugsnagSettings
, you will want to start by stripping off the AnnotatedException
wrapper and folding the annotations into the event there.
myDefaultBugsnagSettings :: BugsnagSettings
myDefaultBugsnagSettings =
defaultBugsnagSettings
{ bsBeforeNotify = \event ->
updateEventFromOriginalException $ \(AnnotatedException anns exn) event ->
(annotationsToBugsnagModifier anns event)
{ beException =
(beException event)
{ beOriginalException =
Just exn
}
}
}
This will allow any other updateEventOnOriginalException
to target the correct exception type. Additionally, you won't need to remember to strip annotations when doing a notifyBugsnag
- it'll "Just Work" on any AnnotatedException
directly.
Using internal checkpoint
to add before notify hooks
Another pattern (in our app, at least) is to use Bugsnag with uncaught exceptions - something like:
case maybeThing of
Just thing -> do
doWorkOn thing
pure ()
Nothing ->
reportException $ SomeProblem "Thing wasn't present"
This is a non-annotated exception, and so we lose out on the annotations that may have been embedded had this been checkpoint
ed.
To work around this, we provide our own checkpoint
that adds a bsBeforeNotify
hook after the ones already defined.
myCheckpoint :: (MyAnnotation ann, MonadBugsnag m, MonadUnliftIO m) => ann -> m a -> m a
myCheckpoint ann action =
withBeforeNotifyHook (myAnnotationToBeforeNotify ann) $
checkpoint (Annotation (MyAnnotationWrapper ann)) action
withBeforeNotifyHook f action =
local (\bsSettings -> bsSettings { bsBeforeNotify = f . bsBeforeNotify bsSettings })
The withBeforeNotifyHook
ensures the annotation is present on the exception if it is reported inside of action
, and the checkpoint
ensures that it is attached to any exceptions that escape action
.