packages feed

patrol 1.2.0.5 → 1.3.0.0

raw patch · 14 files changed

+418/−9 lines, 14 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Patrol: data ClientReport
+ Patrol: data DataCategory
+ Patrol: data DiscardedEvent
+ Patrol.Type.ClientReport: ClientReport :: Maybe UTCTime -> [DiscardedEvent] -> ClientReport
+ Patrol.Type.ClientReport: [discardedEvents] :: ClientReport -> [DiscardedEvent]
+ Patrol.Type.ClientReport: [timestamp] :: ClientReport -> Maybe UTCTime
+ Patrol.Type.ClientReport: data ClientReport
+ Patrol.Type.ClientReport: empty :: ClientReport
+ Patrol.Type.ClientReport: instance Data.Aeson.Types.ToJSON.ToJSON Patrol.Type.ClientReport.ClientReport
+ Patrol.Type.ClientReport: instance GHC.Classes.Eq Patrol.Type.ClientReport.ClientReport
+ Patrol.Type.ClientReport: instance GHC.Internal.Show.Show Patrol.Type.ClientReport.ClientReport
+ Patrol.Type.DataCategory: Attachment :: DataCategory
+ Patrol.Type.DataCategory: Default :: DataCategory
+ Patrol.Type.DataCategory: Error :: DataCategory
+ Patrol.Type.DataCategory: Feedback :: DataCategory
+ Patrol.Type.DataCategory: Internal :: DataCategory
+ Patrol.Type.DataCategory: LogItem :: DataCategory
+ Patrol.Type.DataCategory: Monitor :: DataCategory
+ Patrol.Type.DataCategory: Profile :: DataCategory
+ Patrol.Type.DataCategory: ProfileChunk :: DataCategory
+ Patrol.Type.DataCategory: Replay :: DataCategory
+ Patrol.Type.DataCategory: Security :: DataCategory
+ Patrol.Type.DataCategory: Session :: DataCategory
+ Patrol.Type.DataCategory: Span :: DataCategory
+ Patrol.Type.DataCategory: TraceMetric :: DataCategory
+ Patrol.Type.DataCategory: Transaction :: DataCategory
+ Patrol.Type.DataCategory: data DataCategory
+ Patrol.Type.DataCategory: fromText :: MonadThrow m => Text -> m DataCategory
+ Patrol.Type.DataCategory: instance Data.Aeson.Types.FromJSON.FromJSON Patrol.Type.DataCategory.DataCategory
+ Patrol.Type.DataCategory: instance Data.Aeson.Types.ToJSON.ToJSON Patrol.Type.DataCategory.DataCategory
+ Patrol.Type.DataCategory: instance GHC.Classes.Eq Patrol.Type.DataCategory.DataCategory
+ Patrol.Type.DataCategory: instance GHC.Classes.Ord Patrol.Type.DataCategory.DataCategory
+ Patrol.Type.DataCategory: instance GHC.Internal.Show.Show Patrol.Type.DataCategory.DataCategory
+ Patrol.Type.DataCategory: intoText :: DataCategory -> Text
+ Patrol.Type.DiscardedEvent: DiscardedEvent :: Text -> DataCategory -> Int -> DiscardedEvent
+ Patrol.Type.DiscardedEvent: [category] :: DiscardedEvent -> DataCategory
+ Patrol.Type.DiscardedEvent: [quantity] :: DiscardedEvent -> Int
+ Patrol.Type.DiscardedEvent: [reason] :: DiscardedEvent -> Text
+ Patrol.Type.DiscardedEvent: data DiscardedEvent
+ Patrol.Type.DiscardedEvent: instance Data.Aeson.Types.ToJSON.ToJSON Patrol.Type.DiscardedEvent.DiscardedEvent
+ Patrol.Type.DiscardedEvent: instance GHC.Classes.Eq Patrol.Type.DiscardedEvent.DiscardedEvent
+ Patrol.Type.DiscardedEvent: instance GHC.Internal.Show.Show Patrol.Type.DiscardedEvent.DiscardedEvent
+ Patrol.Type.Item: ClientReport :: ClientReport -> Item

Files

README.md view
@@ -12,6 +12,10 @@ The simplest way to report an exception is with `captureException`. It reads the DSN from the `SENTRY_DSN` environment variable. +Note: `captureException` itself can throw exceptions, for example if+the call to Sentry returns a non-success HTTP code or the network is+down.+ ```haskell import qualified Patrol @@ -30,6 +34,9 @@ Use `captureExceptionWith` to add tags, set the environment, attach user info, or otherwise modify the event before it is sent. +As with `captureException`, `captureExceptionWith` itself can throw+exceptions.+ ```haskell import qualified Data.Map as Map import qualified Data.Text as Text@@ -90,4 +97,16 @@           , (Text.pack "status_code", Text.pack (show (Http.statusCode status)))           ]       }+```++If you want the response handler to `respond` even if the call to+Sentry throws an exception, you can e.g. wrap the call in `catch` or+`try`, like++```haskell+import Control.Exception (try) -- or UnliftIO.Exception(try)++[...]++      _ <- try (Patrol.captureExceptionWith (modifyEvent request status) dsn err) :: IO (Either SomeException Patrol.Response) ```
patrol.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: patrol-version: 1.2.0.5+version: 1.3.0.0 synopsis: Sentry SDK description: Patrol is a Sentry SDK. build-type: Simple@@ -79,12 +79,15 @@     Patrol.Type.BreadcrumbType     Patrol.Type.BrowserContext     Patrol.Type.CError+    Patrol.Type.ClientReport     Patrol.Type.ClientSdkInfo     Patrol.Type.ClientSdkPackage     Patrol.Type.Context+    Patrol.Type.DataCategory     Patrol.Type.DebugImage     Patrol.Type.DebugMeta     Patrol.Type.DeviceContext+    Patrol.Type.DiscardedEvent     Patrol.Type.Dsn     Patrol.Type.Envelope     Patrol.Type.ErrorType@@ -159,12 +162,15 @@     Patrol.Type.BreadcrumbTypeSpec     Patrol.Type.BrowserContextSpec     Patrol.Type.CErrorSpec+    Patrol.Type.ClientReportSpec     Patrol.Type.ClientSdkInfoSpec     Patrol.Type.ClientSdkPackageSpec     Patrol.Type.ContextSpec+    Patrol.Type.DataCategorySpec     Patrol.Type.DebugImageSpec     Patrol.Type.DebugMetaSpec     Patrol.Type.DeviceContextSpec+    Patrol.Type.DiscardedEventSpec     Patrol.Type.DsnSpec     Patrol.Type.EnvelopeSpec     Patrol.Type.ErrorTypeSpec
source/library/Patrol.hs view
@@ -9,12 +9,15 @@     Patrol.Type.BreadcrumbType.BreadcrumbType,     Patrol.Type.BrowserContext.BrowserContext,     Patrol.Type.CError.CError,+    Patrol.Type.ClientReport.ClientReport,     Patrol.Type.ClientSdkInfo.ClientSdkInfo,     Patrol.Type.ClientSdkPackage.ClientSdkPackage,     Patrol.Type.Context.Context,+    Patrol.Type.DataCategory.DataCategory,     Patrol.Type.DebugImage.DebugImage,     Patrol.Type.DebugMeta.DebugMeta,     Patrol.Type.DeviceContext.DeviceContext,+    Patrol.Type.DiscardedEvent.DiscardedEvent,     Patrol.Type.Dsn.Dsn,     Patrol.Type.Envelope.Envelope,     Patrol.Type.ErrorType.ErrorType,@@ -64,12 +67,15 @@ import qualified Patrol.Type.Breadcrumbs import qualified Patrol.Type.BrowserContext import qualified Patrol.Type.CError+import qualified Patrol.Type.ClientReport import qualified Patrol.Type.ClientSdkInfo import qualified Patrol.Type.ClientSdkPackage import qualified Patrol.Type.Context+import qualified Patrol.Type.DataCategory import qualified Patrol.Type.DebugImage import qualified Patrol.Type.DebugMeta import qualified Patrol.Type.DeviceContext+import qualified Patrol.Type.DiscardedEvent import qualified Patrol.Type.Dsn import qualified Patrol.Type.Envelope import qualified Patrol.Type.ErrorType
source/library/Patrol/Client.hs view
@@ -19,6 +19,8 @@ -- | Capture an exception by sending it to Sentry. The DSN is read from the -- @SENTRY_DSN@ environment variable. To customize the behavior, use -- 'captureExceptionWith'.+--+-- May throw an exception of the call to Sentry fails. captureException ::   (Catch.Exception e, IO.MonadIO io, Catch.MonadThrow io) =>   e ->@@ -29,6 +31,13 @@     Dsn.fromText $ maybe Text.empty Text.pack maybeString   captureExceptionWith pure dsn e +-- | Capture an exception by sending it to Sentry, possibly modifying+-- the event first.+--+-- Unlike `captureException`, this gets the DSN from an argument+-- instead of from environment variables.+--+-- May throw an exception of the call to Sentry fails. captureExceptionWith ::   (Catch.Exception e, IO.MonadIO io, Catch.MonadThrow io) =>   -- | How to modify the 'Event.Event' before it is sent. Use @'pure'@ if you
+ source/library/Patrol/Type/ClientReport.hs view
@@ -0,0 +1,30 @@+module Patrol.Type.ClientReport where++import qualified Data.Aeson as Aeson+import qualified Data.Time as Time+import qualified Patrol.Extra.Aeson as Aeson+import qualified Patrol.Type.DiscardedEvent as DiscardedEvent++-- | A client report envelope item, used to report locally-discarded telemetry+-- back to Sentry.+--+-- <https://develop.sentry.dev/sdk/telemetry/client-reports/>+data ClientReport = ClientReport+  { timestamp :: Maybe Time.UTCTime,+    discardedEvents :: [DiscardedEvent.DiscardedEvent]+  }+  deriving (Eq, Show)++instance Aeson.ToJSON ClientReport where+  toJSON clientReport =+    Aeson.intoObject+      [ Aeson.pair "timestamp" $ timestamp clientReport,+        Aeson.pair "discarded_events" $ discardedEvents clientReport+      ]++empty :: ClientReport+empty =+  ClientReport+    { timestamp = Nothing,+      discardedEvents = []+    }
+ source/library/Patrol/Type/DataCategory.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}++module Patrol.Type.DataCategory where++import qualified Control.Monad.Catch as Catch+import qualified Data.Aeson as Aeson+import qualified Data.Text as Text+import qualified Data.Typeable as Typeable+import qualified Patrol.Exception.Problem as Problem++-- | The data category associated with discarded and/or rate-limited events.+--+-- <https://develop.sentry.dev/sdk/foundations/transport/rate-limiting/#definitions>+-- <https://develop.sentry.dev/sdk/telemetry/client-reports/#data-categories>+data DataCategory+  = Default+  | Error+  | Transaction+  | Monitor+  | Span+  | LogItem+  | Security+  | Attachment+  | Session+  | Profile+  | ProfileChunk+  | Replay+  | Feedback+  | TraceMetric+  | Internal+  deriving (Eq, Ord, Show)++instance Aeson.FromJSON DataCategory where+  parseJSON =+    let name = show $ Typeable.typeRep (Typeable.Proxy :: Typeable.Proxy DataCategory)+     in Aeson.withText name $ maybe (fail $ "invalid " <> name) pure . fromText++instance Aeson.ToJSON DataCategory where+  toJSON = Aeson.toJSON . intoText++-- | Render a 'DataCategory' to its canonical wire format.+intoText :: DataCategory -> Text.Text+intoText dataCategory = case dataCategory of+  Attachment -> "attachment"+  Default -> "default"+  Error -> "error"+  Feedback -> "feedback"+  Internal -> "internal"+  LogItem -> "log_item"+  Monitor -> "monitor"+  Profile -> "profile"+  ProfileChunk -> "profile_chunk"+  Replay -> "replay"+  Security -> "security"+  Session -> "session"+  Span -> "span"+  TraceMetric -> "trace_metric"+  Transaction -> "transaction"++-- | Attempt to parse the given string into a 'DataCategory'; the inverse of+-- 'intoText'.+fromText :: (Catch.MonadThrow m) => Text.Text -> m DataCategory+fromText text = case text of+  "attachment" -> pure Attachment+  "default" -> pure Default+  "error" -> pure Error+  "feedback" -> pure Feedback+  "internal" -> pure Internal+  "log_item" -> pure LogItem+  "monitor" -> pure Monitor+  "profile" -> pure Profile+  "profile_chunk" -> pure ProfileChunk+  "replay" -> pure Replay+  "security" -> pure Security+  "session" -> pure Session+  "span" -> pure Span+  "trace_metric" -> pure TraceMetric+  "transaction" -> pure Transaction+  _ -> Catch.throwM . Problem.Problem $ "invalid DataCategory: " <> show text
+ source/library/Patrol/Type/DiscardedEvent.hs view
@@ -0,0 +1,25 @@+module Patrol.Type.DiscardedEvent where++import qualified Data.Aeson as Aeson+import qualified Data.Text as Text+import qualified Patrol.Extra.Aeson as Aeson+import qualified Patrol.Type.DataCategory as DataCategory++-- | A single entry in a client report describing how many events were discarded+-- for a given reason and data category.+--+-- <https://develop.sentry.dev/sdk/telemetry/client-reports/#envelope-item-payload>+data DiscardedEvent = DiscardedEvent+  { reason :: Text.Text,+    category :: DataCategory.DataCategory,+    quantity :: Int+  }+  deriving (Eq, Show)++instance Aeson.ToJSON DiscardedEvent where+  toJSON discardedEvent =+    Aeson.intoObject+      [ Aeson.pair "reason" $ reason discardedEvent,+        Aeson.pair "category" $ category discardedEvent,+        Aeson.pair "quantity" $ quantity discardedEvent+      ]
source/library/Patrol/Type/EventId.hs view
@@ -4,13 +4,15 @@ import qualified Control.Monad.Catch as Catch import qualified Control.Monad.IO.Class as IO import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Lazy as LazyByteString import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text import qualified Data.Text.Read as Text import qualified Data.Typeable as Typeable import qualified Data.UUID as Uuid import qualified Data.UUID.V4 as Uuid import qualified Patrol.Exception.Problem as Problem-import qualified Text.Printf as Printf  newtype EventId   = EventId Uuid.UUID@@ -36,10 +38,16 @@ random :: (IO.MonadIO io) => io EventId random = IO.liftIO $ fmap fromUuid Uuid.nextRandom +-- | Render an 'EventId' as 32 zero-padded lowercase hexadecimal digits.+--+-- __NOTE__: The output is ASCII, so 'Text.decodeLatin1' is total. intoText :: EventId -> Text.Text intoText eventId =   let (lo, hi) = Uuid.toWords64 $ intoUuid eventId-   in Text.pack $ Printf.printf "%016x%016x" lo hi+   in Text.decodeLatin1+        . LazyByteString.toStrict+        . Builder.toLazyByteString+        $ Builder.word64HexFixed lo <> Builder.word64HexFixed hi  fromText :: (Catch.MonadThrow m) => Text.Text -> m EventId fromText t1 = do
source/library/Patrol/Type/Item.hs view
@@ -4,9 +4,10 @@  import qualified Data.Aeson as Aeson import qualified Data.Aeson.Key as Key-import qualified Data.ByteString as ByteString import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy as LazyByteString+import Data.Int (Int64)+import qualified Patrol.Type.ClientReport as ClientReport import qualified Patrol.Type.Event as Event  -- | <https://develop.sentry.dev/sdk/data-model/envelope-items/>@@ -15,19 +16,31 @@     --     -- <https://develop.sentry.dev/sdk/envelopes/#event>     Event Event.Event+  | -- | A 'Patrol.Type.ClientReport.ClientReport' item, used to report+    -- locally-discarded telemetry back to Sentry.+    --+    -- <https://develop.sentry.dev/sdk/telemetry/client-reports/>+    ClientReport ClientReport.ClientReport   | -- | A sentinel item used to filter raw envelopes.     Raw   deriving (Eq, Show)  serialize :: Item -> Builder.Builder serialize item = case item of-  Event event ->-    let payload = LazyByteString.toStrict $ Aeson.encode event-        headers = buildHeaders "event" (ByteString.length payload)-     in headers <> Builder.char7 '\n' <> Builder.byteString payload+  Event event -> envelopeItem "event" (Aeson.encode event)+  ClientReport clientReport -> envelopeItem "client_report" (Aeson.encode clientReport)   Raw -> mempty   where-    buildHeaders :: Key.Key -> Int -> Builder.Builder+    -- Keep the encoded payload lazy: take the item-header length from the lazy+    -- chunks and stream them straight into the builder, rather than forcing a+    -- contiguous strict copy of every payload.+    envelopeItem :: Key.Key -> LazyByteString.ByteString -> Builder.Builder+    envelopeItem type_ payload =+      buildHeaders type_ (LazyByteString.length payload)+        <> Builder.char7 '\n'+        <> Builder.lazyByteString payload++    buildHeaders :: Key.Key -> Int64 -> Builder.Builder     buildHeaders type_ length_ =       Aeson.fromEncoding $         Aeson.pairs ("type" Aeson..= type_ <> "length" Aeson..= length_)
+ source/test-suite/Patrol/Type/ClientReportSpec.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Patrol.Type.ClientReportSpec where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.QQ.Simple as Aeson+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Char8 as Char8+import qualified Data.ByteString.Lazy as LazyByteString+import qualified Patrol.Type.ClientReport as ClientReport+import qualified Patrol.Type.DataCategory as DataCategory+import qualified Patrol.Type.DiscardedEvent as DiscardedEvent+import qualified Patrol.Type.Item as Item+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Patrol.Type.ClientReport" $ do+  Hspec.describe "ToJSON" $ do+    Hspec.it "omits timestamp when absent" $ do+      let report =+            ClientReport.ClientReport+              { ClientReport.timestamp = Nothing,+                ClientReport.discardedEvents =+                  [ DiscardedEvent.DiscardedEvent+                      { DiscardedEvent.reason = "sample_rate",+                        DiscardedEvent.category = DataCategory.Error,+                        DiscardedEvent.quantity = 3+                      }+                  ]+              }+      Aeson.toJSON report+        `Hspec.shouldBe` [Aeson.aesonQQ| {"discarded_events": [{"reason": "sample_rate", "category": "error", "quantity": 3}]} |]++    Hspec.it "omits discarded_events when empty" $ do+      Aeson.toJSON ClientReport.empty+        `Hspec.shouldBe` [Aeson.aesonQQ| {} |]++  Hspec.describe "Item.serialize" $ do+    Hspec.it "frames a ClientReport item with the correct type header" $ do+      let report =+            ClientReport.ClientReport+              { ClientReport.timestamp = Nothing,+                ClientReport.discardedEvents =+                  [ DiscardedEvent.DiscardedEvent+                      { DiscardedEvent.reason = "sample_rate",+                        DiscardedEvent.category = DataCategory.Error,+                        DiscardedEvent.quantity = 3+                      }+                  ]+              }+          actual =+            LazyByteString.toStrict+              . Builder.toLazyByteString+              . Item.serialize+              $ Item.ClientReport report+          -- The serialized item is a one-line JSON header, a newline, then the+          -- payload. aeson emits no newlines, so the separator inserted by+          -- 'Item.serialize' is the only one and splitting on it is safe.+          (header, rest) = Char8.break (== '\n') actual+          payload = ByteString.drop 1 rest+      -- Assert the header framing and that it declares the payload's length.+      Aeson.decodeStrict header+        `Hspec.shouldBe` Just+          ( Aeson.object+              [ "type" Aeson..= ("client_report" :: String),+                "length" Aeson..= ByteString.length payload+              ]+          )+      -- Compare the payload as decoded JSON so the assertion does not depend on+      -- aeson's (semantically meaningless) object key ordering.+      Aeson.decodeStrict payload+        `Hspec.shouldBe` Just [Aeson.aesonQQ| {"discarded_events": [{"reason": "sample_rate", "category": "error", "quantity": 3}]} |]
+ source/test-suite/Patrol/Type/DataCategorySpec.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE QuasiQuotes #-}++module Patrol.Type.DataCategorySpec where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.QQ.Simple as Aeson+import Data.Foldable (for_)+import qualified Patrol.Type.DataCategory as DataCategory+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Patrol.Type.DataCategory" $ do+  Hspec.describe "ToJSON" $ do+    Hspec.it "works for Attachment" $ do+      Aeson.toJSON DataCategory.Attachment `Hspec.shouldBe` [Aeson.aesonQQ| "attachment" |]++    Hspec.it "works for Default" $ do+      Aeson.toJSON DataCategory.Default `Hspec.shouldBe` [Aeson.aesonQQ| "default" |]++    Hspec.it "works for Error" $ do+      Aeson.toJSON DataCategory.Error `Hspec.shouldBe` [Aeson.aesonQQ| "error" |]++    Hspec.it "works for Feedback" $ do+      Aeson.toJSON DataCategory.Feedback `Hspec.shouldBe` [Aeson.aesonQQ| "feedback" |]++    Hspec.it "works for Internal" $ do+      Aeson.toJSON DataCategory.Internal `Hspec.shouldBe` [Aeson.aesonQQ| "internal" |]++    Hspec.it "works for LogItem" $ do+      Aeson.toJSON DataCategory.LogItem `Hspec.shouldBe` [Aeson.aesonQQ| "log_item" |]++    Hspec.it "works for Monitor" $ do+      Aeson.toJSON DataCategory.Monitor `Hspec.shouldBe` [Aeson.aesonQQ| "monitor" |]++    Hspec.it "works for Profile" $ do+      Aeson.toJSON DataCategory.Profile `Hspec.shouldBe` [Aeson.aesonQQ| "profile" |]++    Hspec.it "works for ProfileChunk" $ do+      Aeson.toJSON DataCategory.ProfileChunk `Hspec.shouldBe` [Aeson.aesonQQ| "profile_chunk" |]++    Hspec.it "works for Replay" $ do+      Aeson.toJSON DataCategory.Replay `Hspec.shouldBe` [Aeson.aesonQQ| "replay" |]++    Hspec.it "works for Security" $ do+      Aeson.toJSON DataCategory.Security `Hspec.shouldBe` [Aeson.aesonQQ| "security" |]++    Hspec.it "works for Session" $ do+      Aeson.toJSON DataCategory.Session `Hspec.shouldBe` [Aeson.aesonQQ| "session" |]++    Hspec.it "works for Span" $ do+      Aeson.toJSON DataCategory.Span `Hspec.shouldBe` [Aeson.aesonQQ| "span" |]++    Hspec.it "works for TraceMetric" $ do+      Aeson.toJSON DataCategory.TraceMetric `Hspec.shouldBe` [Aeson.aesonQQ| "trace_metric" |]++    Hspec.it "works for Transaction" $ do+      Aeson.toJSON DataCategory.Transaction `Hspec.shouldBe` [Aeson.aesonQQ| "transaction" |]++  Hspec.describe "FromJSON" $ do+    Hspec.it "fails for an unknown token" $ do+      Aeson.fromJSON [Aeson.aesonQQ| "bogus" |] `Hspec.shouldBe` (Aeson.Error "invalid DataCategory" :: Aeson.Result DataCategory.DataCategory)++    Hspec.it "round-trips every category through ToJSON" $ do+      let categories =+            [ DataCategory.Attachment,+              DataCategory.Default,+              DataCategory.Error,+              DataCategory.Feedback,+              DataCategory.Internal,+              DataCategory.LogItem,+              DataCategory.Monitor,+              DataCategory.Profile,+              DataCategory.ProfileChunk,+              DataCategory.Replay,+              DataCategory.Security,+              DataCategory.Session,+              DataCategory.Span,+              DataCategory.TraceMetric,+              DataCategory.Transaction+            ]+      for_ categories $ \c ->+        Aeson.fromJSON (Aeson.toJSON c) `Hspec.shouldBe` Aeson.Success c
+ source/test-suite/Patrol/Type/DiscardedEventSpec.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Patrol.Type.DiscardedEventSpec where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.QQ.Simple as Aeson+import qualified Patrol.Type.DataCategory as DataCategory+import qualified Patrol.Type.DiscardedEvent as DiscardedEvent+import qualified Test.Hspec as Hspec++spec :: Hspec.Spec+spec = Hspec.describe "Patrol.Type.DiscardedEvent" $ do+  Hspec.describe "ToJSON" $ do+    Hspec.it "serializes all fields" $ do+      let discardedEvent =+            DiscardedEvent.DiscardedEvent+              { DiscardedEvent.reason = "sample_rate",+                DiscardedEvent.category = DataCategory.Error,+                DiscardedEvent.quantity = 3+              }+      Aeson.toJSON discardedEvent+        `Hspec.shouldBe` [Aeson.aesonQQ| {"reason": "sample_rate", "category": "error", "quantity": 3} |]++    Hspec.it "serializes quantity of 0" $ do+      let discardedEvent =+            DiscardedEvent.DiscardedEvent+              { DiscardedEvent.reason = "before_send",+                DiscardedEvent.category = DataCategory.Transaction,+                DiscardedEvent.quantity = 0+              }+      Aeson.toJSON discardedEvent+        `Hspec.shouldBe` [Aeson.aesonQQ| {"reason": "before_send", "category": "transaction", "quantity": 0} |]
source/test-suite/Patrol/Type/ItemSpec.hs view
@@ -5,6 +5,7 @@ import qualified Data.ByteString as ByteString import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy as LazyByteString+import qualified Patrol.Type.ClientReport as ClientReport import qualified Patrol.Type.Event as Event import qualified Patrol.Type.Item as Item import qualified Test.Hspec as Hspec@@ -19,4 +20,22 @@               . Item.serialize               $ Item.Event Event.empty       let expected = "{\"type\":\"event\",\"length\":47}\n{\"event_id\":\"00000000000000000000000000000000\"}" :: ByteString.ByteString+      actual `Hspec.shouldBe` expected++    Hspec.it "works on ClientReport" $ do+      let actual =+            LazyByteString.toStrict+              . Builder.toLazyByteString+              . Item.serialize+              $ Item.ClientReport ClientReport.empty+      let expected = "{\"type\":\"client_report\",\"length\":2}\n{}" :: ByteString.ByteString+      actual `Hspec.shouldBe` expected++    Hspec.it "works on Raw" $ do+      let actual =+            LazyByteString.toStrict+              . Builder.toLazyByteString+              . Item.serialize+              $ Item.Raw+      let expected = "" :: ByteString.ByteString       actual `Hspec.shouldBe` expected
source/test-suite/PatrolSpec.hs view
@@ -11,12 +11,15 @@ import qualified Patrol.Type.BreadcrumbsSpec import qualified Patrol.Type.BrowserContextSpec import qualified Patrol.Type.CErrorSpec+import qualified Patrol.Type.ClientReportSpec import qualified Patrol.Type.ClientSdkInfoSpec import qualified Patrol.Type.ClientSdkPackageSpec import qualified Patrol.Type.ContextSpec+import qualified Patrol.Type.DataCategorySpec import qualified Patrol.Type.DebugImageSpec import qualified Patrol.Type.DebugMetaSpec import qualified Patrol.Type.DeviceContextSpec+import qualified Patrol.Type.DiscardedEventSpec import qualified Patrol.Type.DsnSpec import qualified Patrol.Type.EnvelopeSpec import qualified Patrol.Type.ErrorTypeSpec@@ -70,12 +73,15 @@   Patrol.Type.BreadcrumbTypeSpec.spec   Patrol.Type.BrowserContextSpec.spec   Patrol.Type.CErrorSpec.spec+  Patrol.Type.ClientReportSpec.spec   Patrol.Type.ClientSdkInfoSpec.spec   Patrol.Type.ClientSdkPackageSpec.spec   Patrol.Type.ContextSpec.spec+  Patrol.Type.DataCategorySpec.spec   Patrol.Type.DebugImageSpec.spec   Patrol.Type.DebugMetaSpec.spec   Patrol.Type.DeviceContextSpec.spec+  Patrol.Type.DiscardedEventSpec.spec   Patrol.Type.DsnSpec.spec   Patrol.Type.EnvelopeSpec.spec   Patrol.Type.HeadersSpec.spec