packages feed

patrol 1.0.0.11 → 1.0.1.0

raw patch · 22 files changed

+655/−34 lines, 22 filesdep +http-client-tlsdep ~aesondep ~basedep ~bytestringPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: http-client-tls

Dependency ranges changed: aeson, base, bytestring, case-insensitive, exceptions, network-uri

API changes (from Hackage documentation)

- Patrol.Type.Exception: fromSomeException :: SomeException -> Exception
+ Patrol: captureException :: (Exception e, MonadIO io, MonadThrow io) => e -> io Response
+ Patrol: captureExceptionWith :: (Exception e, MonadIO io, MonadThrow io) => (SomeException -> Maybe CallStack) -> (Envelope -> io Envelope) -> Dsn -> e -> io Response
+ Patrol: data Envelope
+ Patrol: data Headers
+ Patrol: data Item
+ Patrol.Client: captureException :: (Exception e, MonadIO io, MonadThrow io) => e -> io Response
+ Patrol.Client: captureExceptionWith :: (Exception e, MonadIO io, MonadThrow io) => (SomeException -> Maybe CallStack) -> (Envelope -> io Envelope) -> Dsn -> e -> io Response
+ Patrol.Constant: applicationXSentryEnvelope :: ByteString
+ Patrol.Type.ClientSdkInfo: patrol :: ClientSdkInfo
+ Patrol.Type.Dsn: fromText :: MonadThrow m => Text -> m Dsn
+ Patrol.Type.Envelope: Envelope :: Headers -> [Item] -> Envelope
+ Patrol.Type.Envelope: [headers] :: Envelope -> Headers
+ Patrol.Type.Envelope: [items] :: Envelope -> [Item]
+ Patrol.Type.Envelope: data Envelope
+ Patrol.Type.Envelope: fromEvent :: Dsn -> Event -> Envelope
+ Patrol.Type.Envelope: fromException :: (Exception e, MonadIO io) => (SomeException -> Maybe CallStack) -> Dsn -> e -> io Envelope
+ Patrol.Type.Envelope: instance GHC.Classes.Eq Patrol.Type.Envelope.Envelope
+ Patrol.Type.Envelope: instance GHC.Internal.Show.Show Patrol.Type.Envelope.Envelope
+ Patrol.Type.Envelope: intoRequest :: MonadThrow m => Dsn -> Envelope -> m Request
+ Patrol.Type.Envelope: serialize :: Envelope -> Builder
+ Patrol.Type.Event: fromException :: (Exception e, MonadIO io) => (SomeException -> Maybe CallStack) -> e -> io Event
+ Patrol.Type.Event: initial :: Event
+ Patrol.Type.Event: setEventId :: MonadIO io => Event -> io Event
+ Patrol.Type.Event: setTimestamp :: MonadIO io => Event -> io Event
+ Patrol.Type.Exception: fromException :: Exception e => (SomeException -> Maybe CallStack) -> e -> Exception
+ Patrol.Type.Exceptions: fromException :: Exception e => (SomeException -> Maybe CallStack) -> e -> Exceptions
+ Patrol.Type.Headers: Headers :: Object -> Headers
+ Patrol.Type.Headers: empty :: Headers
+ Patrol.Type.Headers: fromObject :: Object -> Headers
+ Patrol.Type.Headers: instance Data.Aeson.Types.FromJSON.FromJSON Patrol.Type.Headers.Headers
+ Patrol.Type.Headers: instance Data.Aeson.Types.ToJSON.ToJSON Patrol.Type.Headers.Headers
+ Patrol.Type.Headers: instance GHC.Classes.Eq Patrol.Type.Headers.Headers
+ Patrol.Type.Headers: instance GHC.Internal.Show.Show Patrol.Type.Headers.Headers
+ Patrol.Type.Headers: intoObject :: Headers -> Object
+ Patrol.Type.Headers: newtype Headers
+ Patrol.Type.Headers: serialize :: Headers -> Builder
+ Patrol.Type.Item: Item :: Headers -> ByteString -> Item
+ Patrol.Type.Item: [headers] :: Item -> Headers
+ Patrol.Type.Item: [payload] :: Item -> ByteString
+ Patrol.Type.Item: data Item
+ Patrol.Type.Item: fromEvent :: Event -> Item
+ Patrol.Type.Item: instance GHC.Classes.Eq Patrol.Type.Item.Item
+ Patrol.Type.Item: instance GHC.Internal.Show.Show Patrol.Type.Item.Item
+ Patrol.Type.Item: serialize :: Item -> Builder

Files

patrol.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: patrol-version: 1.0.0.11+version: 1.0.1.0 synopsis: Sentry SDK description: Patrol is a Sentry SDK. build-type: Simple@@ -22,7 +22,7 @@   manual: True  common library-  build-depends: base ^>=4.19.0.0 || ^>=4.20.0.0 || ^>=4.21.0.0+  build-depends: base ^>=4.19 || ^>=4.20 || ^>=4.21   default-language: Haskell2010   ghc-options:     -Weverything@@ -51,14 +51,15 @@   import: library   autogen-modules: Paths_patrol   build-depends:-    aeson ^>=2.1.2.1 || ^>=2.2.2.0,-    bytestring ^>=0.11.4.0 || ^>=0.12.0.2,-    case-insensitive ^>=1.2.1.0,+    aeson ^>=2.1.2.1 || ^>=2.2.2,+    bytestring ^>=0.11.4 || ^>=0.12.0.2,+    case-insensitive ^>=1.2.1,     containers ^>=0.6.7 || ^>=0.7,     exceptions ^>=0.10.7,     http-client ^>=0.7.17,+    http-client-tls ^>=0.3.6.4,     http-types ^>=0.12.4,-    network-uri ^>=2.6.4.2 || ^>=2.7.0.0,+    network-uri ^>=2.6.4.2 || ^>=2.7,     text ^>=2.0.2 || ^>=2.1,     time ^>=1.12.2 || ^>=1.14,     uuid ^>=1.3.15,@@ -85,6 +86,7 @@     Patrol.Type.DebugMeta     Patrol.Type.DeviceContext     Patrol.Type.Dsn+    Patrol.Type.Envelope     Patrol.Type.ErrorType     Patrol.Type.Event     Patrol.Type.EventId@@ -95,6 +97,8 @@     Patrol.Type.Frame     Patrol.Type.Geo     Patrol.Type.GpuContext+    Patrol.Type.Headers+    Patrol.Type.Item     Patrol.Type.Level     Patrol.Type.LogEntry     Patrol.Type.MachException@@ -130,7 +134,6 @@     bytestring,     case-insensitive,     containers,-    exceptions,     hspec ^>=2.11.8,     http-client,     http-types,@@ -161,6 +164,7 @@     Patrol.Type.DebugMetaSpec     Patrol.Type.DeviceContextSpec     Patrol.Type.DsnSpec+    Patrol.Type.EnvelopeSpec     Patrol.Type.ErrorTypeSpec     Patrol.Type.EventIdSpec     Patrol.Type.EventProcessingErrorSpec@@ -171,6 +175,8 @@     Patrol.Type.FrameSpec     Patrol.Type.GeoSpec     Patrol.Type.GpuContextSpec+    Patrol.Type.HeadersSpec+    Patrol.Type.ItemSpec     Patrol.Type.LevelSpec     Patrol.Type.LogEntrySpec     Patrol.Type.MachExceptionSpec
source/library/Patrol.hs view
@@ -1,5 +1,7 @@ module Patrol-  ( Patrol.Exception.Problem.Problem,+  ( Patrol.Client.captureException,+    Patrol.Client.captureExceptionWith,+    Patrol.Exception.Problem.Problem,     Patrol.Type.AppContext.AppContext,     Patrol.Type.AppleDebugImage.AppleDebugImage,     Patrol.Type.Breadcrumb.Breadcrumb,@@ -14,7 +16,10 @@     Patrol.Type.DebugMeta.DebugMeta,     Patrol.Type.DeviceContext.DeviceContext,     Patrol.Type.Dsn.Dsn,+    Patrol.Type.Envelope.Envelope,     Patrol.Type.ErrorType.ErrorType,+    Patrol.Type.Headers.Headers,+    Patrol.Type.Item.Item,     Patrol.Type.Event.Event,     Patrol.Type.EventId.EventId,     Patrol.Type.EventProcessingError.EventProcessingError,@@ -50,6 +55,7 @@   ) where +import qualified Patrol.Client import qualified Patrol.Exception.Problem import qualified Patrol.Type.AppContext import qualified Patrol.Type.AppleDebugImage@@ -65,6 +71,7 @@ import qualified Patrol.Type.DebugMeta import qualified Patrol.Type.DeviceContext import qualified Patrol.Type.Dsn+import qualified Patrol.Type.Envelope import qualified Patrol.Type.ErrorType import qualified Patrol.Type.Event import qualified Patrol.Type.EventId@@ -75,6 +82,8 @@ import qualified Patrol.Type.Frame import qualified Patrol.Type.Geo import qualified Patrol.Type.GpuContext+import qualified Patrol.Type.Headers+import qualified Patrol.Type.Item import qualified Patrol.Type.Level import qualified Patrol.Type.LogEntry import qualified Patrol.Type.MachException
source/library/Patrol/Client.hs view
@@ -3,12 +3,52 @@ import qualified Control.Monad.Catch as Catch import qualified Control.Monad.IO.Class as IO import qualified Data.Aeson as Aeson+import qualified Data.Text as Text+import qualified GHC.Stack as Stack import qualified Network.HTTP.Client as Client+import qualified Network.HTTP.Client.TLS as Tls import qualified Patrol.Exception.Problem as Problem import qualified Patrol.Type.Dsn as Dsn+import qualified Patrol.Type.Envelope as Envelope import qualified Patrol.Type.Event as Event import qualified Patrol.Type.Response as Response+import qualified System.Environment as Environment +-- | Capture an exception by sending it to Sentry. The DSN is read from the+-- @SENTRY_DSN@ environment variable. To customize the behavior, use+-- 'captureExceptionWith'.+captureException ::+  (Catch.Exception e, IO.MonadIO io, Catch.MonadThrow io) =>+  e ->+  io Response.Response+captureException e = do+  dsn <- do+    maybeString <- IO.liftIO $ Environment.lookupEnv "SENTRY_DSN"+    Dsn.fromText $ maybe Text.empty Text.pack maybeString+  captureExceptionWith (const Nothing) pure dsn e++captureExceptionWith ::+  (Catch.Exception e, IO.MonadIO io, Catch.MonadThrow io) =>+  -- | How to get a 'Stack.CallStack' from a 'Catch.SomeException'. Use+  -- @'const' 'Nothing'@ if you don't want to get a call stack.+  (Catch.SomeException -> Maybe Stack.CallStack) ->+  -- | How to modify the 'Envelope.Envelope' before it is sent. Use @'pure'@ if+  -- you don't want to modify the envelope.+  (Envelope.Envelope -> io Envelope.Envelope) ->+  Dsn.Dsn ->+  e ->+  io Response.Response+captureExceptionWith getCallStack modifyEnvelope dsn e = do+  initialEnvelope <- Envelope.fromException getCallStack dsn e+  envelope <- modifyEnvelope initialEnvelope+  request <- Envelope.intoRequest dsn envelope+  manager <- Tls.newTlsManager+  response <- IO.liftIO $ Client.httpLbs request manager+  either (Catch.throwM . Problem.Problem . mappend "invalid response body: ") pure+    . Aeson.eitherDecode+    $ Client.responseBody response++-- TODO: Deprecate. store ::   (IO.MonadIO io, Catch.MonadThrow io) =>   Client.Manager ->
source/library/Patrol/Constant.hs view
@@ -10,6 +10,9 @@ applicationJson :: ByteString.ByteString applicationJson = Text.encodeUtf8 $ Text.pack "application/json" +applicationXSentryEnvelope :: ByteString.ByteString+applicationXSentryEnvelope = Text.encodeUtf8 $ Text.pack "application/x-sentry-envelope"+ sentryVersion :: Text.Text sentryVersion = Text.singleton '7' 
source/library/Patrol/Type/ClientSdkInfo.hs view
@@ -4,6 +4,7 @@ import qualified Data.Text as Text import qualified Patrol.Extra.Aeson as Aeson import qualified Patrol.Type.ClientSdkPackage as ClientSdkPackage+import qualified Patrol.Version as Version  -- | <https://develop.sentry.dev/sdk/event-payloads/types/#clientsdkinfo> data ClientSdkInfo = ClientSdkInfo@@ -30,4 +31,11 @@       name = Text.empty,       packages = [],       version = Text.empty+    }++patrol :: ClientSdkInfo+patrol =+  empty+    { name = Text.pack "patrol",+      version = Version.text     }
source/library/Patrol/Type/Dsn.hs view
@@ -23,6 +23,11 @@   }   deriving (Eq, Show) +fromText :: (Catch.MonadThrow m) => Text.Text -> m Dsn+fromText text = case Uri.parseURI (Text.unpack text) of+  Nothing -> Catch.throwM $ Problem.Problem "invalid URI"+  Just uri -> fromUri uri+ fromUri :: (Catch.MonadThrow m) => Uri.URI -> m Dsn fromUri uri = do   theProtocol <- maybe (Catch.throwM $ Problem.Problem "invalid scheme") pure . Text.stripSuffix (Text.singleton ':') . Text.pack $ Uri.uriScheme uri
+ source/library/Patrol/Type/Envelope.hs view
@@ -0,0 +1,92 @@+module Patrol.Type.Envelope where++import qualified Control.Monad.Catch as Catch+import qualified Control.Monad.IO.Class as IO+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Lazy as LazyByteString+import qualified Data.Maybe as Maybe+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Encoding+import qualified GHC.Stack as Stack+import qualified Network.HTTP.Client as Client+import qualified Network.HTTP.Types as Http+import qualified Patrol.Constant as Constant+import qualified Patrol.Extra.List as List+import qualified Patrol.Type.ClientSdkInfo as ClientSdkInfo+import qualified Patrol.Type.Dsn as Dsn+import qualified Patrol.Type.Event as Event+import qualified Patrol.Type.Headers as Headers+import qualified Patrol.Type.Item as Item++-- | <https://develop.sentry.dev/sdk/data-model/envelopes/>+data Envelope = Envelope+  { headers :: Headers.Headers,+    items :: [Item.Item]+  }+  deriving (Eq, Show)++fromException ::+  (Catch.Exception e, IO.MonadIO io) =>+  (Catch.SomeException -> Maybe Stack.CallStack) ->+  Dsn.Dsn ->+  e ->+  io Envelope+fromException getCallStack dsn =+  fmap (fromEvent dsn)+    . Event.fromException getCallStack++fromEvent :: Dsn.Dsn -> Event.Event -> Envelope+fromEvent dsn event =+  Envelope+    { headers =+        Headers.fromObject+          . KeyMap.fromList+          $ Maybe.catMaybes+            [ Just (Key.fromString "dsn", Aeson.toJSON $ Dsn.intoUri dsn),+              Just (Key.fromString "sdk", Aeson.toJSON ClientSdkInfo.patrol),+              (,) (Key.fromString "sent_at") . Aeson.toJSON <$> Event.timestamp event+            ],+      items = [Item.fromEvent event]+    }++intoRequest :: (Catch.MonadThrow m) => Dsn.Dsn -> Envelope -> m Client.Request+intoRequest dsn envelope = do+  request <-+    Client.parseUrlThrow $+      mconcat+        [ Text.unpack $ Dsn.protocol dsn,+          "://",+          Text.unpack $ Dsn.host dsn,+          maybe "" ((':' :) . show) $ Dsn.port dsn,+          Text.unpack $ Dsn.path dsn,+          "api/",+          Text.unpack $ Dsn.projectId dsn,+          "/envelope/"+        ]+  let body =+        LazyByteString.toStrict+          . Builder.toLazyByteString+          $ serialize envelope+  pure+    request+      { Client.method = Http.methodPost,+        Client.requestHeaders =+          List.insertAll+            [ (Http.hContentLength, Encoding.encodeUtf8 . Text.pack . show $ ByteString.length body),+              (Http.hContentType, Constant.applicationXSentryEnvelope),+              (Http.hUserAgent, Encoding.encodeUtf8 Constant.userAgent),+              (Constant.xSentryAuth, Dsn.intoAuthorization dsn)+            ]+            $ Client.requestHeaders request,+        Client.requestBody = Client.RequestBodyBS body+      }++serialize :: Envelope -> Builder.Builder+serialize envelope =+  Headers.serialize (headers envelope)+    <> Builder.char7 '\n'+    <> foldMap ((<> Builder.char7 '\n') . Item.serialize) (items envelope)
source/library/Patrol/Type/Event.hs view
@@ -8,6 +8,7 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Time as Time+import qualified GHC.Stack as Stack import qualified Network.HTTP.Client as Client import qualified Network.HTTP.Types as Http import qualified Patrol.Constant as Constant@@ -129,21 +130,44 @@       version = Text.empty     } -new :: (IO.MonadIO io) => io Event-new = do+initial :: Event+initial =+  empty+    { environment = Text.pack "production",+      level = Just Level.Error,+      platform = Just Platform.Haskell,+      type_ = Just EventType.Default,+      version = Constant.sentryVersion+    }++setEventId :: (IO.MonadIO io) => Event -> io Event+setEventId event = do   theEventId <- EventId.random+  pure event {eventId = theEventId}++setTimestamp :: (IO.MonadIO io) => Event -> io Event+setTimestamp event = do   theTimestamp <- IO.liftIO Time.getCurrentTime+  pure event {timestamp = Just theTimestamp}++new :: (IO.MonadIO io) => io Event+new = do+  withEventId <- setEventId initial+  setTimestamp withEventId++fromException ::+  (Catch.Exception e, IO.MonadIO io) =>+  (Catch.SomeException -> Maybe Stack.CallStack) ->+  e ->+  io Event+fromException getCallStack e = do+  event <- new   pure-    empty-      { environment = Text.pack "production",-        eventId = theEventId,-        level = Just Level.Error,-        platform = Just Platform.Haskell,-        timestamp = Just theTimestamp,-        type_ = Just EventType.Default,-        version = Constant.sentryVersion+    event+      { exception = Just $ Exceptions.fromException getCallStack e       } +-- TODO: Deprecate. intoRequest :: (Catch.MonadThrow m) => Dsn.Dsn -> Event -> m Client.Request intoRequest dsn event = do   theRequest <-
source/library/Patrol/Type/Exception.hs view
@@ -4,6 +4,7 @@ import qualified Data.Aeson as Aeson import qualified Data.Text as Text import qualified Data.Typeable as Typeable+import qualified GHC.Stack as Stack import qualified Patrol.Extra.Aeson as Aeson import qualified Patrol.Type.Mechanism as Mechanism import qualified Patrol.Type.Stacktrace as Stacktrace@@ -41,9 +42,14 @@       value = Text.empty     } -fromSomeException :: Catch.SomeException -> Exception-fromSomeException (Catch.SomeException e) =+fromException ::+  (Catch.Exception e) =>+  (Catch.SomeException -> Maybe Stack.CallStack) ->+  e ->+  Exception+fromException getCallStack e =   empty-    { type_ = Text.pack . show $ Typeable.typeOf e,+    { stacktrace = fmap Stacktrace.fromCallStack . getCallStack $ Catch.toException e,+      type_ = Text.pack . show $ Typeable.typeOf e,       value = Text.pack $ Catch.displayException e     }
source/library/Patrol/Type/Exceptions.hs view
@@ -1,6 +1,8 @@ module Patrol.Type.Exceptions where +import qualified Control.Monad.Catch as Catch import qualified Data.Aeson as Aeson+import qualified GHC.Stack as Stack import qualified Patrol.Extra.Aeson as Aeson import qualified Patrol.Type.Exception as Exception @@ -20,4 +22,14 @@ empty =   Exceptions     { values = []+    }++fromException ::+  (Catch.Exception e) =>+  (Catch.SomeException -> Maybe Stack.CallStack) ->+  e ->+  Exceptions+fromException getCallStack e =+  Exceptions+    { values = [Exception.fromException getCallStack e]     }
+ source/library/Patrol/Type/Headers.hs view
@@ -0,0 +1,27 @@+module Patrol.Type.Headers where++import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Builder as Builder++-- | <https://develop.sentry.dev/sdk/data-model/envelopes/#headers>+newtype Headers+  = Headers Aeson.Object+  deriving (Eq, Show)++instance Aeson.FromJSON Headers where+  parseJSON = fmap fromObject . Aeson.parseJSON++instance Aeson.ToJSON Headers where+  toJSON = Aeson.Object . intoObject++fromObject :: Aeson.Object -> Headers+fromObject = Headers++intoObject :: Headers -> Aeson.Object+intoObject (Headers object) = object++empty :: Headers+empty = fromObject mempty++serialize :: Headers -> Builder.Builder+serialize = Aeson.fromEncoding . Aeson.toEncoding
+ source/library/Patrol/Type/Item.hs view
@@ -0,0 +1,37 @@+module Patrol.Type.Item where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Lazy as LazyByteString+import qualified Patrol.Type.Event as Event+import qualified Patrol.Type.Headers as Headers++-- | <https://develop.sentry.dev/sdk/data-model/envelope-items/>+data Item = Item+  { headers :: Headers.Headers,+    payload :: ByteString.ByteString+  }+  deriving (Eq, Show)++fromEvent :: Event.Event -> Item+fromEvent event =+  let thePayload = LazyByteString.toStrict $ Aeson.encode event+   in Item+        { headers =+            Headers.fromObject $+              KeyMap.fromList+                [ (Key.fromString "type", Aeson.toJSON "event"),+                  (Key.fromString "length", Aeson.toJSON $ ByteString.length thePayload),+                  (Key.fromString "event_id", Aeson.toJSON $ Event.eventId event)+                ],+          payload = thePayload+        }++serialize :: Item -> Builder.Builder+serialize item =+  Headers.serialize (headers item)+    <> Builder.char7 '\n'+    <> Builder.byteString (payload item)
source/test-suite/Patrol/ConstantSpec.hs view
@@ -12,6 +12,10 @@     Hspec.it "is correct" $ do       Constant.applicationJson `Hspec.shouldBe` Text.encodeUtf8 (Text.pack "application/json") +  Hspec.describe "applicationXSentryEnvelope" $ do+    Hspec.it "is correct" $ do+      Constant.applicationXSentryEnvelope `Hspec.shouldBe` Text.encodeUtf8 (Text.pack "application/x-sentry-envelope")+   Hspec.describe "sentryVersion" $ do     Hspec.it "is correct" $ do       Constant.sentryVersion `Hspec.shouldBe` Text.singleton '7'
source/test-suite/Patrol/Type/ClientSdkInfoSpec.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}  module Patrol.Type.ClientSdkInfoSpec where@@ -5,6 +6,7 @@ import qualified Data.Aeson as Aeson import qualified Data.Aeson.QQ.Simple as Aeson import qualified Data.Text as Text+import qualified Patrol.Constant as Constant import qualified Patrol.Type.ClientSdkInfo as ClientSdkInfo import qualified Patrol.Type.ClientSdkPackage as ClientSdkPackage import qualified Test.Hspec as Hspec@@ -37,3 +39,8 @@       let clientSdkInfo = ClientSdkInfo.empty {ClientSdkInfo.version = Text.pack "example-version"}           json = [Aeson.aesonQQ| { "version": "example-version" } |]       Aeson.toJSON clientSdkInfo `Hspec.shouldBe` json++  Hspec.describe "patrol" $ do+    Hspec.it "agrees with the user agent" $ do+      let actual = ClientSdkInfo.name ClientSdkInfo.patrol <> "/" <> ClientSdkInfo.version ClientSdkInfo.patrol+      actual `Hspec.shouldBe` Constant.userAgent
source/test-suite/Patrol/Type/DsnSpec.hs view
@@ -5,12 +5,43 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Network.URI.Static as Uri+import qualified Patrol.Exception.Problem as Problem import qualified Patrol.Type.Dsn as Dsn import qualified Patrol.Version as Version import qualified Test.Hspec as Hspec  spec :: Hspec.Spec spec = Hspec.describe "Patrol.Type.Dsn" $ do+  Hspec.describe "fromText" $ do+    Hspec.it "fails with an invalid DSN" $ do+      Dsn.fromText (Text.pack "a:") `Hspec.shouldThrow` (\(Problem.Problem _) -> True)++    Hspec.it "succeeds with a minimal DSN" $ do+      dsn <- Dsn.fromText (Text.pack "a://b@c/d")+      dsn+        `Hspec.shouldBe` Dsn.Dsn+          { Dsn.protocol = Text.singleton 'a',+            Dsn.publicKey = Text.singleton 'b',+            Dsn.secretKey = Text.empty,+            Dsn.host = Text.singleton 'c',+            Dsn.port = Nothing,+            Dsn.path = Text.singleton '/',+            Dsn.projectId = Text.singleton 'd'+          }++    Hspec.it "succeeds with a maximal DSN" $ do+      dsn <- Dsn.fromText (Text.pack "a://b:c@d:5/f/g")+      dsn+        `Hspec.shouldBe` Dsn.Dsn+          { Dsn.protocol = Text.singleton 'a',+            Dsn.publicKey = Text.singleton 'b',+            Dsn.secretKey = Text.singleton 'c',+            Dsn.host = Text.singleton 'd',+            Dsn.port = Just 5,+            Dsn.path = Text.pack "/f/",+            Dsn.projectId = Text.singleton 'g'+          }+   Hspec.describe "fromUri" $ do     Hspec.it "fails with an invalid DSN" $ do       Dsn.fromUri [Uri.uri|a:|] `Hspec.shouldBe` Nothing
+ source/test-suite/Patrol/Type/EnvelopeSpec.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE OverloadedStrings #-}++module Patrol.Type.EnvelopeSpec where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Lazy as LazyByteString+import qualified Data.Text.Encoding as Text+import qualified Network.HTTP.Client as Client+import qualified Network.HTTP.Types as Http+import qualified Patrol.Constant as Constant+import qualified Patrol.Extra.Aeson as Aeson+import qualified Patrol.Type.Dsn as Dsn+import qualified Patrol.Type.Envelope as Envelope+import qualified Patrol.Type.Event as Event+import qualified Patrol.Type.Headers as Headers+import qualified Patrol.Type.Item as Item+import qualified Patrol.Version as Version+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Patrol.Type.Envelope" $ do+  Hspec.describe "serialize" $ do+    Hspec.it "works with no items" $ do+      let actual =+            Builder.toLazyByteString $+              Envelope.serialize+                Envelope.Envelope+                  { Envelope.headers = Headers.empty,+                    Envelope.items = []+                  }+      actual `Hspec.shouldBe` "{}\n"++    Hspec.it "works with one item" $ do+      let actual =+            Builder.toLazyByteString $+              Envelope.serialize+                Envelope.Envelope+                  { Envelope.headers = Headers.fromObject . KeyMap.singleton "a" $ Aeson.toJSON False,+                    Envelope.items =+                      [ Item.Item+                          { Item.headers = Headers.fromObject . KeyMap.singleton "b" $ Aeson.toJSON True,+                            Item.payload = "c"+                          }+                      ]+                  }+      actual `Hspec.shouldBe` "{\"a\":false}\n{\"b\":true}\nc\n"++    Hspec.it "works with two items" $ do+      let actual =+            Builder.toLazyByteString $+              Envelope.serialize+                Envelope.Envelope+                  { Envelope.headers = Headers.fromObject . KeyMap.singleton "a" $ Aeson.toJSON (1 :: Int),+                    Envelope.items =+                      [ Item.Item+                          { Item.headers = Headers.fromObject . KeyMap.singleton "b" $ Aeson.toJSON (2 :: Int),+                            Item.payload = "c"+                          },+                        Item.Item+                          { Item.headers = Headers.fromObject . KeyMap.singleton "d" $ Aeson.toJSON (3 :: Int),+                            Item.payload = "e"+                          }+                      ]+                  }+      actual `Hspec.shouldBe` "{\"a\":1}\n{\"b\":2}\nc\n{\"d\":3}\ne\n"++  Hspec.describe "fromEvent" $ do+    Hspec.it "sets the header" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      event <- Event.fromException (const Nothing) $ userError ""+      let envelope = Envelope.fromEvent dsn event {Event.timestamp = Nothing}+      let expected =+            Headers.fromObject $+              KeyMap.fromList+                [ ("dsn", "http://key@sentry.test/1"),+                  ( "sdk",+                    Aeson.object+                      [ Aeson.pair "name" ("patrol" :: String),+                        Aeson.pair "version" Version.version+                      ]+                  )+                ]+      Envelope.headers envelope `Hspec.shouldBe` expected++    Hspec.it "sets the items" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      event <- Event.fromException (const Nothing) $ userError ""+      let envelope = Envelope.fromEvent dsn event+      Envelope.items envelope `Hspec.shouldNotSatisfy` null++  Hspec.describe "intoRequest" $ do+    Hspec.it "sets the method" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      Client.method request `Hspec.shouldBe` Http.methodPost++    Hspec.it "sets the host" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      Client.host request `Hspec.shouldBe` "sentry.test"++    Hspec.it "sets the port" $ do+      dsn <- Dsn.fromText "http://key@sentry.test:8080/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      Client.port request `Hspec.shouldBe` 8080++    Hspec.it "sets the path" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      Client.path request `Hspec.shouldBe` "/api/1/envelope/"++    Hspec.it "handles a custom path" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/custom/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      Client.path request `Hspec.shouldBe` "/custom/api/1/envelope/"++    Hspec.it "sets the body" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      actual <- case Client.requestBody request of+        Client.RequestBodyBS byteString -> pure byteString+        _ -> fail "unexpected request body"+      let expected = LazyByteString.toStrict . Builder.toLazyByteString $ Envelope.serialize envelope+      actual `Hspec.shouldBe` expected++    Hspec.it "sets the content type" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      lookup Http.hContentType (Client.requestHeaders request) `Hspec.shouldBe` Just Constant.applicationXSentryEnvelope++    Hspec.it "sets the user agent" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      lookup Http.hUserAgent (Client.requestHeaders request) `Hspec.shouldBe` Just (Text.encodeUtf8 Constant.userAgent)++    Hspec.it "sets the authorization" $ do+      dsn <- Dsn.fromText "http://key@sentry.test/1"+      envelope <- Envelope.fromException (const Nothing) dsn $ userError ""+      request <- Envelope.intoRequest dsn envelope+      lookup Constant.xSentryAuth (Client.requestHeaders request) `Hspec.shouldBe` Just (Dsn.intoAuthorization dsn)
source/test-suite/Patrol/Type/EventSpec.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}  module Patrol.Type.EventSpec where@@ -12,7 +13,6 @@ import qualified Data.Time as Time import qualified Network.HTTP.Client as Client import qualified Network.HTTP.Types as Http-import qualified Network.URI.Static as Uri import qualified Patrol.Constant as Constant import qualified Patrol.Type.Breadcrumb as Breadcrumb import qualified Patrol.Type.Breadcrumbs as Breadcrumbs@@ -218,37 +218,37 @@    Hspec.describe "intoRequest" $ do     Hspec.it "sets the method" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       Client.method request `Hspec.shouldBe` Http.methodPost      Hspec.it "sets the host" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test:8080/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test:8080/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       Client.host request `Hspec.shouldBe` Text.encodeUtf8 (Text.pack "sentry.test")      Hspec.it "sets the port" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test:8080/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test:8080/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       Client.port request `Hspec.shouldBe` 8080      Hspec.it "sets the path" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       Client.path request `Hspec.shouldBe` Text.encodeUtf8 (Text.pack "/api/project-id/store/")      Hspec.it "handles a custom path" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/custom/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/custom/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       Client.path request `Hspec.shouldBe` Text.encodeUtf8 (Text.pack "/custom/api/project-id/store/")      Hspec.it "sets the body" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       case Client.requestBody request of@@ -256,19 +256,52 @@         _ -> fail "unexpected request body"      Hspec.it "sets the content type" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       lookup Http.hContentType (Client.requestHeaders request) `Hspec.shouldSatisfy` Maybe.isJust      Hspec.it "sets the user agent" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       lookup Http.hUserAgent (Client.requestHeaders request) `Hspec.shouldSatisfy` Maybe.isJust      Hspec.it "sets the authorization" $ do-      dsn <- maybe (fail "invalid DSN") pure $ Dsn.fromUri [Uri.uri|http://public-key@sentry.test/project-id|]+      dsn <- Dsn.fromText "http://public-key@sentry.test/project-id"       event <- Event.new       request <- Event.intoRequest dsn event       lookup Constant.xSentryAuth (Client.requestHeaders request) `Hspec.shouldSatisfy` Maybe.isJust++  Hspec.describe "fromException" $ do+    Hspec.it "sets the environment" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.environment event `Hspec.shouldBe` Text.pack "production"++    Hspec.it "sets the event ID" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.eventId event `Hspec.shouldNotBe` EventId.empty++    Hspec.it "sets the exception" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.exception event `Hspec.shouldSatisfy` Maybe.isJust++    Hspec.it "sets the level" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.level event `Hspec.shouldBe` Just Level.Error++    Hspec.it "sets the platform" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.platform event `Hspec.shouldBe` Just Platform.Haskell++    Hspec.it "sets the timestamp" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.timestamp event `Hspec.shouldSatisfy` Maybe.isJust++    Hspec.it "sets the type" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.type_ event `Hspec.shouldBe` Just EventType.Default++    Hspec.it "sets the version" $ do+      event <- Event.fromException (const Nothing) $ userError ""+      Event.version event `Hspec.shouldBe` Constant.sentryVersion
source/test-suite/Patrol/Type/ExceptionSpec.hs view
@@ -2,11 +2,11 @@  module Patrol.Type.ExceptionSpec where -import qualified Control.Monad.Catch as Catch import qualified Data.Aeson as Aeson import qualified Data.Aeson.QQ.Simple as Aeson import qualified Data.Map as Map import qualified Data.Text as Text+import qualified GHC.Stack as Stack import qualified Patrol.Type.Exception as Exception import qualified Patrol.Type.Mechanism as Mechanism import qualified Patrol.Type.Stacktrace as Stacktrace@@ -52,11 +52,16 @@           json = [Aeson.aesonQQ| { "value": "example-value" } |]       Aeson.toJSON exception `Hspec.shouldBe` json -  Hspec.describe "fromSomeException" $ do-    let exception = Exception.fromSomeException . Catch.toException $ userError "example-exception-value"+  Hspec.describe "fromException" $ do+    Hspec.it "sets the stacktrace" $ do+      let callStack = Stack.callStack+      let exception = Exception.fromException (const $ Just callStack) $ userError ""+      Exception.stacktrace exception `Hspec.shouldBe` Just (Stacktrace.fromCallStack callStack)      Hspec.it "sets the type" $ do+      let exception = Exception.fromException (const Nothing) $ userError ""       Exception.type_ exception `Hspec.shouldBe` Text.pack "IOException"      Hspec.it "sets the value" $ do+      let exception = Exception.fromException (const Nothing) $ userError "example-exception-value"       Exception.value exception `Hspec.shouldBe` Text.pack "user error (example-exception-value)"
source/test-suite/Patrol/Type/ExceptionsSpec.hs view
@@ -22,3 +22,8 @@           exceptions = Exceptions.empty {Exceptions.values = [exception]}           json = [Aeson.aesonQQ| { "values": [ { "type": "example-type" } ] } |]       Aeson.toJSON exceptions `Hspec.shouldBe` json++  Hspec.describe "fromException" $ do+    Hspec.it "works" $ do+      let exceptions = Exceptions.fromException (const Nothing) $ userError ""+      Exceptions.values exceptions `Hspec.shouldNotSatisfy` null
+ source/test-suite/Patrol/Type/HeadersSpec.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}++module Patrol.Type.HeadersSpec where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.ByteString.Builder as Builder+import qualified Patrol.Type.Headers as Headers+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Patrol.Type.Headers" $ do+  Hspec.describe "serialize" $ do+    Hspec.it "works with an empty object" $ do+      let actual = Builder.toLazyByteString $ Headers.serialize Headers.empty+      actual `Hspec.shouldBe` "{}"++    Hspec.it "works with a singleton object" $ do+      let actual =+            Builder.toLazyByteString+              . Headers.serialize+              . Headers.fromObject+              $ KeyMap.singleton "k" Aeson.Null+      actual `Hspec.shouldBe` "{\"k\":null}"++    Hspec.it "works with a non-empty object" $ do+      let actual =+            Builder.toLazyByteString+              . Headers.serialize+              . Headers.fromObject+              $ KeyMap.fromList [("a", Aeson.toJSON False), ("b", Aeson.toJSON True)]+      actual `Hspec.shouldBe` "{\"a\":false,\"b\":true}"++    Hspec.it "escapes newlines" $ do+      let actual =+            Builder.toLazyByteString+              . Headers.serialize+              . Headers.fromObject+              $ KeyMap.singleton "k" "\n"+      actual `Hspec.shouldBe` "{\"k\":\"\\n\"}"
+ source/test-suite/Patrol/Type/ItemSpec.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}++module Patrol.Type.ItemSpec where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Builder as Builder+import qualified Patrol.Type.Event as Event+import qualified Patrol.Type.Headers as Headers+import qualified Patrol.Type.Item as Item+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Patrol.Type.Item" $ do+  Hspec.describe "serialize" $ do+    Hspec.it "works with an empty payload" $ do+      let actual =+            Builder.toLazyByteString $+              Item.serialize+                Item.Item+                  { Item.headers = Headers.empty,+                    Item.payload = ""+                  }+      actual `Hspec.shouldBe` "{}\n"++    Hspec.it "works with a non-empty payload" $ do+      let actual =+            Builder.toLazyByteString $+              Item.serialize+                Item.Item+                  { Item.headers = Headers.empty,+                    Item.payload = "x"+                  }+      actual `Hspec.shouldBe` "{}\nx"++    Hspec.it "allows newlines in the payload" $ do+      -- <https://develop.sentry.dev/sdk/data-model/envelopes/#items>+      -- If no `length` is specified, the payload implicitly goes to the next+      -- newline. For payloads containing newline characters, the `length` must+      -- be specified.+      let actual =+            Builder.toLazyByteString $+              Item.serialize+                Item.Item+                  { Item.headers =+                      Headers.fromObject+                        . KeyMap.singleton "length"+                        $ Aeson.toJSON (3 :: Int),+                    Item.payload = "a\nb"+                  }+      actual `Hspec.shouldBe` "{\"length\":3}\na\nb"++  Hspec.describe "fromEvent" $ do+    Hspec.it "sets the headers" $ do+      let event = Event.empty+      let actual = Item.fromEvent event+      let expected =+            Headers.fromObject $+              KeyMap.fromList+                [ ("type", "event"),+                  ("length", Aeson.toJSON (47 :: Int)),+                  ("event_id", Aeson.toJSON $ Event.eventId event)+                ]+      Item.headers actual `Hspec.shouldBe` expected++    Hspec.it "sets the payload" $ do+      let event = Event.empty+      let actual = Item.fromEvent event+      let expected = "{\"event_id\":\"00000000000000000000000000000000\"}" :: ByteString.ByteString+      Item.payload actual `Hspec.shouldBe` expected
source/test-suite/PatrolSpec.hs view
@@ -18,6 +18,7 @@ import qualified Patrol.Type.DebugMetaSpec import qualified Patrol.Type.DeviceContextSpec import qualified Patrol.Type.DsnSpec+import qualified Patrol.Type.EnvelopeSpec import qualified Patrol.Type.ErrorTypeSpec import qualified Patrol.Type.EventIdSpec import qualified Patrol.Type.EventProcessingErrorSpec@@ -28,6 +29,8 @@ import qualified Patrol.Type.FrameSpec import qualified Patrol.Type.GeoSpec import qualified Patrol.Type.GpuContextSpec+import qualified Patrol.Type.HeadersSpec+import qualified Patrol.Type.ItemSpec import qualified Patrol.Type.LevelSpec import qualified Patrol.Type.LogEntrySpec import qualified Patrol.Type.MachExceptionSpec@@ -73,6 +76,9 @@   Patrol.Type.DebugMetaSpec.spec   Patrol.Type.DeviceContextSpec.spec   Patrol.Type.DsnSpec.spec+  Patrol.Type.EnvelopeSpec.spec+  Patrol.Type.HeadersSpec.spec+  Patrol.Type.ItemSpec.spec   Patrol.Type.ErrorTypeSpec.spec   Patrol.Type.EventIdSpec.spec   Patrol.Type.EventProcessingErrorSpec.spec