<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sentry</title>
    <description>The latest articles on DEV Community by Sentry (sentry).</description>
    <link>https://dev.to/sentry</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3774%2F99e0624e-6fb6-4460-819d-3a0d967519cb.webp</url>
      <title>DEV Community: Sentry</title>
      <link>https://dev.to/sentry</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sentry"/>
    <language>en</language>
    <item>
      <title>Sentry vs OpenTelemetry: You Don’t Need to Pick One</title>
      <dc:creator>Anton Bjorkman</dc:creator>
      <pubDate>Mon, 22 Jun 2026 21:23:57 +0000</pubDate>
      <link>https://dev.to/sentry/sentry-vs-opentelemetry-you-dont-need-to-pick-one-36ed</link>
      <guid>https://dev.to/sentry/sentry-vs-opentelemetry-you-dont-need-to-pick-one-36ed</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — If your backend already uses OpenTelemetry, you can send traces and logs to Sentry by changing a few environment variables. No SDK swap, no instrumentation rewrite. Point your OTLP exporter at Sentry’s endpoint, add the Sentry SDK on the frontend for browser context, and you get one connected trace from click to backend span.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You already instrumented the backend with OpenTelemetry. Your services emit spans. Your teams know the OTel APIs. Maybe you already run a Collector. So when you start evaluating Sentry, the obvious question is:&lt;/p&gt;

&lt;p&gt;Do you need to replace your OpenTelemetry setup with the Sentry SDK?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;The practical answer is usually: keep OpenTelemetry where it already works, add the Sentry SDK where it gives you more application context, and send OpenTelemetry Protocol (OTLP) events to Sentry. For a web app, that often means using the Sentry SDK on the frontend for browser tracing, errors, &lt;a href="https://sentry.io/product/logs/" rel="noopener noreferrer"&gt;logs&lt;/a&gt;, &lt;a href="https://sentry.io/product/session-replay/" rel="noopener noreferrer"&gt;Session Replay&lt;/a&gt;, and source maps, while keeping OpenTelemetry on the backend for existing service instrumentation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One scope note: OTLP can carry traces, logs, and metrics. At this moment, Sentry’s OTLP ingest supports logs and traces, not metrics. We’re considering adding support for them in the future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important part is separating two decisions that often get lumped together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  How traces stay connected across frontend and backend.&lt;/li&gt;
&lt;li&gt;  How backend OTLP events are exported to Sentry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you separate those, the architecture gets a lot easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sentry vs OpenTelemetry is the wrong question
&lt;/h2&gt;

&lt;p&gt;The first decision is trace linking. If a user clicks a button in your React app and that click triggers a backend request, the frontend and backend need to agree on the same &lt;a href="https://sentry.io/product/tracing/" rel="noopener noreferrer"&gt;distributed trace&lt;/a&gt; context. In this example, the Sentry frontend SDK sends W3C &lt;code&gt;traceparent&lt;/code&gt; headers (configurable through the &lt;code&gt;propagateTraceparent&lt;/code&gt; option), and the OpenTelemetry backend continues the trace.&lt;/p&gt;

&lt;p&gt;That linking is handled by the frontend SDK configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browserTracingIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;tracesSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ensure traceparent headers get sent&lt;/span&gt;
  &lt;span class="na"&gt;propagateTraceparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tracePropagationTargets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sr"&gt;/^http:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;localhost:8000&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sr"&gt;/^http:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;127&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;1:8000&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// your backend endpoint here&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second decision is export. After your backend creates telemetry, where do those OTLP events go?&lt;/p&gt;

&lt;p&gt;There are two common options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Send OTLP events directly from the backend to Sentry’s OTLP endpoint.&lt;/li&gt;
&lt;li&gt; Send OTLP events to an OpenTelemetry Collector, then have the Collector forward them to Sentry’s OTLP endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That trace-continuation step is what lets a Sentry-instrumented browser action become the parent of backend OpenTelemetry work, regardless of which OTLP export option you choose.&lt;/p&gt;

&lt;p&gt;If you want the reference docs for these pieces, start with &lt;a href="https://docs.sentry.io/concepts/otlp/sentry-with-otel/" rel="noopener noreferrer"&gt;linking Sentry SDKs with OpenTelemetry SDKs&lt;/a&gt;, &lt;a href="https://docs.sentry.io/concepts/otlp/direct/traces/" rel="noopener noreferrer"&gt;sending OpenTelemetry traces directly to Sentry&lt;/a&gt;, &lt;a href="https://docs.sentry.io/concepts/otlp/direct/logs/" rel="noopener noreferrer"&gt;sending OpenTelemetry logs directly to Sentry&lt;/a&gt;, and &lt;a href="https://docs.sentry.io/concepts/otlp/forwarding/" rel="noopener noreferrer"&gt;forwarding OpenTelemetry data to Sentry&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct OTLP vs Collector forwarding
&lt;/h2&gt;

&lt;p&gt;Direct OTLP and Collector forwarding both end at Sentry’s OTLP endpoint. The difference is whether your service talks to Sentry itself or talks to a Collector first.&lt;/p&gt;

&lt;p&gt;Approach&lt;/p&gt;

&lt;p&gt;Use it when&lt;/p&gt;

&lt;p&gt;What you get&lt;/p&gt;

&lt;p&gt;Tradeoff&lt;/p&gt;

&lt;p&gt;Direct OTLP to Sentry&lt;/p&gt;

&lt;p&gt;You have one backend service or project and want the smallest setup&lt;/p&gt;

&lt;p&gt;Fewer moving parts and a short path from service to Sentry&lt;/p&gt;

&lt;p&gt;Less central control over processing, sampling, and routing&lt;/p&gt;

&lt;p&gt;Collector forwarding&lt;/p&gt;

&lt;p&gt;You have multiple services, already run a Collector, need processing, or want multi-vendor routing&lt;/p&gt;

&lt;p&gt;Centralized routing, batching, processing, sampling, and easier vendor evaluation&lt;/p&gt;

&lt;p&gt;Another component to deploy and operate&lt;/p&gt;

&lt;p&gt;Direct OTLP is the simplest path for a single backend project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://o{ORG_ID}.ingest.sentry.io/api/{PROJECT_ID}/integration/otlp/v1/traces"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://o{ORG_ID}.ingest.sentry.io/api/{PROJECT_ID}/integration/otlp/v1/logs"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_TRACES_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"x-sentry-auth=sentry sentry_key={PUBLIC_KEY}"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_LOGS_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"x-sentry-auth=sentry sentry_key={PUBLIC_KEY}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collector forwarding is the better fit when your observability setup is already more than one app talking to one destination. It gives you one place to receive telemetry from many services, batch it, process it, and send it to one or more backends.&lt;/p&gt;

&lt;p&gt;That last part matters when you are evaluating Sentry. You can keep routing telemetry to an existing vendor while also forwarding a copy to Sentry, then compare the debugging experience without rewriting backend instrumentation.&lt;/p&gt;

&lt;p&gt;There is one important Sentry-specific detail for larger setups: a generic OTLP HTTP exporter points at one Sentry project endpoint with one project key. If you send every service through that one exporter, every service lands in the same Sentry project. For multi-project routing, use the &lt;a href="https://docs.sentry.io/concepts/otlp/forwarding/pipelines/sentry-exporter/" rel="noopener noreferrer"&gt;Sentry exporter&lt;/a&gt;. It can route OTLP events to projects based on a resource attribute like &lt;code&gt;service.name&lt;/code&gt;, and it can auto-create missing projects when configured with the right Sentry API permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A demo architecture
&lt;/h2&gt;

&lt;p&gt;Let’s check out a &lt;a href="https://github.com/nikolovlazar/sentry-otel/tree/bf5e026c4923483e3d148d9fd30ead4d1540c774" rel="noopener noreferrer"&gt;demo project&lt;/a&gt; that uses the Collector forwarding path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React + @sentry/react
  -&amp;gt; fetch with traceparent
  -&amp;gt; FastAPI + OpenTelemetry
  -&amp;gt; OpenTelemetry Collector
  -&amp;gt; Sentry OTLP endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend lives in &lt;code&gt;frontend/&lt;/code&gt;. It is a React + Vite app using &lt;code&gt;@sentry/react&lt;/code&gt;. The backend lives in &lt;code&gt;backend/&lt;/code&gt;. It is a FastAPI service using the OpenTelemetry SDK, FastAPI instrumentation, SQLAlchemy instrumentation, manual spans, and standard logging in checkout logic. The Collector lives in &lt;code&gt;collector/&lt;/code&gt; and forwards those OTLP events to Sentry.&lt;/p&gt;

&lt;p&gt;The point of the demo is not that every layer uses the same SDK. The point is that every layer participates in the same trace, with backend logs attached to that debugging context.&lt;/p&gt;

&lt;h3&gt;
  
  
  The frontend uses the Sentry SDK
&lt;/h3&gt;

&lt;p&gt;The Sentry setup is in &lt;a href="https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/frontend/src/instrument.ts" rel="noopener noreferrer"&gt;&lt;code&gt;frontend/src/instrument.ts&lt;/code&gt;&lt;/a&gt;. It enables browser tracing, Session Replay, logs, and trace propagation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_SENTRY_DSN&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sendDefaultPii&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browserTracingIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replayIntegration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;maskAllText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;blockAllMedia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;enableLogs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tracesSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;propagateTraceparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tracePropagationTargets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sr"&gt;/^http:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;localhost:8000&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sr"&gt;/^http:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;127&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;1:8000&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;replaysSessionSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;replaysOnErrorSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend checkout flow is in &lt;a href="https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/frontend/src/hooks/useCheckoutLab.ts#L98-L107" rel="noopener noreferrer"&gt;&lt;code&gt;frontend/src/hooks/useCheckoutLab.ts&lt;/code&gt;&lt;/a&gt;. It creates Sentry spans for user-facing work, logs useful state changes, and captures unexpected errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Run checkout scenario&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ui.checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;totalCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setLastOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual request is a normal &lt;code&gt;fetch&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/checkout`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scenario&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no manual trace header code in that request. The Sentry browser tracing integration handles the propagation as long as the destination matches &lt;code&gt;tracePropagationTargets&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The backend keeps OpenTelemetry
&lt;/h3&gt;

&lt;p&gt;The backend does not install or initialize the Sentry SDK. Its OpenTelemetry setup is in &lt;a href="https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/backend/app/core/observability.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/app/core/observability.py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It creates an OpenTelemetry &lt;code&gt;TracerProvider&lt;/code&gt;, attaches service resource attributes, exports spans over OTLP HTTP, and registers W3C trace-context propagation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nf"&gt;parse_resource_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otel_resource_attributes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otel_service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployment.environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app_environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otel_exporter_otlp_traces_endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otel_exporter_otlp_traces_endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_global_textmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;CompositePropagator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;TraceContextTextMapPropagator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;W3CBaggagePropagator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also configures OTLP log export. The backend creates a &lt;code&gt;LoggerProvider&lt;/code&gt;, attaches an &lt;code&gt;OTLPLogExporter&lt;/code&gt;, and adds an OpenTelemetry &lt;code&gt;LoggingHandler&lt;/code&gt; to the app logger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoggerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;build_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OTLPLogExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otel_exporter_otlp_logs_endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_log_record_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BatchLogRecordProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;set_logger_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;otel_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoggingHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app_logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout_trace_lab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otel_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default backend endpoints are local:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT&lt;/span&gt;=&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="n"&gt;localhost&lt;/span&gt;:&lt;span class="m"&gt;4318&lt;/span&gt;/&lt;span class="n"&gt;v1&lt;/span&gt;/&lt;span class="n"&gt;traces&lt;/span&gt;
&lt;span class="n"&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT&lt;/span&gt;=&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="n"&gt;localhost&lt;/span&gt;:&lt;span class="m"&gt;4318&lt;/span&gt;/&lt;span class="n"&gt;v1&lt;/span&gt;/&lt;span class="n"&gt;logs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the backend exports OTLP traces and logs to the Collector, not directly to Sentry.&lt;/p&gt;

&lt;p&gt;The FastAPI app also allows the browser trace headers through CORS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;allow_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentry-trace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baggage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traceparent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracestate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easy to miss. If your browser is making cross-origin requests and CORS blocks the propagation headers, your frontend and backend traces can split apart even if both sides are instrumented correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The checkout flow adds manual OTel spans and logs
&lt;/h3&gt;

&lt;p&gt;The backend service code in &lt;a href="https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/backend/app/services/checkout.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/app/services/checkout.py&lt;/code&gt;&lt;/a&gt; models a checkout workflow. It creates manual OpenTelemetry spans for business operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CheckoutRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProductRow&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout.validate_cart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requested_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cart.item_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checkout path includes spans for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;checkout.validate_cart&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;checkout.reserve_inventory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;checkout.calculate_tax&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;checkout.payment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;checkout.write_order&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;checkout.send_confirmation&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also emits backend logs for the same business steps, such as &lt;code&gt;checkout.started&lt;/code&gt;, &lt;code&gt;checkout.cart_validated&lt;/code&gt;, &lt;code&gt;checkout.inventory_reserved&lt;/code&gt;, &lt;code&gt;checkout.payment_approved&lt;/code&gt;, &lt;code&gt;checkout.order_written&lt;/code&gt;, and &lt;code&gt;checkout.completed&lt;/code&gt;. Those logs go through Python’s standard &lt;code&gt;logging&lt;/code&gt; API, then the OpenTelemetry &lt;code&gt;LoggingHandler&lt;/code&gt; exports them through OTLP.&lt;/p&gt;

&lt;p&gt;This is the part OTel-first teams care about most. Those spans and logs stay in the OpenTelemetry pipeline. You do not need to rewrite them with &lt;code&gt;Sentry.startSpan()&lt;/code&gt; or Sentry logging APIs just to view the trace and related logs in Sentry.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Collector forwards OTLP to Sentry
&lt;/h3&gt;

&lt;p&gt;The Collector config is in &lt;a href="https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/collector/otel-collector.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;collector/otel-collector.yaml&lt;/code&gt;&lt;/a&gt;. It receives OTLP over gRPC and HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4317&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4318&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It batches the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;send_batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it forwards to Sentry with the OTLP HTTP exporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;verbosity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;basic&lt;/span&gt;
  &lt;span class="na"&gt;otlphttp/sentry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${env:SENTRY_OTLP_ENDPOINT}&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;x-sentry-auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentry&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sentry_key=${env:SENTRY_OTLP_PUBLIC_KEY}"&lt;/span&gt;
    &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
    &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reminder: this demo uses generic &lt;code&gt;otlphttp&lt;/code&gt; for a single Sentry project. For multi-project routing or automatic project creation, swap it for the &lt;a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/sentryexporter" rel="noopener noreferrer"&gt;&lt;code&gt;sentry&lt;/code&gt; exporter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The configured pipelines send to both the debug exporter and Sentry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;debug&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;otlphttp/sentry&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;debug&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;otlphttp/sentry&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the Collector forwarding pattern: the backend sends OTLP events to one local endpoint, and the Collector decides where that telemetry goes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each layer is responsible for
&lt;/h2&gt;

&lt;p&gt;The cleanest way to understand this architecture is by ownership.&lt;/p&gt;

&lt;p&gt;The frontend Sentry SDK owns browser-specific debugging context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Browser spans and frontend transactions&lt;/li&gt;
&lt;li&gt;  Frontend errors and React error boundaries&lt;/li&gt;
&lt;li&gt;  Session Replay&lt;/li&gt;
&lt;li&gt;  Frontend logs&lt;/li&gt;
&lt;li&gt;  Source maps through the Sentry Vite plugin&lt;/li&gt;
&lt;li&gt;  Trace propagation to the backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend OpenTelemetry SDK owns backend instrumentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  FastAPI request spans&lt;/li&gt;
&lt;li&gt;  SQLAlchemy spans&lt;/li&gt;
&lt;li&gt;  HTTPX spans if the backend calls other services&lt;/li&gt;
&lt;li&gt;  Manual checkout spans&lt;/li&gt;
&lt;li&gt;  Backend logs exported with OpenTelemetry&lt;/li&gt;
&lt;li&gt;  Resource attributes such as &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;deployment.environment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  OTLP export to the Collector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Collector owns routing and processing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Receiving OTLP on &lt;code&gt;4317&lt;/code&gt; and &lt;code&gt;4318&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Batching telemetry&lt;/li&gt;
&lt;li&gt;  Printing debug output locally&lt;/li&gt;
&lt;li&gt;  Forwarding to Sentry’s OTLP endpoint&lt;/li&gt;
&lt;li&gt;  Providing the place to add sampling, transforms, filters, or multi-vendor routing later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why Sentry and OpenTelemetry are not competing choices here. They are doing different jobs in the same observability pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can see in Sentry
&lt;/h2&gt;

&lt;p&gt;In the demo, I ran the checkout scenarios from the same frontend session. In Sentry, they show up as one connected trace that starts in React and continues through the FastAPI backend. You can see the normal checkout, slow payment, inventory miss, payment declined, and backend crash work in the same distributed trace instead of jumping between separate tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Fsentry-otel-trace-view.BW5GyfwG.jpg%26w%3D1920%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Fsentry-otel-trace-view.BW5GyfwG.jpg%26w%3D1920%26q%3D100" alt="Sentry trace view showing the frontend checkout flow and backend OpenTelemetry spans for normal checkout, slow payment, inventory miss, payment declined, and backend crash." width="1920" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The backend logs are associated with that trace too. Logs like &lt;code&gt;checkout.started&lt;/code&gt;, &lt;code&gt;checkout.inventory_reserved&lt;/code&gt;, &lt;code&gt;checkout.payment_slow_path&lt;/code&gt;, and &lt;code&gt;checkout.completed&lt;/code&gt; come from Python’s standard logging API, get exported through OTLP, and land in Sentry attached to the same debugging context as the spans.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Fsentry-otel-logs-view.B4AoIDV_.jpg%26w%3D1920%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Fsentry-otel-logs-view.B4AoIDV_.jpg%26w%3D1920%26q%3D100" alt="Sentry trace-associated logs showing backend OpenTelemetry logs along with frontend Sentry logs for the checkout flow." width="1920" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the useful part of the setup: the frontend SDK gives you browser context, the backend keeps its OpenTelemetry spans and logs, and Sentry gives you one place to inspect the full request path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision tree for your own app
&lt;/h2&gt;

&lt;p&gt;Use direct OTLP to Sentry when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You have one backend service or one project.&lt;/li&gt;
&lt;li&gt;  You want the fewest moving parts.&lt;/li&gt;
&lt;li&gt;  You do not need central processing or multi-destination routing yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Collector forwarding when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You already run an OpenTelemetry Collector.&lt;/li&gt;
&lt;li&gt;  You have multiple backend services.&lt;/li&gt;
&lt;li&gt;  You need services to land in separate Sentry projects.&lt;/li&gt;
&lt;li&gt;  You need sampling, filtering, transforms, or batching outside the app process.&lt;/li&gt;
&lt;li&gt;  You want to send telemetry to Sentry and another vendor while evaluating Sentry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the Sentry backend SDK later when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You need error monitoring. OpenTelemetry does not capture and send errors to Sentry—only the Sentry SDK can capture backend exceptions and link them to the trace, so you can jump from an error to the full request path and inspect the related logs, spans, and context.&lt;/li&gt;
&lt;li&gt;  You want profiling.&lt;/li&gt;
&lt;li&gt;  You want &lt;a href="https://docs.sentry.io/product/metrics/" rel="noopener noreferrer"&gt;Sentry’s Application Metrics&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  You want other Sentry features that are not represented by your current OpenTelemetry data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last step is optional, not a prerequisite. You can start with Sentry on the frontend and OpenTelemetry on the backend, then decide later whether adding the backend Sentry SDK is worth it for your services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the smallest change that preserves your trace
&lt;/h2&gt;

&lt;p&gt;If your backend already uses OpenTelemetry, do not start by rewriting instrumentation. Start by deciding where OTLP events should go.&lt;/p&gt;

&lt;p&gt;For a single backend, direct OTLP to Sentry is usually enough.&lt;/p&gt;

&lt;p&gt;For multiple services, vendor evaluation, or anything that needs routing and processing, put a Collector in the middle.&lt;/p&gt;

&lt;p&gt;Then link your frontend Sentry SDK to your backend OTel SDK with W3C &lt;code&gt;traceparent&lt;/code&gt; propagation. That gives you the useful part first: one trace that starts where the user action starts and continues through the backend code you already instrumented.&lt;/p&gt;

&lt;p&gt;You do not need to pick Sentry or OpenTelemetry. Use both where they fit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on the &lt;a href="https://blog.sentry.io/sentry-opentelemetry-work-together/" rel="noopener noreferrer"&gt;Sentry Blog&lt;/a&gt; by &lt;a href="https://blog.sentry.io/authors/lazar-nikolov/" rel="noopener noreferrer"&gt;Lazar Nikolov&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>sentry</category>
      <category>automation</category>
    </item>
    <item>
      <title>Errors, traces, logs, metrics: when to reach for what</title>
      <dc:creator>Sergiy Dybskiy</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:33:37 +0000</pubDate>
      <link>https://dev.to/sentry/errors-traces-logs-metrics-when-to-reach-for-what-3j5f</link>
      <guid>https://dev.to/sentry/errors-traces-logs-metrics-when-to-reach-for-what-3j5f</guid>
      <description>&lt;p&gt;When should I reach for a log, a trace, or a metric? I hit that question constantly when I instrument code, and I watch coding agents hit it too. It sounds like it should be obvious. Errors, traces, logs, and metrics are the four kinds of telemetry most apps run on, four tools in one box, and they overlap enough that the honest answer is every developer’s favourite: &lt;em&gt;it depends&lt;/em&gt;. You can stuff context into span attributes instead of logging it. You can count log events instead of emitting a metric. You can add a duration to a log and call it a span.&lt;/p&gt;

&lt;p&gt;[I had a spiderman meme here but legal told me it would be infringing so I removed it]&lt;/p&gt;

&lt;p&gt;But the fact that you &lt;em&gt;can&lt;/em&gt; doesn’t mean you &lt;em&gt;should&lt;/em&gt;. Each signal exists because it answers a different question, and feeds a different workflow once it lands. Left without solid guidelines, the default is to reach for whatever’s most familiar or already there, and miss what the other kinds are for.&lt;/p&gt;

&lt;p&gt;This post is the guidance I wanted to have, for myself and my robots. Want just the skill? Skip to the end.&lt;/p&gt;

&lt;p&gt;In Sentry, errors, traces, logs, and metrics all come from one SDK, included on every plan. Errors and &lt;a href="https://sentry.io/product/tracing/" rel="noopener noreferrer"&gt;tracing&lt;/a&gt; have been around for years (&lt;a href="https://blog.sentry.io/the-story-of-sentry/" rel="noopener noreferrer"&gt;2012&lt;/a&gt; and &lt;a href="https://blog.sentry.io/see-slow-faster-with-performance-monitoring/" rel="noopener noreferrer"&gt;2020&lt;/a&gt;), &lt;a href="https://sentry.io/product/logs/" rel="noopener noreferrer"&gt;structured logs landed last year&lt;/a&gt;, and &lt;a href="https://sentry.io/product/metrics/" rel="noopener noreferrer"&gt;Application Metrics&lt;/a&gt; completed the set back in May of this year. If you’ve had your application instrumented with Sentry for a while, errors and traces are probably already flowing, with logs and metrics left as tools for you to complete your telemetry story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors, traces, logs, metrics: one question each
&lt;/h2&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://docs.sentry.io/product/issues/" rel="noopener noreferrer"&gt;Errors&lt;/a&gt;: “What just broke?”
&lt;/h4&gt;

&lt;p&gt;A stack trace and an exception type, grouped into an Issue that gets deduplicated, assigned, and tracked until it’s resolved. If your code threw an exception, it’s an error.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://docs.sentry.io/product/explore/trace-explorer/" rel="noopener noreferrer"&gt;Traces&lt;/a&gt;: “Did the request flow the way it was supposed to?”
&lt;/h4&gt;

&lt;p&gt;A trace is a waterfall of timed spans. It’s how you follow a request across your services and see where the time went: the DB query that dragged, the API call that timed out, the LLM tool call that took 8 seconds instead of 200ms.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://docs.sentry.io/product/explore/metrics/" rel="noopener noreferrer"&gt;Metrics&lt;/a&gt;: “How’s this trending over time?”
&lt;/h4&gt;

&lt;p&gt;Counters, gauges, and distributions, each kept as an individual measurement you can slice by any attribute and drill from an aggregate back into the samples (and the trace) behind it. Not just “12,000 checkouts this week,” but 8,400 from the US, 2,600 from the EU, and 1,000 from everywhere else, and how that line moved across the last deploy. Metrics are a historical signal as much as a right-now one, which makes them an easy candidate for dashboards and alerts (but you can still set up alerts on pretty much all signals from Sentry).&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://docs.sentry.io/product/explore/logs/" rel="noopener noreferrer"&gt;Logs&lt;/a&gt;: “What was happening at this point in the code?”
&lt;/h4&gt;

&lt;p&gt;The state of the system at one specific moment, captured as a structured event: config values, feature flags, the inputs and outputs of a function, the user ID. Logs are the trail through a function’s decision tree: the markers you drop at the points where the code makes a choice, so that later, a human or an agent can follow the reasoning. They fill in the &lt;em&gt;why&lt;/em&gt; once errors and traces have told you what broke and where the time went.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real(ish) world example
&lt;/h2&gt;

&lt;p&gt;Let’s say you run a storefront with a React frontend and a Python API. Support starts forwarding tickets: the product recommendations on the account page look generic for a chunk of logged-in customers: bestsellers, not the personalized picks they’re used to. The vibes are off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did anything crash?
&lt;/h3&gt;

&lt;p&gt;First place I’d look is Issues. No exception in the React app, no failed request, every call to &lt;code&gt;/recommendations/{user_id}&lt;/code&gt; came back 200. As far as error tracking is concerned, the app is perfectly healthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Was anything slow, or did the request go off-path?
&lt;/h3&gt;

&lt;p&gt;Pull a trace for one of the affected requests. The route and the database queries are auto-instrumented; I added a few &lt;a href="https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/#add-spans-to-a-transaction" rel="noopener noreferrer"&gt;named spans&lt;/a&gt; for the recommendation steps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Frecommendations-trace-waterfall.DhViXsUW.png%26w%3D1920%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Frecommendations-trace-waterfall.DhViXsUW.png%26w%3D1920%26q%3D100" alt="An affected request's trace in Sentry: an http.server span for the GET /recommendations route over child spans for the user lookup, the ranking\_v2 flag check, the empty recommendations\_v2 query, the fallback to popular items, and ranking." width="1600" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The request loaded the user, evaluated the &lt;code&gt;ranking_v2&lt;/code&gt; flag, queried &lt;code&gt;recommendations_v2&lt;/code&gt;, fell back to popular items, and ranked them. The path is right and the timing’s fine. That &lt;code&gt;recommendations_v2&lt;/code&gt; query &lt;em&gt;succeeded&lt;/em&gt; (returning zero rows is a perfectly successful query), so the code did what it was built to do and fell back. The trace tells me the request flowed as designed. It can’t tell me the design just quietly failed this user. On the surface, everything is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can we dig a little deeper?
&lt;/h3&gt;

&lt;p&gt;Search the logs for the user from the ticket, and the structured log from inside the handler will give you the state at the moment it decided to fall back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Frecommendations-logs-user-search.BpBrroFS.png%26w%3D1920%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Frecommendations-logs-user-search.BpBrroFS.png%26w%3D1920%26q%3D100" alt="The recommendations lookup log for user.id 124 in Sentry, expanded to show its attributes: the ranking\_v2 flag is on, source\_table is recommendations\_v2, candidate\_count is 0, and outcome is fallback." width="1600" height="928"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This user got bucketed into the &lt;code&gt;ranking_v2&lt;/code&gt; feature flag, which reads personalized picks from a new &lt;code&gt;recommendations_v2&lt;/code&gt; table. The table shipped, but the rows were never backfilled, so the lookup came back empty. To the code, an empty result is a perfectly valid “no personalized recs for this user,” the same thing a brand-new user with no history would get. So it falls back to bestsellers and returns 200.&lt;/p&gt;

&lt;p&gt;Why not just attach this data on the span? You could set &lt;code&gt;outcome&lt;/code&gt; and &lt;code&gt;candidate_count&lt;/code&gt; as span attributes. But traces might be sampled, and the one request a customer is complaining about &lt;em&gt;usually&lt;/em&gt; ends up being the one that’s sampled out (at least with my luck). A span attribute is great for reading a trace you’ve found; it can’t help you find one. Logs aren’t sampled.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many people hit it?
&lt;/h3&gt;

&lt;p&gt;One affected customer is a support ticket. Knowing whether it’s a small subset of users or a significant chunk is the difference between fixing it Monday and paging someone tonight. A &lt;code&gt;recommendations.served&lt;/code&gt; counter, tagged with &lt;code&gt;ranking_version&lt;/code&gt; and &lt;code&gt;outcome&lt;/code&gt;, draws the line:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Frecommendations-metric-rate.D44k6wab.png%26w%3D1920%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Frecommendations-metric-rate.D44k6wab.png%26w%3D1920%26q%3D100" alt="Sentry's Application Metrics explorer showing the recommendations.served counter with two queries (one filtered to outcome:personalized, one for the total) and an equation A / B \* 100 grouped by ranking\_version, producing a personalized rate of 97.9% for v1 and 3.3% for v2." width="1600" height="1030"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The v2 path is serving almost nothing but fallbacks, v1 is normal, and the drop lines up with the flag rollout. Scope and trigger, without opening a single trace.&lt;/p&gt;

&lt;p&gt;No one signal cracked it; each ruled something out. No Issues in the feed meant it wasn’t a crash. The metric said it wasn’t a one-off: the whole &lt;code&gt;v2&lt;/code&gt; cohort was falling back. The trace, where one was sampled, showed the path running exactly as designed, which is why it slipped through. The log, pulled up by the &lt;code&gt;user_id&lt;/code&gt; from the ticket, said &lt;em&gt;why&lt;/em&gt;, and I never needed the trace to get to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for what
&lt;/h2&gt;

&lt;p&gt;I use this as a gut check:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want to know&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Something crashed, show the stack trace&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Errors&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How long did this take? Which step was slow?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the request flow through the steps I expected?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What was the state when the code made this decision?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What did this function receive and return?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How often does X happen? Is the rate normal?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did something change after the deploy?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tricky cases are the overlaps, and of course there is nuance to all of this because the same value can show up in more than one signal.&lt;/p&gt;

&lt;h4&gt;
  
  
  Span attribute or metric?
&lt;/h4&gt;

&lt;p&gt;If it’s context about &lt;em&gt;one request’s flow through the system&lt;/em&gt; and you want it while reading that trace, it’s a span attribute. It rides on the span in the waterfall. If it’s a standalone value you want to chart, alert on, or slice over time across &lt;em&gt;all&lt;/em&gt; requests, it’s a metric. The same number can warrant both: &lt;code&gt;candidate_count&lt;/code&gt; as a span attribute lets me read one request; &lt;code&gt;recommendations.served&lt;/code&gt; as a metric lets me watch the rate. One is for inspecting a single flow, the other for watching the aggregate.&lt;/p&gt;

&lt;h4&gt;
  
  
  Log or span?
&lt;/h4&gt;

&lt;p&gt;The span is the timed node in the flow, and most of them are auto-instrumented, so you rarely write them. The log is the decision-point state &lt;em&gt;inside&lt;/em&gt; that node, and you always write it on purpose. Span answers &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;how long&lt;/em&gt;; log answers &lt;em&gt;what was true and why&lt;/em&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Log or metric?
&lt;/h4&gt;

&lt;p&gt;A log is one request’s story, the needle. A metric is the aggregate, the question of whether the haystack is normal. When you want to find the specific request that went wrong, that’s a log. When you want to know how many requests went wrong, that’s a metric.&lt;/p&gt;

&lt;h4&gt;
  
  
  Error or log?
&lt;/h4&gt;

&lt;p&gt;If it needs a stack trace and should be tracked as an Issue, it’s an error. If it’s an unexpected-but-handled condition worth recording, it’s a log. If it’s truly non-critical, &lt;code&gt;logger.warning(exc_info=True)&lt;/code&gt; captures the traceback in logs without creating noise in your error feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the instrumentation looks like
&lt;/h2&gt;

&lt;p&gt;Everything above came out of one endpoint: the &lt;code&gt;GET /recommendations/{user_id}&lt;/code&gt; route from the walkthrough, the function that loads the user, checks the &lt;code&gt;ranking_v2&lt;/code&gt; flag, queries &lt;code&gt;recommendations_v2&lt;/code&gt;, and falls back to popular items when it comes back empty. Here’s that same handler with the instrumentation in place.&lt;/p&gt;

&lt;p&gt;Most of it you don’t write. The FastAPI integration traces the request, the database integration traces every query, so you get the path and the timing without a single hand-written span.&lt;/p&gt;

&lt;p&gt;What you do place by hand are the deliberate signals: a span attribute or two to enrich the flow, the decision-point log, and the metric.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;

&lt;span class="c1"&gt;# The route is auto-instrumented. FastAPI gives you the request span;
# the DB integration gives you a span for every query below. You write none of it.
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/recommendations/{user_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_recommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;# auto-instrumented db span
&lt;/span&gt;    &lt;span class="n"&gt;use_v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flag_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ranking_v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ranking_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;use_v2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;personalized_recs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ranking_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# auto db span
&lt;/span&gt;    &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;personalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popular_items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;             &lt;span class="c1"&gt;# auto db span on the fallback
&lt;/span&gt;
    &lt;span class="c1"&gt;# SPAN ATTRIBUTE: context about THIS request's flow, read inside the trace.
&lt;/span&gt;    &lt;span class="c1"&gt;# It rides on the auto-instrumented request span; no new span needed.
&lt;/span&gt;    &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ranking_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ranking_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendation.outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# LOG: the trail through the decision tree, the state at the moment the
&lt;/span&gt;    &lt;span class="c1"&gt;# code chose personalized vs. fallback. The only signal that records *why*.
&lt;/span&gt;    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendations lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ranking_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ranking_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flag.ranking_v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;use_v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendations_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ranking_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candidate_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# METRIC: the rate across all requests, sliceable by version and outcome.
&lt;/span&gt;    &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendations.served&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ranking_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ranking_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three deliberate touches, each carrying a piece the others can’t. The span attribute tags the request’s flow with the ranking path so it’s right there when I open the trace. The log records what the function decided and why, at the instant it decided. The metric counts the outcome with enough dimension to slice it later.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;do&lt;/em&gt; want a sub-operation timed in the waterfall (say the ranking step, or a call to an external recommender), you can wrap it in a custom span with &lt;a href="https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/" rel="noopener noreferrer"&gt;&lt;code&gt;sentry_sdk.start_span&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Beyond what you write, the SDK fills in even more on its own. Frontend SDKs tag everything with the browser, OS, and release. Call &lt;code&gt;sentry_sdk.set_user()&lt;/code&gt; once and that user follows the errors, spans, logs, and metrics for the request. And because all four come from the same SDK, they share a &lt;code&gt;trace_id&lt;/code&gt; and correlate on their own: every log carries the trace it belongs to, and you can jump from a metric spike straight into the traces behind it, without gluing four vendors together to get there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Ftrace-connected-waterfall.NzzCgsnV.png%26w%3D1920%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Ftrace-connected-waterfall.NzzCgsnV.png%26w%3D1920%26q%3D100" alt="Sentry trace view for the GET /recommendations route: the http.server route span and the database query spans are auto-instrumented, alongside a few custom spans for the recommendation steps, with Waterfall, Logs, and Application Metrics tabs all hanging off the same trace." width="1600" height="832"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of this is ready for you to use and included in every plan. The deliberate signals (the span attributes, the decision-point logs, the metrics) are the ones you place yourself, and they only help if you do it ahead of time, at the spots where your code makes a decision worth questioning later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right tool for the job
&lt;/h2&gt;

&lt;p&gt;The split above isn’t just conceptual. It’s baked into the APIs, and each one is tuned for its job. The &lt;strong&gt;Metrics API&lt;/strong&gt; is built for emitting counts and measures you’ll aggregate. The &lt;strong&gt;span API&lt;/strong&gt; is built for measuring durations and the shape of a request. The &lt;strong&gt;log API&lt;/strong&gt; integrates with your favourite &lt;a href="https://sentry.io/product/logs/" rel="noopener noreferrer"&gt;structured logging&lt;/a&gt; library, so the lines you already write become queryable events. Reaching for the API that matches the workflow usually means reaching for the one that matches the &lt;em&gt;kind&lt;/em&gt; of value you have: a count, a duration, or a moment.&lt;/p&gt;

&lt;p&gt;Sampling falls out of the same logic. Traces are best as a &lt;a href="https://docs.sentry.io/platforms/python/tracing/configure-sampling/" rel="noopener noreferrer"&gt;&lt;em&gt;sampled representation&lt;/em&gt;&lt;/a&gt; of your traffic: you don’t need every request to understand where time goes, so a percentage is plenty (and cheaper). Logs are the opposite: you keep all of them, because the entire point is to find the one rare request that went sideways, and you can’t find what you sampled away. Metrics aren’t sampled either; like logs, you filter them with &lt;a href="https://docs.sentry.io/platforms/python/metrics/#before_send_metric" rel="noopener noreferrer"&gt;&lt;code&gt;before_send_metric&lt;/code&gt;&lt;/a&gt;. Match the retention to the question: a representative sample for “where does time go,” every single event for “what happened to &lt;em&gt;this&lt;/em&gt; request.”&lt;/p&gt;

&lt;h2&gt;
  
  
  You’re not the only one debugging your codebase anymore
&lt;/h2&gt;

&lt;p&gt;Cody from &lt;a href="https://modem.dev/" rel="noopener noreferrer"&gt;Modem&lt;/a&gt; instrumented his AI agent to find out where it was spending time. He worked with Codex to wrap the async work and the logical chunks (everything that runs before the call to the model, say) in spans. Cache hits and time-to-first-token became metrics he could watch over time. Values that only meant something next to a specific operation stayed as span attributes, and the lightweight “this happened here” markers became logs. The span-attribute-versus-metric call wasn’t always obvious to him; his rule was that if a value only made sense in the context of a span, it lived on the span.&lt;/p&gt;

&lt;p&gt;With the tracing in place, he pointed Codex at the Sentry data through the MCP server, feeding it real runs from his Playwright tests in development, and gave it one goal: optimize the code path. The agent read the spans, found work that could run in parallel, and rewrote the code to stop awaiting results until they were actually needed.&lt;/p&gt;

&lt;p&gt;It could do that because a trace is a structured dependency tree with timing on every node, a format an agent can reason about directly. Hand it the same information as a stream of log lines and it would have to reconstruct the call graph from timestamps and string matching first.&lt;/p&gt;

&lt;h2&gt;
  
  
  But what about wide events?
&lt;/h2&gt;

&lt;p&gt;There’s a popular argument that the four signals are overkill: emit one rich, wide event per request and derive the rest later. It’s half right.&lt;/p&gt;

&lt;p&gt;Emit wide, absolutely. The best version of any signal is a structured event packed with context (the flag that was on, the user, the inputs and the outputs), not a bare number or a one-line string.&lt;/p&gt;

&lt;p&gt;But the shape you emit is the shape you get to work with. One fat event in a columnar store charts fine after the fact, but it can’t group itself into a deduplicated Issue, render itself as a waterfall, or fire a real-time alert on a threshold you haven’t defined yet. Those are workflows, and each needs its data in a particular shape.&lt;/p&gt;

&lt;p&gt;So emit wide, into the signal whose workflow you actually need. That’s why the handler emits both a metric and a log: same decision, same trace, two shapes, because watching a rate and reconstructing one request are different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Logs and metrics are the two you probably haven’t turned on yet — they’re relatively new to Sentry, and people are still just finding them. Both are included on every plan.&lt;/p&gt;

&lt;p&gt;You don’t have to wire them up by hand. Point your coding agent at &lt;a href="https://skills.sentry.dev/" rel="noopener noreferrer"&gt;Sentry’s setup skills&lt;/a&gt; for your stack and it installs the SDK, turns on tracing, logs, and metrics, and drops instrumentation at the decision points. Then aim it at your Sentry data through the &lt;a href="https://mcp.sentry.dev/" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; and give it something real: your slowest trace, your newest issue.&lt;/p&gt;

&lt;p&gt;Prefer to grab just the decision framework? It’s a skill of its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add getsentry/sentry-for-ai &lt;span class="nt"&gt;--skill&lt;/span&gt; sentry-instrumentation-guide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The telemetry you emit to debug is the same telemetry it reads to help.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on the &lt;a href="https://blog.sentry.io/errors-traces-logs-metrics-when-to-reach-for-what/" rel="noopener noreferrer"&gt;Sentry Blog&lt;/a&gt; by &lt;a href="https://blog.sentry.io/authors/sergiy-dybskiy/" rel="noopener noreferrer"&gt;Sergiy Dybskiy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>devops</category>
      <category>logging</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your agent can't fix what it can't see</title>
      <dc:creator>Sergiy Dybskiy</dc:creator>
      <pubDate>Thu, 28 May 2026 14:04:58 +0000</pubDate>
      <link>https://dev.to/sentry/your-agent-cant-fix-what-it-cant-see-4391</link>
      <guid>https://dev.to/sentry/your-agent-cant-fix-what-it-cant-see-4391</guid>
      <description>&lt;p&gt;Agents are getting better and better at fixing bugs. They’re even getting better at testing their work, thanks to headless browsers, sandboxes, simulators, etc.&lt;/p&gt;

&lt;p&gt;But what about the bugs that only show up once you bring in different browsers, languages, extensions, internet speeds, and all the other variables that get mixed in the second you ship to prod? Or all the bugs that only show up when you account for… well, humans being humans and doing weird stuff you didn’t expect them to do?&lt;/p&gt;

&lt;p&gt;The bottleneck for self-healing software isn’t agent intelligence. It’s that agents have no idea what actually broke. They’re debugging from source code alone, which is roughly as effective as diagnosing a server outage by skimming the README. What they’re missing is production context: the stack trace, the request payload, the environment, the breadcrumbs leading up to the failure.&lt;/p&gt;

&lt;p&gt;Your agents need someone/something telling them what’s breaking in the wild &lt;em&gt;and&lt;/em&gt; giving them the context they need to understand why.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://mcp.sentry.dev/" rel="noopener noreferrer"&gt;Sentry MCP&lt;/a&gt; and the &lt;a href="https://cli.sentry.dev/" rel="noopener noreferrer"&gt;Sentry CLI&lt;/a&gt; to make that context available to both humans, and increasingly as important, their agents. You can wire up a system today where a Sentry alert triggers an agent, the agent investigates the issue using the same evidence you would, and a draft PR with a fix lands in your repo before you open a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why draft PRs, not auto-merge
&lt;/h2&gt;

&lt;p&gt;Let’s be honest about what’s realistic. A system that detects, fixes, tests, deploys, and monitors its own patches without human involvement is not something you should build today. That’s how you get a very exciting incident review.&lt;/p&gt;

&lt;p&gt;The useful version is more modest: a production error fires, an agent investigates it with real Sentry context, writes a small fix with a regression test, and opens a draft PR. A human is very much in the loop.&lt;/p&gt;

&lt;p&gt;That’s not fully autonomous, but it’s not trivial either. Most bugs sit in a queue, triaged, prioritized, assigned, waiting, and often lose out to new features. Seer diagnoses the root cause in under two minutes. A complete Autofix run, from root cause analysis to an opened PR, takes about six minutes.&lt;/p&gt;

&lt;p&gt;An agent that opens a reviewable, mergeable fix six minutes after the error fires is a meaningful change to your mean time to resolution, even if a human still clicks merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to give your agent production context
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sentry MCP&lt;/strong&gt; is the right choice for agents that support the Model Context Protocol (Claude Code, Cursor, Codex, Windsurf, VS Code with Copilot). Your agent connects to the hosted server, authenticates via OAuth, and gets structured access to issues, events, traces, and Seer analysis. No local install required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-liner for any MCP-compatible client&lt;/span&gt;
npx add-mcp https://mcp.sentry.dev/mcp

&lt;span class="c"&gt;# Or for Claude Code specifically&lt;/span&gt;
claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http sentry https://mcp.sentry.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your client doesn’t support the one-liner, add the config manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sentry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.sentry.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Sentry CLI&lt;/strong&gt; is the right choice for scripted workflows, CI pipelines, or any automation where you need structured output you can pipe to &lt;code&gt;jq&lt;/code&gt; or feed into another process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://cli.sentry.dev/install &lt;span class="nt"&gt;-fsS&lt;/span&gt; | bash
sentry auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;sentry issue list

Issues &lt;span class="k"&gt;in &lt;/span&gt;acme/checkout:
╭──────────────┬──────────────────────────────────────────────────────┬──────┬─────┬────────┬───────┬──────────────╮
│ SHORT ID     │ ISSUE                                                │ SEEN │ AGE │ EVENTS │ USERS │ TRIAGE       │
├──────────────┼──────────────────────────────────────────────────────┼──────┼─────┼────────┼───────┼──────────────┤
│ CHECKOUT-P1  │ TimeoutError: Payment charge exceeded 30s            │   3h │  3h │  1.8k  │   340 │ High  86%    │
├──────────────┼──────────────────────────────────────────────────────┼──────┼─────┼────────┼───────┼──────────────┤
│ CHECKOUT-N7  │ TypeError: Cannot &lt;span class="nb"&gt;read &lt;/span&gt;property &lt;span class="s1"&gt;'total'&lt;/span&gt;              │   1d │  5d │    215 │    82 │ High  71%    │
├──────────────┼──────────────────────────────────────────────────────┼──────┼─────┼────────┼───────┼──────────────┤
│ API-34       │ RateLimitError: Too many requests to /v1/charges     │   3d │ 21d │     67 │    24 │ Med   42%    │
╰──────────────┴──────────────────────────────────────────────────────┴──────┴─────┴────────┴───────┴──────────────╯
Tip: Use &lt;span class="s1"&gt;'sentry issue view &amp;lt;ID&amp;gt;'&lt;/span&gt; to view details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CHECKOUT-P1&lt;/code&gt; is at the top, a timeout in the checkout service with 1.8k events and an 86% fixability score. Drill in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;sentry issue view CHECKOUT-P1

CHECKOUT-P1: TimeoutError: Payment charge exceeded 30s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
╭────────────┬─────────────────────────────────────────────╮
│ Status     │ ● Unresolved &lt;span class="o"&gt;(&lt;/span&gt;Ongoing&lt;span class="o"&gt;)&lt;/span&gt;                      │
│ Fixability │ High &lt;span class="o"&gt;(&lt;/span&gt;86%&lt;span class="o"&gt;)&lt;/span&gt;                                  │
│ Level      │ error                                       │
│ Platform   │ node                                        │
│ Project    │ checkout-service                            │
│ Events     │ 1832                                        │
│ Users      │ 340                                         │
│ First seen │ 3 hours ago                                 │
│ Last seen  │ 12 minutes ago                              │
│ Culprit    │ chargeCustomer &lt;span class="o"&gt;(&lt;/span&gt;src/payment.ts&lt;span class="o"&gt;)&lt;/span&gt;             │
│ Link       │ https://acme.sentry.io/issues/CHECKOUT-P1/  │
╰────────────┴─────────────────────────────────────────────╯

Tip: Use &lt;span class="s1"&gt;'sentry issue explain CHECKOUT-P1'&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;AI root cause analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like a straightforward timeout. An agent with just this would add retry logic or bump the timeout. But run &lt;code&gt;sentry issue explain&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;sentry issue explain CHECKOUT-P1

ℹ Starting root cause analysis, it can take several minutes...

Root Cause Analysis Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Cause &lt;span class="c"&gt;#0: The checkout service's /charge endpoint times out&lt;/span&gt;
waiting &lt;span class="k"&gt;for &lt;/span&gt;the payment service, which blocks on an inventory
availability check. The inventory service&lt;span class="s1"&gt;'s check_stock query
regressed from ~200ms to ~28s after migration
0047_drop_unused_indexes removed the compound index on
(product_id, warehouse_id).

Repository: acme/inventory-service
Affected: src/queries/check_stock.ts:18
First seen: release-3.1.0 (deployed 3h ago)

Reproduction steps:
1. User submits checkout → POST /charge
2. Payment service calls inventory.check_stock(items)
3. check_stock runs full table scan (missing index) → 28s
4. Payment call exceeds 30s timeout → TimeoutError bubbles up to checkout

To create a plan, run: sentry issue plan CHECKOUT-P1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root cause isn’t in the checkout service at all. It’s a dropped database index in the inventory service, two hops away in the trace. No amount of retry logic in &lt;code&gt;payment.ts&lt;/code&gt; fixes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  From alert to draft PR
&lt;/h2&gt;

&lt;p&gt;When a Sentry alert fires on a new or regressed issue, a webhook triggers a worker that checks out your repo and runs a coding agent with a prompt grounded in the specific issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A production error was captured by Sentry. The issue ID is CHECKOUT-P1.

Use Sentry MCP to retrieve the full issue details: stack trace,
breadcrumbs, tags, release, environment, distributed traces,
suspect commits, and Seer analysis.

Based on the evidence:

1. Identify the root cause. Follow traces across services.
2. Make the smallest safe fix in the right repository.
3. Add or update a regression test that covers this failure.
4. Run the test suite.
5. Open a draft PR with the Sentry issue link, root-cause
   summary, files changed, and test results.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent pulls the issue via MCP. The distributed trace shows the checkout call chaining through the payment service into an inventory check that’s taking 28 seconds. Metrics confirm the inventory service’s p99 spiked from 200ms to 28s three hours ago. Suspect commits point at a migration in &lt;code&gt;acme/inventory-service&lt;/code&gt; that dropped a compound index. Session replay shows users rage-clicking “Pay” while nothing happens, generating duplicate charge attempts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sentry issue plan CHECKOUT-P1&lt;/code&gt; lays out the fix: restore the compound index on &lt;code&gt;(product_id, warehouse_id)&lt;/code&gt;. A draft PR lands in &lt;code&gt;acme/inventory-service&lt;/code&gt; with the migration, a root-cause summary linking back to the Sentry trace, and a regression test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Fsentry-self-healing-loop-diagram.BgBqAHWo.png%26w%3D828%26q%3D100" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.sentry.io%2F_vercel%2Fimage%3Furl%3D_astro%252Fsentry-self-healing-loop-diagram.BgBqAHWo.png%26w%3D828%26q%3D100" alt="Self-healing loop: production error flows to Sentry for context and root cause, triggers a coding agent that opens a draft PR, human reviews and merges the fix" width="828" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it with Cursor Automations
&lt;/h2&gt;

&lt;p&gt;We publish a &lt;a href="https://sentry.io/cookbook/regressed-issue-to-pr-cursor/" rel="noopener noreferrer"&gt;cookbook recipe&lt;/a&gt; for this exact workflow using Cursor’s Automations feature. It walks through connecting your repo to Sentry, adding the MCP server to an automation, and configuring a webhook alert to trigger on regressed issues.&lt;/p&gt;

&lt;p&gt;Because Sentry knows the release history and suspect commits, the agent doesn’t search the entire repo for the problem. It starts where the evidence points. For regressed issues specifically, it can identify which commit reintroduced the bug, read the original fix, and understand what went wrong the second time around.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;The more telemetry your app sends to Sentry (traces, metrics, logs, session replays), the harder the bugs an agent can tackle. Today it’s dropped indexes across service boundaries. Six months ago it was null checks. The merge rate on Autofix PRs has climbed from 41% to 46% in that time, and the diagnosis complexity is growing with it.&lt;/p&gt;

&lt;p&gt;There are real limits. Bugs that need product judgment, issues in code the agent can’t reach, and problems where there isn’t enough telemetry to connect the dots: those still need you. But the surface area of what agents can fix is expanding every month.&lt;/p&gt;

&lt;p&gt;Connect &lt;a href="https://mcp.sentry.dev/" rel="noopener noreferrer"&gt;Sentry MCP&lt;/a&gt; to your editor or install the &lt;a href="https://cli.sentry.dev" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;. Hook up your repos for code mappings and tracing. Run &lt;code&gt;sentry issue explain&lt;/code&gt; on something that’s been sitting in your backlog and see what it finds.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://docs.sentry.io/product/ai-in-sentry/seer/autofix/" rel="noopener noreferrer"&gt;Seer Autofix docs&lt;/a&gt; for more on coding agent handoff to Claude Code and Cursor.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on the &lt;a href="https://blog.sentry.io/agents-need-production-context/" rel="noopener noreferrer"&gt;Sentry Blog&lt;/a&gt; by &lt;a href="https://blog.sentry.io/authors/sergiy-dybskiy/" rel="noopener noreferrer"&gt;Sergiy Dybskiy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>agents</category>
    </item>
    <item>
      <title>App store rankings hate slow apps - mobile vitals can help you fix them</title>
      <dc:creator>pry0rity</dc:creator>
      <pubDate>Tue, 04 Mar 2025 20:27:42 +0000</pubDate>
      <link>https://dev.to/sentry/app-store-rankings-hate-slow-apps-mobile-vitals-can-help-you-fix-them-321p</link>
      <guid>https://dev.to/sentry/app-store-rankings-hate-slow-apps-mobile-vitals-can-help-you-fix-them-321p</guid>
      <description>&lt;p&gt;Mobile devs know the struggle. Small regressions can cause big issues in production, and fixing them isn't as easy as pushing a quick patch. Unlike a web app, shipping fixes for apps means navigating app store approvals, and often hopping on meetings with customers to debug because mobile issues can be so challenging to recreate.&lt;/p&gt;

&lt;p&gt;Catching these issues before the 1-star reviews roll in is crucial. Luckily, Sentry just made it easier than ever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile Vitals&lt;/strong&gt; - our latest addition to the Insights feature - highlights your slowest app starts, screen loads, and renders, so you can quickly tackle frozen or laggy interactions before users rage-quit. Whether you're building with React Native, Flutter, Android, or iOS, let's dive into what you can do with it.&lt;/p&gt;

&lt;p&gt;(P.S. We first wrote about mobile vitals years ago—if you're curious about how this new tool works under the hood, it's a &lt;a href="https://blog.sentry.io/mobile-vitals-four-metrics-every-mobile-developer-should-care-about/" rel="noopener noreferrer"&gt;good read&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What does Mobile Vitals track?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1: App Start Performance
&lt;/h3&gt;

&lt;p&gt;You’re doom-scrolling Instagram recipes and suddenly a great ad for a calorie counting app pops up. You download it out of curiosity, and for some reason, it takes 20 seconds to boot up the first time, and 10 seconds every time after that. What are you gonna do - keep forcing yourself to go through that every time you pop another Oreo, or just go back to the scale? &lt;/p&gt;

&lt;p&gt;Sentry tracks App Start performance to help you prevent your users from burning unnecessary calories uninstalling your apps out of frustration. We consider two key metrics here: cold starts &amp;amp; warm starts. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold starts: You never get a second chance at a first impression. Sentry tracks cold starts independently so you can make sure your latest patch doesn’t crush your first impressions.&lt;/li&gt;
&lt;li&gt;Warm starts: Memory management on iOS &amp;amp; Android is complicated and often suspends some tasks while keeping your application ‘warm’ in the background. Pulling up your application from the recent apps selector should be lightning-fast, too. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not just the top-level metric that matters. For warm starts, Sentry auto-instruments &lt;a href="https://docs.sentry.io/platforms/apple/guides/ios/tracing/instrumentation/automatic-instrumentation/#app-start-tracing" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; and &lt;a href="https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/#app-start-instrumentation" rel="noopener noreferrer"&gt;Android&lt;/a&gt; apps with great granular detail. With iOS for instance, you can see pre-runtime, UIKit, frame renders and a bunch of &lt;a href="https://docs.sentry.io/platforms/apple/guides/ios/tracing/instrumentation/automatic-instrumentation/#app-start-tracing" rel="noopener noreferrer"&gt;other native tasks&lt;/a&gt; independently and work to fix what’s bottlenecking your app load performance. &lt;/p&gt;

&lt;p&gt;Clicking into a screen shows you an overview for all your most recent app loads leading up to that screen, which you can then drill into and see what sorts of tasks may be bottlenecking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftthpdq7oqzxcow7o8p16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftthpdq7oqzxcow7o8p16.png" alt="A Sentry UI screenshot showing App Cold Starts and App Warm Starts highlighted with " width="777" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2: Screen Load Performance
&lt;/h3&gt;

&lt;p&gt;If it takes 5 seconds every time you want to load the comments page, nobody’s going to leave comments. It’s really that simple. &lt;/p&gt;

&lt;p&gt;Sentry helps you cut down on UX bloat by tracking &lt;a href="https://docs.sentry.io/product/insights/mobile/screen-loads/" rel="noopener noreferrer"&gt;TTID and TTFD&lt;/a&gt; for every screen load:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time to Initial Display (TTID):&lt;/strong&gt; This tracks the time it takes for a screen to produce its first frame. Often, that’s just a background or a wall of text, but for a user, this is an important metric as it makes the app ‘feel’ fast. Sentry tracks this automatically by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to Full Display (TTFD):&lt;/strong&gt; More importantly is the time it takes for a screen to be fully loaded and functional. This is similar to the LCP core web vital on browser loads. TTFD can include content that’s lazy- or async-loaded after getting its first content, making it a more useful metric for a lot of your mobile screens. This isn’t enabled by default, but it’s super easy to set up. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just like with App Starts, you can drill into Screen Loads by clicking into a screen and drilling into the full trace of the user session, or even dive into a mobile CPU profile. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.arcade.software/share/tYVh6rDXcj9UQNOESjOY" rel="noopener noreferrer"&gt;Check out a 10-second interactive video here. &lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3: Screen Rendering Performance
&lt;/h3&gt;

&lt;p&gt;Your favorite solitaire clone ships a new patch, and suddenly you find your framerate dropping like a brick. You just got your phone a few years ago… is it time to upgrade? Nah, you’ll just throw out a 1-star review, uninstall, and go right back to Wordle.&lt;/p&gt;

&lt;p&gt;We surface 3 key &lt;a href="https://docs.sentry.io/product/insights/mobile/#app-start" rel="noopener noreferrer"&gt;screen rendering performance metrics&lt;/a&gt; by default: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow frames:&lt;/strong&gt; any frame that takes longer than a framecycle to render (16.7ms, or 1/60th of a second at 60hz) is considered ‘slow’, causing laggy UI responses and jittery animations. Sentry calculates the % of slow frames for each screen, so you can quickly find the conditions that lead to laggy or dropped frames. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frozen frames:&lt;/strong&gt; Even worse is a frame that’s stuck for a longer period of time. . Sentry considered any frame render that takes &amp;gt;700ms ‘frozen’. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame delay:&lt;/strong&gt; This is the total ‘hang’ time accumulated on a screen due to slow &amp;amp; frozen frames. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our own testing, we found these 3 metrics super useful for debugging the clunkiest-feeling parts of our mobile application. By combining these with mobile session replays, we were able to quickly recreate conditions that led to clunkiness for our users and get down to the brass tax. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.arcade.software/O1ly5WbccGBhqKMS2fxq" rel="noopener noreferrer"&gt;Check out a 10-second interactive video here.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this matter?
&lt;/h2&gt;

&lt;p&gt;We’re not narcissistic enough to claim that this is as impactful as Core Web Vitals was for the internet - but it’s a start. For mobile apps, it’s extremely difficult to know what to track, let alone how to track it. So, our mobile SDK team took the step to create an awesome new way to get your mobile application performance on track by monitoring a few vital metrics simply &amp;amp; scalably. &lt;/p&gt;

&lt;p&gt;More than just monitoring, Mobile Vitals gives you the why behind slow screens—not just the when. Every insight is backed by a trace, so you can see if the culprit is slow code on your main thread or a deeper issue like sluggish database queries or too complex layouts. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frluzzs943tt5w7fpy3dv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frluzzs943tt5w7fpy3dv.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few of our own, Lazar &amp;amp; Salma, recorded an awesome workshop about how to use Distributed Tracing to debug frontend issues with backend solutions. If you have slow screen loads on your mobile devices but aren’t able to find a root cause on the device itself, this is worth a watch. &lt;/p&gt;

&lt;p&gt;In short, Mobile Vitals is a tool to: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor mobile top level performance metrics&lt;/li&gt;
&lt;li&gt;Easily identify key areas to improve&lt;/li&gt;
&lt;li&gt;Deep dive from a top level metric, to a specific performance issue on a particular screen, to a concrete &lt;a href="https://docs.sentry.io/platforms/apple/tracing/trace-propagation/" rel="noopener noreferrer"&gt;distributed trace&lt;/a&gt;, allowing you to debug and understand an issue across the full-stack&lt;/li&gt;
&lt;li&gt;Connect your metrics with &lt;a href="https://docs.sentry.io/product/explore/session-replay/mobile/" rel="noopener noreferrer"&gt;session replay&lt;/a&gt; and &lt;a href="https://docs.sentry.io/product/explore/profiling/" rel="noopener noreferrer"&gt;profiling&lt;/a&gt;, giving you full insights on what the user was experiencing and which code was running&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What numbers do I need to shoot for?
&lt;/h2&gt;

&lt;p&gt;Goodhart’s law - &lt;em&gt;when a metric becomes a target, it ceases to be a good metric&lt;/em&gt; - still rings true. That said, Google and Apple both openly state that their app store algorithms prioritize app quality, user engagement, app uninstalls and other key factors that are directly related to app performance. &lt;a href="https://x.com/LongTimeHistory/status/1892600734488617138" rel="noopener noreferrer"&gt;Google&lt;/a&gt; in particular recommends cold starts of &amp;lt; 5s, and warm starts of &amp;lt; 2s, but these are just starting points. &lt;/p&gt;

&lt;p&gt;Rather than just setting a target, mobile vitals monitoring is useful for objectively assessing the current state of your mobile app, deciding whether you want to try and improve it, and continuously evaluating your vitals metrics to make sure you’re not accidentally shipping a major slowdown. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsqifc1um87h7kvdt386.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsqifc1um87h7kvdt386.png" alt=" " width="295" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Credit: &lt;a href="https://xkcd.com/2899/" rel="noopener noreferrer"&gt;xkcd&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I get started monitoring my mobile vitals?
&lt;/h3&gt;

&lt;p&gt;Most of the Mobile Vitals metrics are instrumented automatically, so as long as you’ve set up the Sentry SDK in your mobile app, you should be good to go! TTFD is the only exception - you’ll need to call the API to let Sentry know when a screen is fully rendered. Here’s the setup for our most popular SDK’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.sentry.io/platforms/apple/guides/ios/tracing/instrumentation/automatic-instrumentation/" rel="noopener noreferrer"&gt;iOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/" rel="noopener noreferrer"&gt;Android&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.sentry.io/platforms/react-native/tracing/instrumentation/automatic-instrumentation/" rel="noopener noreferrer"&gt;React Native&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.sentry.io/platforms/flutter/tracing/instrumentation/automatic-instrumentation/" rel="noopener noreferrer"&gt;Flutter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to go the extra mile, you can also track custom &lt;a href="https://docs.sentry.io/platforms/apple/tracing/instrumentation/performance-metrics/" rel="noopener noreferrer"&gt;performance metrics&lt;/a&gt;, add &lt;a href="https://docs.sentry.io/platforms/apple/tracing/instrumentation/custom-instrumentation/" rel="noopener noreferrer"&gt;custom spans&lt;/a&gt;, and attach span attributes that you can use to calculate and monitor &lt;a href="https://blog.sentry.io/find-and-fix-performance-bottlenecks-with-sentrys-trace-explorer/" rel="noopener noreferrer"&gt;span metrics&lt;/a&gt; for your most critical user experiences. &lt;/p&gt;

&lt;p&gt;If you’re a mobile developer who cares about your user experiences, tracking Mobile Vitals is a great way to gauge how your mobile app releases impact device performance beyond just the top level metrics. Mobile Vitals is available to all paying Sentry customers - just turn it on in your SDK and give it a shot.&lt;/p&gt;

&lt;p&gt;Questions? Join our passionate community of mobile devs in the &lt;a href="https://discord.com/invite/sentry" rel="noopener noreferrer"&gt;Sentry Discord&lt;/a&gt;. &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Sentry can’t fix React hydration errors, but it can really help you debug them</title>
      <dc:creator>Salma Alam-Naylor</dc:creator>
      <pubDate>Thu, 26 Sep 2024 09:02:11 +0000</pubDate>
      <link>https://dev.to/sentry/sentry-cant-fix-react-hydration-errors-but-it-can-really-help-you-debug-them-40aa</link>
      <guid>https://dev.to/sentry/sentry-cant-fix-react-hydration-errors-but-it-can-really-help-you-debug-them-40aa</guid>
      <description>&lt;p&gt;&lt;em&gt;Hydration failed because the initial ui does not match what was rendered on the server.&lt;/em&gt; Don’t you just &lt;del&gt;love&lt;/del&gt; hate it when that happens?&lt;/p&gt;

&lt;p&gt;If you’re building server-rendered pages with Next.js or any React-based meta-framework, you know hydration errors suck and are usually difficult to debug. Hydration in React is the process that happens when a React application that was rendered as HTML on the server is made interactive in the browser. Hydration errors happen when the markup rendered by React on the client doesn’t match the initial server-rendered HTML, or when invalid HTML was sent by the server, and React couldn’t fix it. Sometimes this is unavoidable, for example, with dates and localization. You can suppress errors for these unavoidable differences in your React code, but most hydration errors would indicate that you’ve got some bugs in your app.&lt;/p&gt;

&lt;p&gt;This article is not about &lt;a href="https://sentry.io/answers/hydration-error-nextjs/" rel="noopener noreferrer"&gt;how to fix hydration errors&lt;/a&gt; but how to debug them using &lt;a href="https://sentry.io/welcome/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;. When hydration errors happen in development, your JavaScript framework of choice will usually show you a large error message with some details about the code that triggered the error. However, &lt;strong&gt;hydration errors aren’t usually visible or obvious in production&lt;/strong&gt;, and your average real-world users probably won’t be able to provide useful screenshots with error reports (and probably won’t think to look in the browser console for errors). What’s more, the &lt;em&gt;automatic&lt;/em&gt; error issue created by Sentry won’t be very useful, either.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0frv6m0v5zhtt2k3nqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0frv6m0v5zhtt2k3nqd.png" alt="Error issue in Sentry created by a hydration issue. The error message is not useful and there isn’t much context associated with the issue." width="799" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  But wait, Session Replay has a superpower
&lt;/h2&gt;

&lt;p&gt;If you’re using Sentry’s &lt;a href="https://docs.sentry.io/product/explore/session-replay/" rel="noopener noreferrer"&gt;Session Replay&lt;/a&gt; to get reproductions of user sessions when a user triggers a hydration error:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sentry will automatically create a specific &lt;a href="https://docs.sentry.io/platforms/javascript/session-replay/issue-types/#configuring-hydration-errors" rel="noopener noreferrer"&gt;Hydration Error Issue&lt;/a&gt; for you (for free!),&lt;/li&gt;
&lt;li&gt;group all the same errors together,&lt;/li&gt;
&lt;li&gt;capture both the server-rendered and client-rendered markup that existed when the error happened,&lt;/li&gt;
&lt;li&gt;and show you the diffs in both visual and raw HTML modes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Just make sure you’re using Sentry’s JavaScript SDK, version 7.87.0 or above, and you’ve got Session Replay enabled to get the good stuff.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s what a grouped Hydration Error looks like in Sentry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is named “Hydration Error,” so you can find it easily in the issue list view.&lt;/li&gt;
&lt;li&gt;Choose which event to view (recommended, latest, oldest) and navigate between events to compare contextual information.&lt;/li&gt;
&lt;li&gt;View a sliding diff between the server and the client (open the diff viewer to also view a side by side visual diff and HTML diff).&lt;/li&gt;
&lt;li&gt;Click the “Resolving Hydration Errors” link to get more help on solving hydration errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotjigjmrtufzjpl4kync.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotjigjmrtufzjpl4kync.png" alt="Grouped hydration error issue view showing how to navigate between grouped events, highlighting the diff viewer, and pointing to a link to get more info about how to solve hydration errors.&lt;br&gt;
" width="799" height="515"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Debugging hydration errors
&lt;/h2&gt;

&lt;p&gt;Given that the standard hydration error message states that the server-rendered HTML did not match the client-rendered HTML, the diff viewer is really helpful in finding those differences. The “Before” view is the server-rendered version, and the “After” view is what React rendered on the client.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6nqw4tf429fmr2803bm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6nqw4tf429fmr2803bm.png" alt="Hydration error side by side visual diff viewer, showing that the server rendered page is the same as the client rendered page." width="799" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you might notice in this example, however, is that the “Before” and “After” versions look the same visually. When inspecting the HTML diff, we see a blank page rendered on the server and HTML rendered on the client. This isn’t correct. HTML is definitely being rendered on the server; I checked in my browser's network tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fliegd4cy2z09tc7uhgjp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fliegd4cy2z09tc7uhgjp.png" alt="Hydration error HTML diff viewer, showing a blank page on the server and invalid HTM on the client." width="799" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real issue in the code causing the hydration error is not that the server-rendered and client-rendered HTML don’t match, it’s that my HTML is invalid (a &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; is contained within a &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;) and React threw an error. This is why hydration errors suck: they don’t always make sense.&lt;/p&gt;

&lt;p&gt;So why does there appear to be no HTML rendered on the server according to the diff? Given that React threw a nonsensical hydration error for invalid HTML, Sentry can only make a best-effort guess as to what the problem is. Usually, if one event has a diff that doesn’t make sense, another event may be more helpful. The bottom line is that when React throws a hydration error, there’s a problem that needs fixing. And because React doesn’t tell us the details needed to fix the problem in production, Sentry fills in the blanks on the hydration error diff tools as much as possible so you can better debug the problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Does creating hydration error issues and diffs increase my Sentry bill?
&lt;/h2&gt;

&lt;p&gt;No. If you’re already using Session Replay, you get automatic grouped hydration error issues for free. Hydration error issues in Sentry are generated from Replays and their associated data, so have no impact on your error quota, either. Additionally, you’ll also get alerted on hydration errors by default.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus tip: level-up your debugging by unmasking non-sensitive data in replays
&lt;/h2&gt;

&lt;p&gt;If you’re using Session Replay, you might have noticed that it masks all text content with * and blocks all media elements on the client by default, before it is sent to Sentry. In this article, the examples shown in the hydration errors images show text rather than asterisks, due to how the &lt;a href="https://docs.sentry.io/product/explore/session-replay/web/" rel="noopener noreferrer"&gt;Session Replay SDK&lt;/a&gt; was configured.&lt;/p&gt;

&lt;p&gt;If you're working on a content-based website that's free of personally identifiable information (PII), you can disable the default text masking and image blocking by configuring the &lt;code&gt;maskAllText&lt;/code&gt;  and &lt;code&gt;blockAllMedia&lt;/code&gt; configuration options in the Session Replay initialization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replayIntegration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;maskAllText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;blockAllMedia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be really helpful in debugging &lt;strong&gt;fake&lt;/strong&gt; hydration errors (that are actually invalid HTML errors) like the one demonstrated in this article.&lt;br&gt;
Note: Only use this if your site has no sensitive data or if you've already set up other options for masking or blocking actual sensitive data. &lt;a href="https://docs.sentry.io/platforms/javascript/session-replay/privacy/#privacy-configuration" rel="noopener noreferrer"&gt;Read more about Session Replay privacy configuration on the Sentry docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debuggability is key
&lt;/h2&gt;

&lt;p&gt;Hydration errors in production have always been a bit of a mystery, without any practical way to deal with and debug them. With the Sentry Replay SDK, you now get the HTML diffs and as much context as possible with each hydration error, helping you to debug and fix things faster. And whilst this ultimately helps you as a developer, it benefits everyone else as well: stakeholders, clients, and most importantly — your users. Now, go fix those bugs.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>The best way to debug slow web pages</title>
      <dc:creator>Salma Alam-Naylor</dc:creator>
      <pubDate>Mon, 01 Jul 2024 14:31:29 +0000</pubDate>
      <link>https://dev.to/sentry/the-number-1-best-way-to-debug-slow-web-pages-41kj</link>
      <guid>https://dev.to/sentry/the-number-1-best-way-to-debug-slow-web-pages-41kj</guid>
      <description>&lt;p&gt;Tools like Page Speed Insights and Google Lighthouse are great for providing advice for front end performance issues. But what these tools can’t do, is evaluate performance across your entire stack of distributed services and applications. &lt;/p&gt;

&lt;p&gt;This is why you need &lt;a href="https://sentry.io/for/tracing/?utm_source=devto&amp;amp;utm_medium=paid-community&amp;amp;utm_campaign=tracing-fy25q2-debuggingtracing&amp;amp;utm_content=video-ad-salma-learnmore" rel="noopener noreferrer"&gt;tracing from Sentry&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>tracing</category>
      <category>sentry</category>
    </item>
    <item>
      <title>How We Reduced Replay SDK Bundle Size by 35%</title>
      <dc:creator>Matt</dc:creator>
      <pubDate>Thu, 16 Nov 2023 18:02:51 +0000</pubDate>
      <link>https://dev.to/sentry/how-we-reduced-replay-sdk-bundle-size-by-35-2g0f</link>
      <guid>https://dev.to/sentry/how-we-reduced-replay-sdk-bundle-size-by-35-2g0f</guid>
      <description>&lt;p&gt;&lt;a href="https://blog.sentry.io/js-browser-sdk-bundle-size-matters/" rel="noopener noreferrer"&gt;Bundle Size matters&lt;/a&gt; - this is something we SDK engineers at Sentry are acutely aware of. In an ideal world, you'd get all the functionality you want with no additional bundle size - oh, wouldn't that be nice? Sadly, in reality any feature we add to the JavaScript SDK results in additional bundle size for the SDK - there is always a trade off to be made.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://docs.sentry.io/product/session-replay/" rel="noopener noreferrer"&gt;Session Replay&lt;/a&gt;, this is especially challenging. Session Replay allows you to capture what's going on in a users' browsers, which can help developers debug errors or other problems the user is experiencing. While this can be incredibly helpful, there is also a considerable amount of JavaScript code required to actually make this possible - thus leading to an increased bundle size.&lt;/p&gt;

&lt;p&gt;In version 7.73.0 of the JavaScript SDKs, we updated the underlying &lt;a href="https://github.com/getsentry/rrweb" rel="noopener noreferrer"&gt;rrweb&lt;/a&gt; package from v1 to v2. While this brought a host of improvements, it also came with a considerable increase in bundle size. This tipped us over the edge to declare a bundle size emergency, and focus on bringing the additional size Session Replay adds to the SDK down as much as possible.&lt;/p&gt;

&lt;p&gt;We're very happy to say that our efforts have been successful, and we managed to reduce the minified &amp;amp; gzipped bundle size compared to the rrweb 2.0 baseline by 23% (~19 KB), and by up to 35% (~29 KB) with maximum tree shaking configuration enabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffodd4ig20rv6z9arxnkl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffodd4ig20rv6z9arxnkl.png" alt=" " width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps we took to reduce bundle size
&lt;/h2&gt;

&lt;p&gt;In order to achieve these bundle size improvements, we took a couple of steps ranging from removing unused code to build time configuration and improved tree shaking:&lt;/p&gt;

&lt;p&gt;Made it possible to remove iframe &amp;amp; shadow DOM support via a build-time flag&lt;br&gt;
Removed canvas recording support by default (users can opt-in via a config option, &lt;a href="https://github.com/getsentry/sentry-javascript/issues/6519" rel="noopener noreferrer"&gt;support is coming&lt;/a&gt;)&lt;br&gt;
Removed unused code from our rrweb fork&lt;br&gt;
Removed unused code in Session Replay itself&lt;br&gt;
Made it possible to remove the included compression worker in favor of hosting it yourself&lt;br&gt;
Moved to a different compression library with a smaller footprint&lt;/p&gt;
&lt;h2&gt;
  
  
  Primer: rrweb
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/getsentry/rrweb" rel="noopener noreferrer"&gt;rrweb&lt;/a&gt; is the underlying tool we use to make the recordings for Session Replay. While we try to contribute to the main rrweb repository as much as possible, there are some changes that are very specific to our needs at Sentry, which is why we also maintain a &lt;a href="https://github.com/getsentry/rrweb" rel="noopener noreferrer"&gt;forked version&lt;/a&gt; of rrweb with some custom changes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Primer: Tree Shaking
&lt;/h2&gt;

&lt;p&gt;Tree shaking allows a JavaScript bundler to remove unused code from the final bundle. If you're not familiar with how it works and the advantages tree shaking brings, you can &lt;a href="https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/" rel="noopener noreferrer"&gt;learn more about it in our docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Made it possible to remove iframe &amp;amp; shadow DOM support via a build-time flag
&lt;/h2&gt;

&lt;p&gt;While rrweb allows you to capture more or less everything that happens on your page, some of the things it can capture may not be necessary for some users. For these cases, we now allow users to manually remove certain parts of the rrweb codebase they may not need at build time, reducing the bundle size.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/getsentry/sentry-javascript/pull/9274" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#9274&lt;/a&gt; &amp;amp; &lt;a href="https://github.com/getsentry/rrweb/pull/114" rel="noopener noreferrer"&gt;getsentry/rrweb#114&lt;/a&gt; we implemented the ground work to allow for tree shaking iframe and shadow DOM recordings. This means that if, for example, you don't have any iframes on your page, you can safely opt-in to remove this code from your application.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/getsentry/sentry-javascript-bundler-plugins/pull/428" rel="noopener noreferrer"&gt;getsentry/sentry-javascript-bundler-plugins#428&lt;/a&gt; we implemented an easy way to implement these optimizations in your app. If you are using one of our bundler plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@sentry/webpack-plugin" rel="noopener noreferrer"&gt;@sentry/webpack-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@sentry/vite-plugin" rel="noopener noreferrer"&gt;@sentry/vite-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@sentry/rollup-plugin" rel="noopener noreferrer"&gt;@sentry/rollup-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@sentry/esbuild-plugin" rel="noopener noreferrer"&gt;@sentry/esbuild-plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can just update to its latest version, and add this configuration to the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sentryPlugin({
  bundleSizeOptimizations: {
    excludeDebugStatements: true,
    excludeReplayIframe: true,
    excludeReplayShadowDom: true,
  },
})

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will save you about 5 KB gzipped of bundle size!&lt;/p&gt;

&lt;p&gt;How we implemented build-time tree shaking flags&lt;br&gt;
We already had some build-time flags for tree shaking implemented in the JavaScript SDK itself (&lt;code&gt;__SENTRY_DEBUG__&lt;/code&gt; and &lt;code&gt;__SENTRY_TRACING__&lt;/code&gt;). We followed the same structure for rrweb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// General tree shaking flag example
if (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) {
  console.log('log a debug message!')
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, this code will result in &lt;code&gt;log a debug message&lt;/code&gt;! being logged. However, if you replace the &lt;code&gt;__SENTRY_DEBUG__&lt;/code&gt; constant at build time with &lt;code&gt;false&lt;/code&gt;, this will result in the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (typeof false === 'undefined' || false) {
  console.log('log a debug message!')
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which bundlers will optimize to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (false) {
  console.log('log a debug message!')
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in turn, since the code inside of &lt;code&gt;if (false)&lt;/code&gt; will definitely never be called, it will be completely tree shaken away.&lt;/p&gt;

&lt;p&gt;For rrweb, we used the same approach to allow you to remove certain recording managers:&lt;/p&gt;

&lt;p&gt;In order to avoid touching all the parts of the code that may use a manager, we added new dummy managers following the same interface but doing nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface ShadowDomManagerInterface {
  init(): void
  addShadowRoot(shadowRoot: ShadowRoot, doc: Document): void
  observeAttachShadow(iframeElement: HTMLIFrameElement): void
  reset(): void
}

class ShadowDomManagerNoop implements ShadowDomManagerInterface {
  public init() {}
  public addShadowRoot() {}
  public observeAttachShadow() {}
  public reset() {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, in the place where the &lt;code&gt;ShadowDomManager&lt;/code&gt; is usually initialized, we can do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const shadowDomManager =
  typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' &amp;amp;&amp;amp; __RRWEB_EXCLUDE_SHADOW_DOM__
    ? new ShadowDomManagerNoop()
    : new ShadowDomManager()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that by default, the regular &lt;code&gt;ShadowDomManager&lt;/code&gt; is used. However, if you replace &lt;code&gt;__RRWEB_EXCLUDE_SHADOW_DOM__&lt;/code&gt; at build time with &lt;code&gt;true&lt;/code&gt;, the &lt;code&gt;ShadowDomManagerNoop&lt;/code&gt; will be used, and the &lt;code&gt;ShadowDomManager&lt;/code&gt; will thus be tree shaken away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removed canvas recording support by default
&lt;/h2&gt;

&lt;p&gt;Since we currently &lt;a href="https://github.com/getsentry/sentry-javascript/issues/6519" rel="noopener noreferrer"&gt;do not support replaying captured canvas elements&lt;/a&gt;, and because the canvas capturing code makes up a considerable amount of the rrweb codebase, we decided to remove this code by default from our rrweb fork, and instead allow you to opt-in to use this by passing a canvas manager into the rrweb &lt;code&gt;record()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;We implemented this in &lt;a href="https://github.com/getsentry/rrweb/pull/122" rel="noopener noreferrer"&gt;getsentry/rrweb#122&lt;/a&gt;, where we started to export a new &lt;code&gt;getCanvasManager&lt;/code&gt; function, as well as accepting such a function in the &lt;code&gt;record()&lt;/code&gt; method. With this, we can successfully tree-shake the unused canvas manager out, leading to smaller bundle size by default, unless users manually import &amp;amp; pass the &lt;code&gt;getCanvasManager&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Once we fully support capturing &amp;amp; replaying canvas elements in Session Replay &lt;a href="https://github.com/getsentry/sentry-javascript/issues/6519" rel="noopener noreferrer"&gt;(coming soon)&lt;/a&gt;, we will add a configuration option to new &lt;code&gt;Replay()&lt;/code&gt; to opt-in to canvas recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removed unused code from rrweb
&lt;/h2&gt;

&lt;p&gt;Another step we took to reduce bundle size was to remove &amp;amp; streamline some code in our rrweb fork. rrweb can be configured in a lot of different ways and is very flexible. However, due to its flexibility, a lot of the code is not tree shakeable, because it depends on runtime configuration.&lt;/p&gt;

&lt;p&gt;For example, consider code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { large, small } from './my-code'

function doSomething(useLarge) {
  return useLarge ? large : small
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this code snippet, even if we know we only ever call this as &lt;code&gt;doSomething(false)&lt;/code&gt;, it is impossible to tree shake the &lt;code&gt;large&lt;/code&gt; code away, because statically we cannot know at build time that &lt;code&gt;useLarge&lt;/code&gt; will always be &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because of this, we ended up fully removing certain parts of rrweb from our fork:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hooks&lt;/code&gt; related code &lt;a href="https://github.com/getsentry/rrweb/pull/126" rel="noopener noreferrer"&gt;getsentry/rrweb#126&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plugins&lt;/code&gt; related code getsentry/rrweb#123&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remove some functions on &lt;code&gt;record&lt;/code&gt; that we don't need &lt;a href="https://github.com/getsentry/rrweb/pull/113" rel="noopener noreferrer"&gt;getsentry/rrweb#113&lt;/a&gt;&lt;br&gt;
In addition, we also made some general small improvements which we also contributed upstream to the main rrweb repository:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid unnecessary cloning of objects or arrays &lt;a href="https://github.com/getsentry/rrweb/pull/125" rel="noopener noreferrer"&gt;getsentry/rrweb#125&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid cloning events to add timestamp &lt;a href="https://github.com/getsentry/rrweb/pull/124" rel="noopener noreferrer"&gt;getsentry/rrweb#124&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Removed unused code in Session Replay
&lt;/h2&gt;

&lt;p&gt;In addition to rrweb, we also identified &amp;amp; removed some unused code in Session Replay itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean up some logs and internal checks &lt;a href="https://github.com/getsentry/sentry-javascript/pull/9392" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#9392&lt;/a&gt;, &lt;a href="https://github.com/getsentry/sentry-javascript/pull/9391" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#9391&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Remove unused function &lt;a href="https://dev.togetsentry/sentry-javascript#9393"&gt;getsentry/sentry-javascript#9393&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Updated library used for compression
&lt;/h2&gt;

&lt;p&gt;We used to compress replay payloads with &lt;a href="https://github.com/nodeca/pako" rel="noopener noreferrer"&gt;pako&lt;/a&gt;, which, while it worked well enough, turned out to be a rather large (bundle-size wise) library for compression. We switched over to use &lt;a href="https://github.com/101arrowz/fflate" rel="noopener noreferrer"&gt;fflate&lt;/a&gt; in &lt;a href="https://github.com/getsentry/sentry-javascript/pull/9436" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#9436&lt;/a&gt; instead, which reduced bundle size by a few KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Made it possible to host compression worker
&lt;/h2&gt;

&lt;p&gt;We use a web worker to compress Session Replay recording data. This helps to send less data over the network, and reduces the performance overhead for users of the SDK. However, the code for the compression worker makes up about 10 KB gzipped of our bundle size - a considerable amount!&lt;/p&gt;

&lt;p&gt;Additionally, since we have to load the worker from an inlined string due to CORS restrictions, the included worker does not work for certain environments, because it requires a more lax &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" rel="noopener noreferrer"&gt;CSP&lt;/a&gt; setting which some applications cannot comply with.&lt;/p&gt;

&lt;p&gt;In order to both satisfy stricter CSP environments, as well as allowing to optimize the bundle size of the SDK, we added a way to tree shake the included compression worker, and instead provide a URL to a self-hosted web worker.&lt;/p&gt;

&lt;p&gt;Implemented in &lt;a href="https://github.com/getsentry/sentry-javascript/pull/9409" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#9409&lt;/a&gt;, we added an example web worker that users can host on their own server, and then pass in a custom &lt;code&gt;workerUrl&lt;/code&gt; to &lt;code&gt;new Replay({})&lt;/code&gt;. With this setup, users save 10 KB gzipped of their bundle size, and can serve the worker as a separate asset that can be cached independently.&lt;/p&gt;

</description>
      <category>sentry</category>
      <category>frontend</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Performance Monitoring for Every Developer: Web Vitals &amp; Function Regression Issues</title>
      <dc:creator>Matt</dc:creator>
      <pubDate>Wed, 15 Nov 2023 20:30:49 +0000</pubDate>
      <link>https://dev.to/sentry/performance-monitoring-for-every-developer-web-vitals-function-regression-issues-26j2</link>
      <guid>https://dev.to/sentry/performance-monitoring-for-every-developer-web-vitals-function-regression-issues-26j2</guid>
      <description>&lt;p&gt;Extracting relevant insights from your performance monitoring tool can be frustrating. You often get back more data than you need, making it difficult to connect that data back to the code you wrote. Sentry’s Performance monitoring product lets you cut through the noise by detecting real problems, then quickly takes you to the exact line of code responsible. The outcome: Less noise, more actionable results.&lt;/p&gt;

&lt;p&gt;Today, we’re announcing two new features to help web, mobile, and backend developers discover and solve performance problems in their apps: Web Vitals and Function Regression Issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web Vitals: From performance scores to slow code
&lt;/h2&gt;

&lt;p&gt;Web Vitals unify the measurement of page quality into a handful of useful metrics like loading performance, interactivity, and visual stability. We used these metrics to develop the Sentry Performance Score, which is a normalized score out of 100 calculated using the weighted averages of Web Vital metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpo5xgt7k3p3qark57nx5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpo5xgt7k3p3qark57nx5.png" alt=" " width="800" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Sentry Performance Score is similar to Google’s Lighthouse performance score, with one key distinction: Sentry collects data from real user experiences, while Lighthouse collects data from a controlled lab environment. We modeled the score to be as close to Lighthouse as possible while excluding components that were only relevant in a lab environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web Vitals identify the biggest opportunities for improvement
&lt;/h2&gt;

&lt;p&gt;To improve your overall performance score, you should start by identifying individual key pages that need performance improvements. To simplify this and help you cut to the chase, we rank pages by Opportunity, which indicates the impact of a single page on the overall performance score.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frz376g8gqzf2ffm9u1e5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frz376g8gqzf2ffm9u1e5.png" alt=" " width="800" height="122"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, the highest opportunity page in Sentry’s own web app is our Issue Details page. Since this is the most commonly accessed page in our product, improving its performance would significantly improve the overall experience of using Sentry.&lt;/p&gt;

&lt;p&gt;After identifying a problematic page, the next step is to find example events where users had a subpar experience. Below, you’ll see events that represent real users loading our Issue Details page, with many experiencing poor or mediocre performance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxa18boiieyfvqzcvp19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmxa18boiieyfvqzcvp19.png" alt=" " width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the above screenshot, you’ll see a user that experienced a performance score of 9 out of 100 (Poor), primarily driven by a 10+ second Largest Contentful Paint (LCP). Ouch. These worst-case events highlight performance problems not evident during local development or under ideal conditions (e.g. when users have a fast network connection, a high-spec device, etc.).&lt;/p&gt;

&lt;p&gt;You’ll notice some of these events have an associated ▶️ Replay. When available, these let you see a video-like reproduction of a user’s real experience with that page. When optimizing your app’s performance, these replays can help you understand where users have a subpar experience–for example, when they struggle with 10-second load times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Span waterfalls highlight your most expensive operations
&lt;/h2&gt;

&lt;p&gt;To find out what caused the slow LCP, you can click the event’s Transaction button, which provides a detailed breakdown of the operations that occurred during page load. We call these operations spans.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4va4e449o2pp39512n3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4va4e449o2pp39512n3j.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most relevant spans are the ones that occur before the red LCP marker, as these spans are potentially LCP-blocking. Spans that occur after the LCP marker still impact overall page performance, but do not impact the initial page load.&lt;/p&gt;

&lt;p&gt;The first span that looks like a clear performance bottleneck is the app.page.bundle-load span, which measures how long it takes to load the JavaScript bundle. In this case, loading the bundle alone takes almost 6 seconds or about 60% of our total LCP duration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtqn6uy4rjp89kv507q8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtqn6uy4rjp89kv507q8.png" alt=" " width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The JavaScript bundle load time depends primarily on its size; reducing the bundle size would significantly improve page load speed. But, even if we reduced bundle load time by 50%, LCP would only drop from 12 to 7 seconds — which means we need to look for additional optimization opportunities.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevwwrhfj5r72wekg04gd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevwwrhfj5r72wekg04gd.png" alt=" " width="799" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next clear opportunity is this ui.long-task.app-init span taking almost 1 second. Long task spans represent operations over 50 milliseconds where the browser is executing JavaScript code and blocking the UI thread. Since this is a pure JavaScript operation, let’s go deeper and find out what’s going on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Profiling takes you to the line of code responsible
&lt;/h2&gt;

&lt;p&gt;Identifying the code causing a long task has traditionally been difficult, as you must reproduce the issue in a development environment with access to a profiler. To solve this, Sentry has launched new support for collecting browser JavaScript profiles in production (on Chromium-based browsers). This lets you debug real user issues and collect a wide range of sample profiles across your user base.&lt;/p&gt;

&lt;p&gt;In this example, we can open the profile associated with the page load event and see the code that executed during the ~1-second long task span:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwmtuznruau4qy8tn088.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwmtuznruau4qy8tn088.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The EChartsReactCore.prototype.componentDidMount function takes 558 ms to execute, which is over half the duration of the long task span. This function is linked to a React component that renders a chart, provided by the open-source ECharts visualization library. This looks exactly like where we should focus our attention if we hope to bring our Issues Details pageload times down.&lt;/p&gt;

&lt;p&gt;In summary, we first identified a page with a poor performance score, then determined that reducing the JavaScript bundle size and optimizing a specific React component could significantly improve the performance of our Issue Details page. By finding pages with high opportunity scores, breaking down page load events, and diving deep into JavaScript performance with profiles, you can enhance your product’s overall user experience.&lt;/p&gt;

&lt;p&gt;Web Vitals are available to Sentry customers today. Profiling for Browser JavaScript is also now available in beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function Regression Issues: more than just alerts
&lt;/h2&gt;

&lt;p&gt;Recently, we launched the ability to view the slowest and most regressed functions across your application. Now, we can help you debug function-level regressions with a new type of Performance Issue. Function Regression Issues notify you when a function in your application regresses, but they do more than just detect the regression— they use profiling data to give you essential context about what changed so you can solve the problem.&lt;/p&gt;

&lt;p&gt;Function Regression Issues can be detected on any platform that supports Sentry Profiling. Below, we’ll walk through a backend example in a Python project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn9jiezikcg2pv0czu7a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn9jiezikcg2pv0czu7a.png" alt=" " width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot above is a real Function Regression Issue that identified a slowdown in Sentry’s live-running server code. It triggered because the duration of a function that checks customer rate limits stored in Redis regressed by nearly 50%.&lt;/p&gt;

&lt;p&gt;The top chart shows how function duration changed over time, while the bottom chart shows the number of invocations (throughput). You’ll notice that throughput also appears to increase during the slowdown period. This suggests that increased load might be one of the causes of this regression.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftirrbfcy6tubpynus58e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftirrbfcy6tubpynus58e.png" alt=" " width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the screenshot above, you’ll see that the same issue gives us other essential information like which API endpoints were impacted by the regression and how much they regressed. This data reveals that the rate-limiting function was widely used and called by many of our endpoints, resulting in a significant regression in overall backend performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jump from Regression Issues to Profiles to find the root cause
&lt;/h2&gt;

&lt;p&gt;The Function Regression Issue makes it easy to see the example profiles captured before and after the regression — they’re displayed right under the “Most Affected” endpoints. Comparing these profiles gives the most crucial context, revealing (at a code level) the change in runtime behavior that caused the regression.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidu1tcdurb46xw3rp33e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidu1tcdurb46xw3rp33e.png" alt=" " width="799" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, we looked at an example profile event that was captured before the regression occurred. We noticed that our regressed function calls into two functions within the third-party redis module: ConnectionPool.get_connection and ConnectionPool.release.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkkhn43gr21v914q5rjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkkhn43gr21v914q5rjq.png" alt=" " width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We compared this to a profile collected after the regression and noticed that one of those two functions, ConnectionPool.get_connection, was taking significantly longer than before. Each function frame in a profile offers source context for where the function was defined and the executed line number. In this case, opening this source location in the redis module yielded the following line:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjg09jyjbv3w22koshge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjg09jyjbv3w22koshge.png" alt=" " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This line attempts to acquire a lock — and the significant wall-time duration increase when executing this line suggests that multiple processes or threads are trying to acquire the lock simultaneously. This lock contention is the code-level reason for the regression.&lt;/p&gt;

&lt;p&gt;This lock contention issue also aligns with what we saw earlier in the throughput graph – increased throughput makes contention more likely. Through additional investigation, we discovered that the increased throughput for this function corresponded to an increase in the number of Redis connections starting around the time of the regression; our next step is to isolate the source of the additional connections.&lt;/p&gt;

&lt;p&gt;Using this example, we’ve illustrated how Function Regression Issues can help you link performance regressions directly to the code causing the regression with profiling data. While this specific example focused on a backend use case, this capability works on any platform that supports Sentry Profiling.&lt;/p&gt;

&lt;p&gt;Function Regression issues are available today to Early Adopters and will become generally available over the next 2 weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A high-performance bar is critical for building differentiated products that people want to use. With Web Vitals and Function Regression Issues, we’ve provided more ways for all developers to solve performance problems by connecting them to code.&lt;/p&gt;

&lt;p&gt;Tune in for more &lt;a href="https://sentry.io/events/launch-week/" rel="noopener noreferrer"&gt;exciting product announcements&lt;/a&gt; with Sentry Launch Week. To set up Sentry Performance today, check out this &lt;a href="https://docs.sentry.io/product/performance/getting-started/" rel="noopener noreferrer"&gt;guide&lt;/a&gt;. You can also drop us a line on &lt;a href="https://twitter.com/getsentry" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; or &lt;a href="https://discord.com/invite/sentry" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;, or share your Web Vitals feedback with us on &lt;a href="https://github.com/getsentry/sentry/discussions/59620" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And, if you’re new to Sentry, you can &lt;a href="https://sentry.io/signup/" rel="noopener noreferrer"&gt;try it for free&lt;/a&gt; or &lt;a href="https://sentry.io/demo/" rel="noopener noreferrer"&gt;request a demo&lt;/a&gt; to get started.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>What’s the difference between API Latency and API Response Time?</title>
      <dc:creator>Lazar Nikolov</dc:creator>
      <pubDate>Mon, 13 Nov 2023 15:28:32 +0000</pubDate>
      <link>https://dev.to/sentry/whats-the-difference-between-api-latency-and-api-response-time-1198</link>
      <guid>https://dev.to/sentry/whats-the-difference-between-api-latency-and-api-response-time-1198</guid>
      <description>&lt;p&gt;Your app’s networking directly affects the user experience of your app. Imagine having to wait a few seconds for the page to load. Or even worse, imagine waiting for a few seconds every time you perform an action. It would be infuriating! Before you go on a fixing adventure, it’s a good idea to understand what causes that waiting time. So let’s do that!&lt;/p&gt;

&lt;h2&gt;
  
  
  The difference between Latency vs Response Time
&lt;/h2&gt;

&lt;p&gt;The total client-server communication time is called &lt;strong&gt;API Response Time&lt;/strong&gt;. That’s from the moment the client makes a request, until it receives a response back. A slow request means a very long response time. Anything that contributes to the slowness affects the response time.&lt;br&gt;
The response time consists of the &lt;strong&gt;latency&lt;/strong&gt; and the &lt;strong&gt;processing time&lt;/strong&gt;. The latency is how long it takes to transfer data between the client and the server, or the “processing component”. A few factors contribute to the latency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the network speed&lt;/li&gt;
&lt;li&gt;the server load&lt;/li&gt;
&lt;li&gt;the performance of the load balancer&lt;/li&gt;
&lt;li&gt;the size of the data we’re sending&lt;/li&gt;
&lt;li&gt;and the API design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/em6l9zw4tzag/3HSe1PeCYHhJ3XzXY4Waka/c1512a223d3988396c4718a84dccdc03/response-time-latency.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/em6l9zw4tzag/3HSe1PeCYHhJ3XzXY4Waka/c1512a223d3988396c4718a84dccdc03/response-time-latency.png" alt="API Response Time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, the client sends a data request to the API and gets a response back in 3 seconds. The total response time is 3 seconds. The latency might be 300ms, which leaves 2500ms, or 2.5 seconds, for processing. Just to have a number in mind, high-performing APIs are considered to have between 0.1 and 1 second average response time. At 2 seconds the delay is noticeable. At 5 seconds the delay is so significant that the users are starting to abandon the application/website.&lt;/p&gt;

&lt;p&gt;The response time is one of the most important metrics we need to keep our eyes on. If not, it can significantly hurt the user experience, and even the business itself. Back in 2008, &lt;a href="https://news.ycombinator.com/item?id=273900" rel="noopener noreferrer"&gt;Amazon reported&lt;/a&gt; that every 100ms latency costs 1% of their profit. &lt;a href="http://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html" rel="noopener noreferrer"&gt;Google also reported&lt;/a&gt; that in an experiment that increased the number of search results they noticed that half a second delay caused a 20% drop in traffic. These are significant numbers. Amazon makes $19.4 billion per month. 1% of those sales is $194 million. Add up that number for every 100ms latency and you’ll see the damage. As you can see, learning how to monitor and optimize your API response time is a very good investment.&lt;/p&gt;

&lt;p&gt;Just like any other web performance metric, the response time should be measured and monitored in production. Your computer and internet speed might be fast, but your users’ computers and internet are probably not as fast. That results in much less favorable results than what you’ll see if you measured your API’s response time locally. But working with production data also means that you’re working with outliers. The Average Response Time is not always a good metric. Let’s learn how to properly measure the API response time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measuring response time
&lt;/h2&gt;

&lt;p&gt;First let’s talk about why ART (Average Response Time) is not a good metric. There are always the edge cases where a small number of your users are reporting the worst response times imaginable. That could be because of a really outdated computer, or they’re trying to access your web app with a bad internet connection (subway, remote locations, etc…), or your API experienced a brief downtime because of a bug or your deployment. You don’t care about those cases because there is usually nothing you can do about them. Calculating an average on that data will take those outliers into account as well, and you don’t want that. You want to exclude those data points from your data. Enter: &lt;strong&gt;percentiles&lt;/strong&gt;.&lt;br&gt;
Percentiles provide you with a different view of your performance data. The data is sorted in a descending order and cut at specific % points. The most commonly used percentiles are p50, p75, p95, p99 and p100.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P50, also called the Median&lt;/strong&gt;, is the value below which 50% of the data falls. This would be the typical performance of your API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P75&lt;/strong&gt; is the value where 75% of the data falls. This percentile is good for frontend applications because it includes more variable data, which mirrors the variety of the user conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P95&lt;/strong&gt; is more valuable for backend applications where the data is more uniform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P99, also called Peak Response Time&lt;/strong&gt;, is also more valuable for backend applications, and it marks the upper limit of the performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P100&lt;/strong&gt; is the maximum observed value. This is the worst measured performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to get into more detail about percentiles, read our &lt;a href="https://blog.sentry.io/choosing-the-right-metric-a-guide-to-percentiles-perf-monitoring/#percentiles-understanding-variability" rel="noopener noreferrer"&gt;“Choosing the Right Metric” article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Another important metric is the Error/Failure Rate of your API. The Error/Failure Rate is a value that indicates the % of requests that resulted with a non-200 status code. Having this data can also help you figure out if things are wrong with your API or documentation. If you’re seeing more “400s” status codes, it might mean that clients don’t use your API properly, or don’t understand it well. If you’re seeing more “500s” status codes then it means you have issues with your code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Monitoring API response time in production
&lt;/h2&gt;

&lt;p&gt;To monitor your API’s response time (and other metrics as well) in production, you need to implement a mechanism that will continuously take those measurements, save them in a database, and provide you with tools to query and visualize the data. Sentry is an &lt;a href="https://sentry.io/for/performance/" rel="noopener noreferrer"&gt;application performance monitoring tool&lt;/a&gt; that you can use to monitor all of your &lt;a href="https://sentry.io/for/frontend/" rel="noopener noreferrer"&gt;frontend&lt;/a&gt;, &lt;a href="https://sentry.io/for/backend/" rel="noopener noreferrer"&gt;backend&lt;/a&gt;, and &lt;a href="https://sentry.io/for/mobile/" rel="noopener noreferrer"&gt;mobile application&lt;/a&gt; projects—not just your API. And don’t worry, Sentry probably has an &lt;a href="https://sentry.io/platforms/" rel="noopener noreferrer"&gt;SDK&lt;/a&gt; for whatever framework you’re using.&lt;/p&gt;

&lt;p&gt;Getting started is easy. After installing and configuring Sentry’s SDK for your framework of choice, you’ll get automatic instrumentation, which means a bigger part of your backend is already set up with performance monitoring. Depending on the size of your application, this could be enough to get started. If you need more detailed performance data, you can add &lt;a href="https://docs.sentry.io/platforms/node/performance/instrumentation/custom-instrumentation/" rel="noopener noreferrer"&gt;custom instrumentations&lt;/a&gt; in certain places.&lt;/p&gt;

&lt;p&gt;When you start getting performance data in your Sentry account, identifying what makes your API slow is quick. Getting to the root cause of the slow performance is even quicker. Sentry’s tracing data makes the performance bottlenecks obvious. Here’s a short arcade that shows you how you can identify performance bottlenecks in your application and figure out what causes them:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://app.arcade.software/share/DsIzAmoLxA4bW8nqsiOZ" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapp.arcade.software%2Fog%2FDsIzAmoLxA4bW8nqsiOZ" height="630" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://app.arcade.software/share/DsIzAmoLxA4bW8nqsiOZ" rel="noopener noreferrer" class="c-link"&gt;
            Backend Performance Monitoring | Arcade
          &lt;/a&gt;
        &lt;/h2&gt;
          
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.arcade.software%2Fimages%2Fbutton-logo-128.png" width="128" height="128"&gt;
          app.arcade.software
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A good resource on improving the performance of your API using Sentry is Lincoln Loop’s “&lt;a href="https://lincolnloop.com/insights/optimizing-response-time-19x-faster/" rel="noopener noreferrer"&gt;19x faster response time&lt;/a&gt;” article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So, a quick recap. API latency is the time it takes for the data to be transmitted between the client and the backend. The API response time is the latency + the time it takes for the backend to process the request and return the result. Factors like network speed, load balancer, data size, server load, API design, and our code affect the response time.&lt;br&gt;
To properly monitor the performance of your backend, you need to use a tool that will monitor your application while in production. Sentry can help you with that, head to &lt;a href="https://sentry.io/signup" rel="noopener noreferrer"&gt;https://sentry.io/signup&lt;/a&gt; to get started.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>performance</category>
    </item>
    <item>
      <title>Getting Started with Jetpack Compose</title>
      <dc:creator>Lazar Nikolov</dc:creator>
      <pubDate>Fri, 03 Mar 2023 21:04:28 +0000</pubDate>
      <link>https://dev.to/sentry/getting-started-with-jetpack-compose-cpb</link>
      <guid>https://dev.to/sentry/getting-started-with-jetpack-compose-cpb</guid>
      <description>&lt;p&gt;Recently, we wrote about the &lt;a href="https://blog.sentry.io/2022/12/07/mobile-the-future-is-declarative/" rel="noopener noreferrer"&gt;demonstrative move to declarative UI&lt;/a&gt;. With &lt;a href="http://d.android.com/compose" rel="noopener noreferrer"&gt;Jetpack Compose&lt;/a&gt;, Android is joining the declarative trends. &lt;/p&gt;

&lt;p&gt;Jetpack Compose, a new declarative UI toolkit by Google made for building native Android apps, is rapidly gaining traction. In fact, as announced at the &lt;a href="https://www.youtube.com/watch?t=1069&amp;amp;v=Awi4J5-tbW4&amp;amp;feature=youtu.be" rel="noopener noreferrer"&gt;Android Dev Summit last year&lt;/a&gt; last year, 160 of the top 1,000 Android apps already use Jetpack Compose. In contrast to the traditional XML Views, Jetpack Compose allows you to build UIs using composable functions that describe how the UI should look and behave. &lt;/p&gt;

&lt;p&gt;The main advantage of using Jetpack Compose is that it allows you to write UI code that is more concise and easier to understand. This leads to improved maintainability and reduced development time. &lt;/p&gt;

&lt;p&gt;The main disadvantage of using Jetpack Compose is that it’s relatively new, so its ecosystem is limited and the number of available libraries, tools, and resources is lower than the traditional ecosystem. &lt;/p&gt;

&lt;p&gt;Despite that, we believe that learning Jetpack Compose is worth the learning curve and challenges. Here are some tips we've found helpful as you are getting started. &lt;/p&gt;

&lt;h2&gt;
  
  
  How to start using Jetpack Compose
&lt;/h2&gt;

&lt;p&gt;The recommended IDE for working with Jetpack Compose is &lt;a href="https://developer.android.com/studio" rel="noopener noreferrer"&gt;Android Studio&lt;/a&gt;. After downloading and installing Android Studio, you’ll get the option to create a new project. To create a new Jetpack Compose application, you need to select either the &lt;code&gt;Empty Compose Activity&lt;/code&gt; (which uses Material v2), or &lt;code&gt;Empty Compose Activity (Material3)&lt;/code&gt; (which uses the Material v3 which is in version 1.0 as of last year). You can see both options in the top right of this screenshot: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzc211jlseavdxdgw7e2s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzc211jlseavdxdgw7e2s.png" alt="Creating a new Jetpack Compose app from Android Studio" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the easiest way to get started with Jetpack Compose. If you’d like to enable Jetpack Compose into an existing Android application, here’s what you need to do:&lt;/p&gt;

&lt;p&gt;1. Add the following build configurations in your app’s &lt;code&gt;build.gradle&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;buildFeatures&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// this flag enables Jetpack Compose&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;composeOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// the compiler version should match&lt;/span&gt;
    &lt;span class="c1"&gt;// your project's Kotlin version&lt;/span&gt;
    &lt;span class="n"&gt;kotlinCompilerExtensionVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.3.2"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Add the Compose BOM (&lt;a href="https://developer.android.com/jetpack/compose/bom/bom" rel="noopener noreferrer"&gt;Bill of Materials&lt;/a&gt;) and the subset of Compose dependencies to your dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;composeBom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'androidx.compose:compose-bom:2023.01.00'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="n"&gt;composeBom&lt;/span&gt;
  &lt;span class="n"&gt;androidTestImplementation&lt;/span&gt; &lt;span class="n"&gt;composeBom&lt;/span&gt;

  &lt;span class="c1"&gt;// Choose one of the following:&lt;/span&gt;
  &lt;span class="c1"&gt;// Material Design 3&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.material3:material3'&lt;/span&gt;
  &lt;span class="c1"&gt;// or Material Design 2&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.material:material'&lt;/span&gt;
  &lt;span class="c1"&gt;// or skip Material Design and build directly on top of foundational components&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.foundation:foundation'&lt;/span&gt;
  &lt;span class="c1"&gt;// or only import the main APIs for the underlying toolkit systems,&lt;/span&gt;
  &lt;span class="c1"&gt;// such as input and measurement/layout&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.ui:ui'&lt;/span&gt;       

  &lt;span class="c1"&gt;// Android Studio Preview support&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.ui:ui-tooling-preview'&lt;/span&gt;
  &lt;span class="n"&gt;debugImplementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.ui:ui-tooling'&lt;/span&gt;

  &lt;span class="c1"&gt;// UI Tests&lt;/span&gt;
  &lt;span class="n"&gt;androidTestImplementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.ui:ui-test-junit4'&lt;/span&gt;
  &lt;span class="n"&gt;debugImplementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.ui:ui-test-manifest'&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional - Included automatically by material, only add when you need&lt;/span&gt;
  &lt;span class="c1"&gt;// the icons but not the material library (e.g. when using Material3 or a&lt;/span&gt;
  &lt;span class="c1"&gt;// custom design system based on Foundation)&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.material:material-icons-core'&lt;/span&gt;
  &lt;span class="c1"&gt;// Optional - Add full set of material icons&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.material:material-icons-extended'&lt;/span&gt;
  &lt;span class="c1"&gt;// Optional - Add window size utils&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.material3:material3-window-size-class'&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional - Integration with activities&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.activity:activity-compose:1.5.1'&lt;/span&gt;
  &lt;span class="c1"&gt;// Optional - Integration with ViewModels&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'&lt;/span&gt;
  &lt;span class="c1"&gt;// Optional - Integration with LiveData&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.runtime:runtime-livedata'&lt;/span&gt;
  &lt;span class="c1"&gt;// Optional - Integration with RxJava&lt;/span&gt;
  &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'androidx.compose.runtime:runtime-rxjava2'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you build UI in Jetpack Compose?
&lt;/h2&gt;

&lt;p&gt;Jetpack Compose uses Composables to define the view hierarchy, and modifier to apply visual appearance and behavior changes to the composables they’re added to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composable functions
&lt;/h3&gt;

&lt;p&gt;Composable functions (or just Composables) are ordinary Kotlin functions that are annotated with &lt;code&gt;@Composable&lt;/code&gt;, can be nested within another composable functions, and return a hierarchy of other composables in order to define their UI. Let’s see a simple composable that defines a contact row UI that contains a user photo, and a name and phone number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ContactRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;painter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;painterResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drawable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;contentDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"A photo of a user"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nc"&gt;Column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Row&lt;/code&gt; composable is a layout composable that renders its children one next to another. The &lt;code&gt;Image&lt;/code&gt; composable is the first child which is going to render the &lt;code&gt;user&lt;/code&gt; drawable. Then we have the &lt;code&gt;Column&lt;/code&gt; composable which, similar to the &lt;code&gt;Row&lt;/code&gt;, is a layout composable, but it renders its children one below another. The children of the &lt;code&gt;Column&lt;/code&gt; composable are two &lt;code&gt;Text&lt;/code&gt; composables that render the user’s name and phone number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modifiers
&lt;/h3&gt;

&lt;p&gt;Modifiers are used to change the visual appearance and behavior of the composables they’re added to. We use modifiers when we want to change UI elements such as the size of the composable (width, height), the padding, background, or alignment.&lt;/p&gt;

&lt;p&gt;Modifiers can also be stacked on top of each other, allowing us to modify multiple visual properties. Here’s an example of how we can set the padding and max width of the Contact row from the previous snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ContactRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you interact with data in Jetpack Compose?
&lt;/h2&gt;

&lt;p&gt;There are multiple ways to keep data within your Jetpack Compose app: &lt;a href="https://volcano-bovid-81c.notion.site/Getting-Started-with-Jetpack-Compose-7187d91a2f0c4e56969db56c51c91ec1" rel="noopener noreferrer"&gt;MutableState&lt;/a&gt;, &lt;a href="https://volcano-bovid-81c.notion.site/Getting-Started-with-Jetpack-Compose-7187d91a2f0c4e56969db56c51c91ec1" rel="noopener noreferrer"&gt;LiveData&lt;/a&gt;, and &lt;a href="https://volcano-bovid-81c.notion.site/Getting-Started-with-Jetpack-Compose-7187d91a2f0c4e56969db56c51c91ec1" rel="noopener noreferrer"&gt;StateFlow&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  MutableState
&lt;/h3&gt;

&lt;p&gt;In Jetpack Compose, state management can be accomplished by using the &lt;code&gt;remember&lt;/code&gt; API to store an object in memory, and the &lt;code&gt;mutableStateOf&lt;/code&gt; to declare a state variable. We can store both mutable and immutable objects. The &lt;code&gt;mutableStateOf&lt;/code&gt; creates an observable &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/MutableState" rel="noopener noreferrer"&gt;&lt;code&gt;MutableState&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/a&gt;, which is an observable type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;MutableState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;T&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any changes to &lt;code&gt;value&lt;/code&gt; schedules a recomposition (re-rendering) of any composable functions that read it.&lt;br&gt;
There are three ways to declare a &lt;code&gt;MutableState&lt;/code&gt; object:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;val mutableState = remember { mutableStateOf(0) }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;var value by remember { mutableStateOf(false) }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;val (value, setValue) = remember { mutableStateOf("Hello, Compose!") }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  LiveData
&lt;/h3&gt;

&lt;p&gt;LiveData is a data holder class that can be observed within a given lifecycle, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This ensures LiveData only updates observers that are in an active lifecycle state, which also ensures no memory leaks happen within your app.&lt;/p&gt;

&lt;p&gt;Let’s see an example of working with LiveData:&lt;br&gt;
1. You need to create an instance of the &lt;code&gt;LiveData&lt;/code&gt; class to hold a certain type of data, which is usually done within your &lt;a href="https://developer.android.com/reference/androidx/lifecycle/ViewModel" rel="noopener noreferrer"&gt;ViewModel&lt;/a&gt; class (use &lt;code&gt;MutableLiveData&lt;/code&gt; if you’d like to update the value at some point):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create a MutableLiveData instance that keeps a string&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableLiveData&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Obtain the value in your composable by calling the &lt;code&gt;observeAsState&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;HomeScreen&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create an observer of the state of userName&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observeAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Use the value in your UI&lt;/span&gt;
  &lt;span class="nc"&gt;Column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3. To update &lt;code&gt;userName&lt;/code&gt;'s value (also usually done in the view model), create a function that sets the new value to its &lt;code&gt;value&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newName&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. You’d use the new function in your Compose file as &lt;code&gt;viewModel.updateUserName("...")&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  StateFlow
&lt;/h3&gt;

&lt;p&gt;StateFlow is a newer alternative to LiveData. Both have similarities, and both are observable. Here’s how you can work with StateFlow in Jetpack Compose:&lt;br&gt;
1. Create an instance of &lt;code&gt;StateFlow&lt;/code&gt; to hold a certain type of data (use MutableStateFlow if you’d like to update the value at some point)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create a MutableStateFlow instance that keeps a string&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2. Obtain the value in your composable by calling the &lt;code&gt;collectAsState&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;HomeScreen&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create an observer to collect the state of userName&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Use the value in your UI&lt;/span&gt;
  &lt;span class="nc"&gt;Column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3. To update &lt;code&gt;userName&lt;/code&gt;'s value (also usually done in the view model), create a function that sets the new value to its &lt;code&gt;value&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newName&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4. You’d use the new function in your Compose file as &lt;code&gt;viewModel.updateUserName("...")&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the best practices for Jetpack Compose?
&lt;/h2&gt;

&lt;p&gt;Aside from the &lt;a href="https://developer.android.com/jetpack/compose/performance/bestpractices" rel="noopener noreferrer"&gt;official best practices&lt;/a&gt; documentation, we’ve got a few additional tips that would make your codebase safer and easier to work in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code organization
&lt;/h3&gt;

&lt;p&gt;Every developer or organization has their own opinions on how a project should be structured. There is no “right” or “wrong” way to do it. Okay, maybe it’s wrong to put every file in one single directory 😅. Here’s an example structure to help you get started, which you can modify and evolve as your project grows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;.
├─ 📁 &lt;span class="gs"&gt;**ui**&lt;/span&gt; (to keep all your UI related things)
|  ├─ 📁 &lt;span class="gs"&gt;**screens**&lt;/span&gt; (where you define your screens composables and their corresponding view models)
|  |  └─ 📁 &lt;span class="gs"&gt;**home**&lt;/span&gt;
|  |     ├─ 📝 &lt;span class="gs"&gt;**HomeScreen.kt**&lt;/span&gt; (the UI for the Home screen)
|  |     └─ 📝 &lt;span class="gs"&gt;**HomeViewModel.kt**&lt;/span&gt; (the view model for the Home screen)
|  ├─ 📁 &lt;span class="gs"&gt;**components**&lt;/span&gt; (where you define components that are shared across multiple screens)
|  |  └─ 📝 &lt;span class="gs"&gt;**UserList.kt**&lt;/span&gt;
|  └─ 📁 &lt;span class="gs"&gt;**theme**&lt;/span&gt; (where you keep your theme definition and design tokens)
|     ├─ 📝 &lt;span class="gs"&gt;**Colors.kt**&lt;/span&gt;
|     ├─ 📝 &lt;span class="gs"&gt;**Shapes.kt**&lt;/span&gt;
|     ├─ 📝 &lt;span class="gs"&gt;**Theme.kt**&lt;/span&gt;
|     └─ 📝 &lt;span class="gs"&gt;**Typography.kt**&lt;/span&gt;
├─ 📁 &lt;span class="gs"&gt;**utils**&lt;/span&gt; (where you keep your various utility functions, like data converters etc...)
|  └─ 📝 &lt;span class="gs"&gt;**DateUtils.kt**&lt;/span&gt; 
└─ 📝 &lt;span class="gs"&gt;**MainActivity.kt**&lt;/span&gt; (this is your default MainActivity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Avoid creating “god” files
&lt;/h3&gt;

&lt;p&gt;“God” files are a big no-no. They’re files that contain all code associated with them: UI, domain, business logic, utility functions etc… It might be easier putting everything into one file, but maintaining that would get harder and harder as you add functionalities. The solution to this is using a proper architecture in your Jetpack Compose app.&lt;/p&gt;

&lt;p&gt;There are multiple architectures that you can use, all with their own pros and cons. The most common one in Jetpack Compose is MVVM, abbreviated from Model-View-ViewModel, because Jetpack Compose has a first-class &lt;a href="https://developer.android.com/topic/libraries/architecture/viewmodel" rel="noopener noreferrer"&gt;&lt;code&gt;ViewModel&lt;/code&gt;&lt;/a&gt; implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stay true to the MVVM
&lt;/h3&gt;

&lt;p&gt;As you saw from the previous examples, Jetpack Compose has a first-class &lt;a href="https://developer.android.com/topic/libraries/architecture/viewmodel" rel="noopener noreferrer"&gt;&lt;code&gt;ViewModel&lt;/code&gt;&lt;/a&gt; implementation. The MVVM, or Model-View-ViewModel, is a software design pattern that is structured to separate business logic from the UI. That means, your UI should not handle state updates, but it should let the view model do that by sending it user actions.&lt;/p&gt;

&lt;p&gt;Let’s explore that with an example. Remember the &lt;code&gt;MutableStateFlow&lt;/code&gt; example from before? That example was oversimplified on purpose, but in a real-world project you would never expose a &lt;code&gt;MutableStateFlow&lt;/code&gt; from your &lt;code&gt;ViewModel&lt;/code&gt;, but just a &lt;code&gt;StateFlow&lt;/code&gt;. In order to make that work, you should define a private &lt;code&gt;MutableStateFlow&lt;/code&gt; variable and a public &lt;code&gt;StateFlow&lt;/code&gt; variable that returns the mutable flow by invoking the &lt;code&gt;asStateFlow()&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create a private MutableStateFlow instance that keeps a string&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Create a public StateFlow that returns the MutableStateFlow as immutable&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asStateFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this simple change, we’re preventing the UI from being able to change the state. But, how do we actually change the state? We’ll expose a function from the view model that does that!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asStateFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Create a public function that updates the private MutableStateFlow value&lt;/span&gt;
  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;setUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_userName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newName&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now the UI has an immutable &lt;code&gt;StateFlow&lt;/code&gt; that it can observe, and a function to update its value. The business logic lives inside of the view model, while the Composable is only responsible to react to state changes and send user actions to the view model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don’t create a thousand flows
&lt;/h3&gt;

&lt;p&gt;So you’ve learned how to create state flows. Great! Would you repeat the same for every state variable you need in your UI? Please don’t 😅 To avoid that, you can create a &lt;code&gt;data class&lt;/code&gt; that keeps all of the values of your state, and create a single flow that uses it.&lt;/p&gt;

&lt;p&gt;Let’s learn this with an example. If we wanted to also keep the user’s phone number, email and address, we can create a data class called &lt;code&gt;HomeScreenState&lt;/code&gt; that contains all those values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;HomeScreenState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userPhone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we would refactor our view model to use the new &lt;code&gt;HomeScreenState&lt;/code&gt; instead of a &lt;code&gt;String&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_uiState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HomeScreenState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;uiState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HomeScreenState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asStateFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we can use all of the values in our composable by &lt;code&gt;viewModel.uiState.userName&lt;/code&gt;. If we also wanted to be able to update all those values, we would create functions for each of them in our view model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_uiState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HomeScreenState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;uiState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HomeScreenState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asStateFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;userName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newName&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateUserEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;userEmail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newEmail&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep a close eye on your errors and performance in production
&lt;/h3&gt;

&lt;p&gt;As you're getting acclimated to Jetpack Compose, an error and performance monitoring tool can be really helpful to reduce your learning curve and ensure that your app is bug-free. Jetpack Compose does a lot of heavy lifting for developers – as a declarative toolkit, developers need to write less code to describe their UI, and Jetpack Compose takes care of the rest. But it does abstract away a lot of code, making it difficult to identify errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sentry.io/for/android/?utm_medium=website&amp;amp;utm_source=jetpack-compose-getting-started&amp;amp;utm_campaign=android&amp;amp;utm_content=blog&amp;amp;utm_term=" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt; offers an out-of-the-box integration that can help you build a better Jetpack Compose app. The integration gives precise context to reduce troubleshooting time with transactions and breadcrumbs. Keep an eye on all the issues and crashes your app is experiencing in production, with a lot of context as to why the issue happened, the exact line of code that triggered it, and all sorts of hardware and software info of the device it ran.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzowc7j7wb86pi5wn876.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzowc7j7wb86pi5wn876.png" alt="Android app monitored in Sentry" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I’d totally understand if you’re feeling overwhelmed by now, but let’s do a quick recap! We’ve learned how to create a new Jetpack Compose project, and that Jetpack Compose uses Composables and Modifiers to define the view hierarchy and apply visual changes. Data in Jetpack Compose can be handled either with a &lt;code&gt;MutableState&lt;/code&gt;, &lt;code&gt;LiveData&lt;/code&gt;, or &lt;code&gt;StateFlow&lt;/code&gt;, which make the composables that observe it re-render when the value changes, making our UI dynamic. We also learned how to keep our projects tidy, and how to write maintainable composables and view models.&lt;/p&gt;

&lt;p&gt;Even though it’s a relatively new technology, Jetpack Compose’s ecosystem is steadily growing, so we can expect to see a lot of libraries pop up that make it easier to create Jetpack Compose apps. With companies like Lyft, Twitter, Airbnb, Square, Reddit, and Firefox putting their trust into it, more and more developers will follow along and create apps, libraries and resources for Jetpack Compose.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Mobile: The Future is Declarative</title>
      <dc:creator>Lazar Nikolov</dc:creator>
      <pubDate>Fri, 30 Dec 2022 10:55:05 +0000</pubDate>
      <link>https://dev.to/sentry/mobile-the-future-is-declarative-1bc9</link>
      <guid>https://dev.to/sentry/mobile-the-future-is-declarative-1bc9</guid>
      <description>&lt;p&gt;The mobile development ecosystem has always been very diverse, arguably more diverse than the web development ecosystem. While it seems like every day there are more frameworks and tools for web developers, a lot of them are built on top of JavaScript and implement similar patterns to each other. The mobile ecosystem, on the other hand, has a core set of languages that make the differences between mobile tools and frameworks much easier to identify.&lt;/p&gt;

&lt;p&gt;Two of the leading native mobile platforms are native Android and iOS, both of which have had interesting innovations recently. With the introductions of Jetpack Compose and SwiftUI, developing native apps looks very similar to developing React Native or Flutter apps. Both React Native and Flutter have a declarative approach from the start, but with Android and iOS now joining the declarative bandwagon, we can see that the future of mobile development is declarative.&lt;/p&gt;

&lt;h2&gt;
  
  
  A little history
&lt;/h2&gt;

&lt;p&gt;While React Native and Flutter have always taken a declarative approach, Android and iOS started completely different.&lt;/p&gt;

&lt;p&gt;Android utilized (and still does) Views. Views are XML files where we define our user interfaces using widgets like Button, TextView, LinearLayout and where we assign IDs to those widgets. Those IDs are then used to reference widgets in our Java files where we develop the functionality and behavior. Here’s an example View file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;LinearLayout&lt;/span&gt;
  &lt;span class="na"&gt;xmlns:android=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.android.com/apk/res/android"&lt;/span&gt;
  &lt;span class="na"&gt;android:layout_width=&lt;/span&gt;&lt;span class="s"&gt;"match_parent"&lt;/span&gt;
  &lt;span class="na"&gt;android:layout_height=&lt;/span&gt;&lt;span class="s"&gt;"match_parent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;TextView&lt;/span&gt;
    &lt;span class="na"&gt;android:id=&lt;/span&gt;&lt;span class="s"&gt;"@+id/text_view_id"&lt;/span&gt;
    &lt;span class="na"&gt;android:layout_height=&lt;/span&gt;&lt;span class="s"&gt;"wrap_content"&lt;/span&gt;
    &lt;span class="na"&gt;android:layout_width=&lt;/span&gt;&lt;span class="s"&gt;"wrap_content"&lt;/span&gt;
    &lt;span class="na"&gt;android:text=&lt;/span&gt;&lt;span class="s"&gt;"@string/hello"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/LinearLayout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we would create a reference to the widget like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Bundle&lt;/span&gt; &lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onCreate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;setContentView&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;R&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;activity_main&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Create a reference to the TextView using its ID&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TextView&lt;/span&gt; &lt;span class="n"&gt;helloTextView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TextView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;findViewById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;R&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text_view_id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Change the TextView's text&lt;/span&gt;
    &lt;span class="n"&gt;helloTextView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;R&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user_greeting&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS did have some declarative features, like Auto Layout, UIAppearance, the Objective-C @property declarations, KVC collection operators, and Combine, but it still required writing some level of imperative code.&lt;/p&gt;

&lt;p&gt;For example, iOS had (and still has) Storyboards. Storyboards is a graphical tool we use to build our UIs. It is actually an XML file under the hood, but the developers almost never touch the XML code itself. Here’s how we added UI elements in our Storyboards, and created references and actions:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pTTpjeSARZw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Becoming declarative
&lt;/h2&gt;

&lt;p&gt;To refresh our memory, the &lt;strong&gt;imperative&lt;/strong&gt; approach is when you provide step-by-step instructions until you achieve the desired UI. The &lt;strong&gt;declarative&lt;/strong&gt; approach is when you describe how the final state of the desired UI should look.&lt;/p&gt;

&lt;p&gt;Android’s new Jetpack Compose is written in Kotlin, which is a programming language as opposed to XML, which is a markup language. The previous XML View example would look something like this in Jetpack Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ComponentActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Start defining the UI&lt;/span&gt;
    &lt;span class="nf"&gt;setContent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;BasicsCodelabTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Add a Row (alternative to LinearLayout)&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Add a Text (alternative to TextView)&lt;/span&gt;
          &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello, Sentry!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, building UIs with this approach requires a lot less code. And, since we stay in a programming language context, we can directly use variables and callbacks instead of creating references to widgets and attaching logic to those widgets. Any change in the values will trigger a recomposition (rerender), so our UI is always up to date.&lt;/p&gt;

&lt;p&gt;iOS’s SwiftUI is pretty much the same, just built with Swift instead. The previous Storyboard example would look something like this in SwiftUI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// handle onClick&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Button"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, much less code. And we’re in a programming language, so we can directly define the Button’s label and provide an &lt;code&gt;onClick&lt;/code&gt; callback without creating a reference.&lt;/p&gt;

&lt;p&gt;One issue we stumbled upon when working with the imperative UIKit is activating constraints of a view before adding it to the view hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;subview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;subview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leadingAnchor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;equalTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leadingAnchor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="c1"&gt;// ^^^ it's going to crash on this line with the error:&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Unable to activate constraint with anchors *** because they have&lt;/span&gt;
&lt;span class="c1"&gt;// no common ancestor. Does the constraint or its anchors reference&lt;/span&gt;
&lt;span class="c1"&gt;// items in different view hierarchies?  That's illegal.&lt;/span&gt;

&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swapping the &lt;code&gt;subview.leadingAnchor.constraint(...)&lt;/code&gt; line with the &lt;code&gt;view.addSubview(view)&lt;/code&gt; line will work without an error. Admittedly, it took me a while to understand what was going on when I first encountered this error, and I triggered this error a few more times until it became a muscle memory for me. But with the new declarative approach we’re not going to encounter this issue. (Muscle memory in coding is good, but it’s better to improve the developer experience).&lt;/p&gt;

&lt;p&gt;This shift towards declarative in Android and iOS is a huge step towards better developer experience and faster development. Defining your UI in a declarative way using a programming language solves a lot of the pain points that Android and iOS developers have.&lt;/p&gt;

&lt;p&gt;Here are some of the benefits of the declarative approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Theming becomes easier and more dynamic.&lt;/li&gt;
&lt;li&gt;State Management feels natural and actually plays a &lt;strong&gt;crucial role&lt;/strong&gt; in the new declarative approach.&lt;/li&gt;
&lt;li&gt;The composition approach allows us to compose our UI by nesting components, which is inferred from the nesting of the code’s block scope.&lt;/li&gt;
&lt;li&gt;Dynamic layouts and conditional rendering are now straightforward because we’re building our UIs with programming languages that have control structures and branching logic.&lt;/li&gt;
&lt;li&gt;It’s easier to &lt;code&gt;grep&lt;/code&gt; through Swift/Kotlin code than through XML.&lt;/li&gt;
&lt;li&gt;We can more uniformly refactor our code using the same tools that apply to the rest of the programming language.&lt;/li&gt;
&lt;li&gt;The code diffs in PRs are easier to understand.&lt;/li&gt;
&lt;li&gt;Since our UI elements are built with actual data structures (i.e. functions, classes, structs) as opposed to markup language, we have the possibility of doing unit tests on our Views as well (not available for Jetpack Compose at the time of writing this blog post).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, there are always some drawbacks to be aware of. It’s worth mentioning that both SwiftUI and Jetpack Compose are only 2-3 years old. A short research on their drawbacks will reveal the lack of documentation, smaller community, some performance issues (for example Android’s lazy columns), not all components from the previous frameworks are supported, and there are limitations when it comes to building more complex UIs. Though they are still evolving and improving, be mindful of the pain points if you decide to start working with them today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example UI
&lt;/h2&gt;

&lt;p&gt;Let’s look at how the same example UI is built in the declarative approach for both iOS and Android:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabevkardb0amesq5js7g.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabevkardb0amesq5js7g.jpeg" alt="An example UI on both iOS and Android platforms" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;iOS (SwiftUI):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;//: An example Modal UI built with SwiftUI&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;PlaygroundSupport&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;emailAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"envelope.fill"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;largeTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sign up to our newsletter!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fontWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Since you love our content so much, why not get them every day in your inbox?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multilineTextAlignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kt"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"john@doe.xyz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$emailAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Handle onClick logic&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Subscribe"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;black&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Present the view controller in the Live View window&lt;/span&gt;
&lt;span class="kt"&gt;PlaygroundPage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLiveView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;a href="https://gist.github.com/nikolovlazar/4cbfa053b0d866f9e7a47ee51157357a" rel="noopener noreferrer"&gt;View iOS gist here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Android (Jetpack Compose):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;horizontalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterHorizontally&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Filled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;246&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// similar to the iOS one&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Sign up to our newsletter!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fontWeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FontWeight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Since you love our content so much, why not get them every day in your inbox?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;textAlign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fontSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;verticalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterVertically&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
                &lt;span class="n"&gt;placeholder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"john@doe.xyz"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextFieldDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFieldColors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;White&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;border&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BorderStroke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;White&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxHeight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* handle onClick logic */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ButtonDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buttonColors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;contentColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;White&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;contentPadding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaddingValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vertical&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RoundedCornerShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxHeight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Subscribe"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;a href="https://gist.github.com/nikolovlazar/93f786e34cbf62d429908d0dd6c00d60" rel="noopener noreferrer"&gt;View Android gist here&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The declarative approach of building UIs brings us a ton of benefits and eliminates a lot of the “pain points” we had in the imperative approach, but it also introduces new ones. Building UIs requires a lot less time with the declarative approach, and a lot less code. It’s also easier to achieve dynamic layouts and conditional rendering.&lt;/p&gt;

&lt;p&gt;Since the native platforms are taking this approach, it also sets up the whole ecosystem for creating new frameworks on top of the native ones that will bring a lot more features and possibilities. It might be a bit early to call them “the industry standard” yet, but the mobile development world is about to get even more productive.&lt;/p&gt;

</description>
      <category>deno</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Measuring application performance in Swift using transactions</title>
      <dc:creator>Lazar Nikolov</dc:creator>
      <pubDate>Tue, 22 Nov 2022 21:43:00 +0000</pubDate>
      <link>https://dev.to/sentry/measuring-application-performance-in-swift-using-transactions-22dp</link>
      <guid>https://dev.to/sentry/measuring-application-performance-in-swift-using-transactions-22dp</guid>
      <description>&lt;p&gt;So you're building a mobile app that’s performing big data requests; or crunching big data. But now you're asking yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How will my app perform in production?&lt;/li&gt;
&lt;li&gt;How will it perform on a lower-tier phone?&lt;/li&gt;
&lt;li&gt;Is there a scenario where the function's execution time is unbearably long?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Sentry’s &lt;a href="https://docs.sentry.io/platforms/apple/guides/ios/performance/instrumentation/custom-instrumentation/?utm_medium=website&amp;amp;utm_source=blog&amp;amp;utm_campaign=mobile-swift&amp;amp;utm_content=&amp;amp;utm_term=" rel="noopener noreferrer"&gt;Custom Instrumentation&lt;/a&gt; you can keep an eye on those big data-handling functions. Let’s see how you can implement them in your Storyboard and SwiftUI projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;First, you need to setup &lt;a href="https://docs.sentry.io/product/performance/?utm_medium=website&amp;amp;utm_source=blog&amp;amp;utm_campaign=mobile-swift&amp;amp;utm_content=&amp;amp;utm_term=" rel="noopener noreferrer"&gt;performance monitoring&lt;/a&gt;. With performance monitoring, Sentry tracks application performance, measures metrics like throughput and latency, and displays the impact of errors across multiple services. &lt;/p&gt;

&lt;p&gt;To get your app setup with performance monitoring, you need to configure the traces sample rate in your &lt;code&gt;Swift.UI.App&lt;/code&gt; file's &lt;code&gt;init()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Sentry&lt;/span&gt;

&lt;span class="c1"&gt;// This should be in your SwiftUI.App file's init() method&lt;/span&gt;
&lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"[YOUR_DSN_HERE]"&lt;/span&gt;

  &lt;span class="c1"&gt;// Example uniform sample rate: capture 100% of transactions&lt;/span&gt;
  &lt;span class="c1"&gt;// In Production you will probably want a smaller number such as 0.5 for 50%&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tracesSampleRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

  &lt;span class="c1"&gt;// OR if you prefer, determine traces sample rate based on the&lt;/span&gt;
  &lt;span class="c1"&gt;// sampling context&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tracesSampler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="c1"&gt;// Don't miss any transactions for VIP users &lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&gt;"vip"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="c1"&gt;// 25% for everything else&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing custom transactions
&lt;/h2&gt;

&lt;p&gt;Now that you’ve got performance monitoring enabled, you can start setting up custom transactions in the functions you’d like to measure. The measurement starts when you start a new transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't forget to import Sentry's SDK at the top&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Sentry&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;syncPersonDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;// Give the transaction a descriptive name&lt;/span&gt;
    &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"transaction-name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Give the operation a descriptive name&lt;/span&gt;
    &lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"transaction-operation"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="c1"&gt;// perform the operation&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, let’s say you want to measure how long it takes to sync a database of all people within a company; the “person database”. The name and operation could be &lt;code&gt;"Sync person database"&lt;/code&gt; and &lt;code&gt;"data.update"&lt;/code&gt; respectfully.&lt;/p&gt;

&lt;p&gt;When you’re done with the operation you want to measure, you can call the &lt;code&gt;finish()&lt;/code&gt; method of the transaction. Finishing the transaction will stop the measurement and send it to your Sentry project.&lt;/p&gt;

&lt;p&gt;The final structure of your function should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't forget to import Sentry's SDK at the top&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Sentry&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;syncPersonDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Sync person database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// give it a name&lt;/span&gt;
    &lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"data.update"&lt;/span&gt; &lt;span class="c1"&gt;// and a name for the operation&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="c1"&gt;// perform the operation&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// stop the measurement and report it to Sentry&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring the performance
&lt;/h2&gt;

&lt;p&gt;So far you’ve set up the performance measuring mechanism of your &lt;code&gt;syncPersonDatabase&lt;/code&gt; function. Now you run your app...&lt;/p&gt;

&lt;p&gt;You run it again...&lt;/p&gt;

&lt;p&gt;Maybe you run it one more time for good measure...&lt;/p&gt;

&lt;p&gt;Okay, that should have kicked off a transaction. You visit your Sentry dashboard and open the Performance tab and see the new custom transaction appear at the bottom:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxsaxyh11b9thrpyqyyiw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxsaxyh11b9thrpyqyyiw.png" alt="Transactions Table" width="800" height="86"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking on the transaction and then into the “Suspect Span” found in the second table, you will see a detailed representation of the span operation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4m7rl1b67wi5571g842m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4m7rl1b67wi5571g842m.png" alt="Span Summary" width="799" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's break this view down a bit.&lt;/p&gt;

&lt;p&gt;The first thing that you’ll see is the Self Time Breakdown chart. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1k7idnl9j9n62lp3ynb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1k7idnl9j9n62lp3ynb0.png" alt="Span Summary Breakdown" width="800" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This chart visualises the p50, p75, p95 and p99 metrics, which are called “latency percentiles”. p50 means that 50% of the function executions are faster than the p50 value (let’s say 0.47ms). To learn more about the latency percentiles, check out the &lt;a href="https://docs.sentry.io/product/performance/metrics/#latency" rel="noopener noreferrer"&gt;Latency section&lt;/a&gt; in the Metrics documentation. &lt;/p&gt;

&lt;p&gt;In the top right corner you can see the Self Time Percentiles widgets. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9h8359y6hrv3sfd1ivxl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9h8359y6hrv3sfd1ivxl.png" alt="Span Summary Self Time Percentiles" width="633" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don’t want to look at the chart, these values will give you an insight on the average “p” values.&lt;/p&gt;

&lt;p&gt;Below the chart is the events table. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xp3eu4yaoef77ufnabw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xp3eu4yaoef77ufnabw.png" alt="Span Summary Events Table" width="799" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here if you notice an event that took longer than you anticipated, you can click on it to get more details about it, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The device that the user was using when the transaction occurred&lt;/li&gt;
&lt;li&gt;The device’s memory at the time of the transaction&lt;/li&gt;
&lt;li&gt;How long the user had been using the app prior to the transaction&lt;/li&gt;
&lt;li&gt;The version of the app&lt;/li&gt;
&lt;li&gt;The breadcrumbs (bits of events and interactions that led up to that transaction)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you enough information on the context and environment to be able to discover potential ways to refactor the function to improve its performance. And, you can get even more granular if your function has multiple steps of execution. You can measure each steps individually and still keep them under the umbrella of the main function transaction. &lt;/p&gt;

&lt;p&gt;Enter: &lt;strong&gt;child spans&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More granular measurement with child spans
&lt;/h2&gt;

&lt;p&gt;Child spans are like mini transactions that are attached to the main transaction. And child spans can have their own child spans as well. This feature allows you to measure your function’s performance in a greater detail, by starting a child span for every step of the function.&lt;/p&gt;

&lt;p&gt;Let’s say your function performs the following steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;1.&lt;/span&gt; Gather changed person properties
&lt;span class="p"&gt;2.&lt;/span&gt; Pass each of them through a validation mechanism
&lt;span class="p"&gt;3.&lt;/span&gt; Perform the updates to the database
&lt;span class="p"&gt;4.&lt;/span&gt; Save the current timestamp as the "lastUpdated"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can measure each step individually using child spans like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't forget to import Sentry's SDK at the top&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Sentry&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;syncPersonDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Sync person database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// give it a name&lt;/span&gt;
    &lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"data.update"&lt;/span&gt; &lt;span class="c1"&gt;// and a name for the operation&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// span the first child&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;gatherSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"gather-changed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// perform the gather operation&lt;/span&gt;
  &lt;span class="n"&gt;gatherSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// span the second child&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;validationSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"validate-changed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// perform the validation&lt;/span&gt;
  &lt;span class="n"&gt;validationSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// span the third child&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;updateSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"update-database"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// perform the update&lt;/span&gt;
  &lt;span class="n"&gt;updateSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// span the last child&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;timestampSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"update-timestamp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// perform the timestamp update&lt;/span&gt;
  &lt;span class="n"&gt;timestampSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember: Only finished spans will be sent along with the transaction. So you need to make sure that you’re finishing each child span before finishing the main transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring the performance of child spans
&lt;/h2&gt;

&lt;p&gt;Alright, you have set up child spans and you're ready to run your app again with the latest changes...&lt;/p&gt;

&lt;p&gt;You run it again...&lt;/p&gt;

&lt;p&gt;Maybe you run it one more time for good measure...&lt;/p&gt;

&lt;p&gt;Done! You head back to your Sentry dashboard and notice that the Suspect Spans table provides more information now. You can see the child spans, and how long they took to execute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzg6kb5di7clgbnk4htl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzg6kb5di7clgbnk4htl.png" alt="Suspect Spans Table" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking on the “View All Spans” button you can see and sort all of them.&lt;/p&gt;

&lt;p&gt;But you realize that your function has conditional steps that don’t always execute. Or maybe you have dynamically generated child spans whose name contains a unique id. To see the actual order of the child spans, you have to pick a specific event from the Events Table above and a new page will open with more details around that specific event.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnie7owzawf8r253wenc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnie7owzawf8r253wenc8.png" alt="Event Details" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see from the Gantt chart the order of execution of the child spans. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6ynm1wuayvao2l6cw44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6ynm1wuayvao2l6cw44.png" alt="Event Details Gantt Chart" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you can see that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;data.update&lt;/code&gt; encapsulates all child spans&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;gather-changed&lt;/code&gt; started executing first and took 16.02ms&lt;/li&gt;
&lt;li&gt;Then the &lt;code&gt;validate-changed&lt;/code&gt; ran for 5.64ms&lt;/li&gt;
&lt;li&gt;Then the &lt;code&gt;update-database&lt;/code&gt; took its 121.02ms to run&lt;/li&gt;
&lt;li&gt;And finally the &lt;code&gt;update-timestamp&lt;/code&gt; flashed for 5.63ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can identify which step contributes the most to the performance of your function with this. You can now turn our whole focus to improving the performance of your &lt;code&gt;update-database&lt;/code&gt; function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a transaction in multiple functions
&lt;/h2&gt;

&lt;p&gt;Now that you have a hang of measuring straightforward functions, you're ready to tackle something a bit more complex. You realize that you want to measure performance on multiple nested functions. You do not need to pass the transaction as an argument. Instead, you can bind the transaction to the current scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Sync person database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// give it a name&lt;/span&gt;
  &lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"data.update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// and a name for the operation&lt;/span&gt;
  &lt;span class="nv"&gt;bindToScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// bind the transaction to the curent scope&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can access this transaction without having to pass it as an argument while you're in the current scope. You could have a function &lt;code&gt;gatherChangedProperties&lt;/code&gt;, that calls another function &lt;code&gt;filterProperties&lt;/code&gt;. And to gain access to the transaction within the &lt;code&gt;filterProperties&lt;/code&gt;, you can directly reference &lt;code&gt;SentrySDK.span&lt;/code&gt;, and if no transaction exists yet, you can create it. Your structure should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Sync person database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// give it a name&lt;/span&gt;
  &lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"data.update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// and a name for the operation&lt;/span&gt;
  &lt;span class="nv"&gt;bindToScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// bind the transaction to the curent scope&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gatherChangedProperties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;gatherChangedProperties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;filteredProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filterProperties&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;filterProperties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// obtain the transaction&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;

  &lt;span class="c1"&gt;// if non existing, recreate it&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SentrySDK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// use it like a normal transaction&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;filterSpan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"filter-properties"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You can now create custom transactions to measure long running functions in your app with &lt;code&gt;SentrySDK.startTransaction(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can measure more granularly by starting child spans to our transaction for each logical step in our function with &lt;code&gt;transaction.startChild(...)&lt;/code&gt; to zero in on the slowest part of the function and improve it. &lt;/li&gt;
&lt;li&gt;If you’re measuring a more complex function with multiple branches, you can bind the transaction to the current scope by setting the &lt;code&gt;bindToScope&lt;/code&gt; property to &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Time to measure all the things! 🙌&lt;/p&gt;

</description>
      <category>swift</category>
      <category>sentry</category>
      <category>performance</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
