How Sentry’s SDK Performance Monitoring API Evolved
This guide traces the evolution of Sentry’s performance‑monitoring SDK, explains how transactions and spans were added, and details the identified issues—such as scope propagation, span‑ingestion modeling, nested transactions, and serialization challenges—along with the lessons learned for future improvements.
Introduction
In early 2019 Sentry began adding tracing capabilities to its SDKs, using the Python and JavaScript SDKs as proof‑of‑concept platforms. The initial proof‑of‑concept was released on 29 April 2019 and delivered to Sentry on 7 May 2019. The implementation reused the existing error‑reporting pipeline, extending the
Sentry Eventtype with a new transaction event type while keeping the same transport layer.
The design emphasized a clear distinction between Transaction and Span objects, but this reuse of the Event interface introduced several limitations that became apparent as the feature matured.
Identified Issues
Scope Propagation
The unified SDK architecture creates a hub per concurrency unit, each holding a client and a scope. The client stores configuration and sends data via the transport, while the scope holds contextual data such as tags and breadcrumbs. In JavaScript, the single‑threaded event loop and lack of a standard way to carry context across asynchronous calls mean that only a global hub is available, which leads to nondeterministic span trees when multiple asynchronous operations run concurrently.
Mobile SDKs face a similar problem: a single global hub is shared across threads, making it impossible to reliably determine the current scope in concurrent contexts.
When the SDK must handle both error reporting and tracing, the scope becomes overloaded with data that has conflicting propagation expectations (e.g., tags should propagate only downstream, while breadcrumbs should also propagate upstream), complicating the design.
Unable to Determine Current Span
In concurrent fetch scenarios, the SDK code retrieves the current span via getCurrentHub().getScope().getSpan(). Because all concurrent fetches share a single hub, the parent span reference can be wrong, producing nondeterministic span trees such as:
t1
\
|- http.client GET https://example.com/f1
t2
\
|- http.client GET https://example.com/f2or a flattened tree where both spans appear under a single parent, losing the hierarchical relationship.
Conflicting Data‑Propagation Expectations
Since span, tag, and breadcrumb data are stored together in the scope, it is unclear which parts should propagate to child functions and which should propagate back to callers, especially when errors bubble up. This makes it difficult to reliably report the SpanID of the span where an error occurred while preserving the full breadcrumb trail.
Span Ingestion Model
Sentry’s model treats every transaction as a root span marked with a name. All child spans must be kept in memory for the duration of the transaction, which can be costly for services that generate many spans (e.g., a backend that performs multiple storage queries per request).
By contrast, OpenTelemetry batches spans and sends them when a batch size or timeout is reached, allowing earlier spans to be flushed while later spans are still in progress. Sentry’s model forces the entire transaction tree to be retained until the outermost transaction completes.
Complex JSON Serialization of Transactions
Sentry distinguishes between transaction span and regular span. Transaction spans carry additional fields (e.g., name) and, when serialized to JSON, are embedded as an Event with a list of child spans. Regular spans are serialized directly as JSON objects. This dual representation leads to larger payloads and extra processing.
Transaction Span Event Attributes
When a transaction is converted to an event, it inherits many attributes that regular spans do not have, such as breadcrumbs, extra, contexts, event_id, fingerprint, release, environment, and user. These enrich the transaction but also increase payload size.
Lifecycle Hooks
Sentry’s SDK exposes a BeforeSend hook for error events, but transaction events were initially excluded to avoid breaking existing BeforeSend logic. To allow mutation of transaction events, an EventProcessor (a more general form of BeforeSend) was introduced, though it adds complexity and exposes internal transaction details.
Nested Transactions
The SDK does not prevent nesting of transactions. When a transaction starts inside another, the inner transaction “swallows” spans that belong to the outer transaction, resulting in missing data and double‑billing (each transaction generates its own event).
Spans Cannot Exist Outside Transactions
Any span that is not part of a transaction and lacks a transaction ancestor is dropped, which leads to loss of valuable tracing information, especially in front‑end environments where automatic transaction detection may be unreliable.
Missing Web Vitals Measurements
Web Vitals are collected as part of an automatic transaction. If the transaction finishes before the browser reports the final Web Vitals values, those measurements are lost, causing incomplete performance data.
Unreliable Front‑End Transaction Duration
Browser SDKs create a transaction for each page load or navigation and must decide when to end it. If the user closes the tab before the transaction finishes, all collected data is lost. The SDK therefore balances the risk of losing all data against sending incomplete data, using LCP as a fallback metric, though LCP may be sent before it is final.
Memory Buffer Impact on Servers
Keeping the full span tree in memory for every concurrent transaction can consume significant resources on high‑throughput servers, making a 100 % span‑and‑transaction retention model impractical.
Inability to Batch Transactions
Sentry’s ingestion model does not support batching multiple transactions into a single request. Each transaction triggers its own HTTP request, leading to inefficiency and high resource consumption under load.
Compatibility Issues
The special handling of Transaction Span is not compatible with OpenTelemetry’s model, making it difficult for users of the OpenTelemetry SDK to adopt Sentry without a dedicated exporter that has known limitations.
Summary
Building the current tracing implementation in Sentry revealed many constraints. The first group of problems stems from the SDK’s unified architecture and scope propagation, requiring internal redesign of core concepts such as breadcrumbs and scope handling. The second group concerns the span ingestion model, which affects performance, resource usage, and compatibility with other observability tools. Addressing these issues will likely require major version changes and coordinated effort across multiple teams, but the potential gains in efficiency, data completeness, and developer experience are substantial.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
