diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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)
 ```
diff --git a/patrol.cabal b/patrol.cabal
--- a/patrol.cabal
+++ b/patrol.cabal
@@ -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
diff --git a/source/library/Patrol.hs b/source/library/Patrol.hs
--- a/source/library/Patrol.hs
+++ b/source/library/Patrol.hs
@@ -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
diff --git a/source/library/Patrol/Client.hs b/source/library/Patrol/Client.hs
--- a/source/library/Patrol/Client.hs
+++ b/source/library/Patrol/Client.hs
@@ -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
diff --git a/source/library/Patrol/Type/ClientReport.hs b/source/library/Patrol/Type/ClientReport.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Patrol/Type/ClientReport.hs
@@ -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 = []
+    }
diff --git a/source/library/Patrol/Type/DataCategory.hs b/source/library/Patrol/Type/DataCategory.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Patrol/Type/DataCategory.hs
@@ -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
diff --git a/source/library/Patrol/Type/DiscardedEvent.hs b/source/library/Patrol/Type/DiscardedEvent.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Patrol/Type/DiscardedEvent.hs
@@ -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
+      ]
diff --git a/source/library/Patrol/Type/EventId.hs b/source/library/Patrol/Type/EventId.hs
--- a/source/library/Patrol/Type/EventId.hs
+++ b/source/library/Patrol/Type/EventId.hs
@@ -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
diff --git a/source/library/Patrol/Type/Item.hs b/source/library/Patrol/Type/Item.hs
--- a/source/library/Patrol/Type/Item.hs
+++ b/source/library/Patrol/Type/Item.hs
@@ -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_)
diff --git a/source/test-suite/Patrol/Type/ClientReportSpec.hs b/source/test-suite/Patrol/Type/ClientReportSpec.hs
new file mode 100644
--- /dev/null
+++ b/source/test-suite/Patrol/Type/ClientReportSpec.hs
@@ -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}]} |]
diff --git a/source/test-suite/Patrol/Type/DataCategorySpec.hs b/source/test-suite/Patrol/Type/DataCategorySpec.hs
new file mode 100644
--- /dev/null
+++ b/source/test-suite/Patrol/Type/DataCategorySpec.hs
@@ -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
diff --git a/source/test-suite/Patrol/Type/DiscardedEventSpec.hs b/source/test-suite/Patrol/Type/DiscardedEventSpec.hs
new file mode 100644
--- /dev/null
+++ b/source/test-suite/Patrol/Type/DiscardedEventSpec.hs
@@ -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} |]
diff --git a/source/test-suite/Patrol/Type/ItemSpec.hs b/source/test-suite/Patrol/Type/ItemSpec.hs
--- a/source/test-suite/Patrol/Type/ItemSpec.hs
+++ b/source/test-suite/Patrol/Type/ItemSpec.hs
@@ -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
diff --git a/source/test-suite/PatrolSpec.hs b/source/test-suite/PatrolSpec.hs
--- a/source/test-suite/PatrolSpec.hs
+++ b/source/test-suite/PatrolSpec.hs
@@ -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
