You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Aug 14, 2024. It is now read-only.
Copy file name to clipboardExpand all lines: src/docs/sdk/research/performance/index.mdx
+170Lines changed: 170 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,4 +24,174 @@ In the next section, we’ll discuss some of the shortcomings with the current m
24
24
25
25
## Identified Issues
26
26
27
+
While the reuse of the [Unified SDK architecture](https://develop.sentry.dev/sdk/unified-api/) (hubs, clients, scopes) and the transaction ingestion model have merits, experience revealed some issues that we categorize into two groups.
28
+
29
+
The first group has to do with scope propagation, in essence the ability to determine what the “current scope” is. This operation is required for both manual instrumentation in user code as well as for automatic instrumentation in SDK integrations.
30
+
31
+
The second group is for issues related to the wire format used to send transaction data from SDKs to Sentry.
32
+
33
+
## Scope Propagation
34
+
35
+
_This issue is tracked by [getsentry/sentry-javascript#3751](https://github.com/getsentry/sentry-javascript/issues/3751)._
36
+
37
+
The [Unified SDK architecture](https://develop.sentry.dev/sdk/unified-api/) is fundamentally based on the existence of a `hub` per unit of concurrency, each `hub` having a stack of pairs of `client` and `scope`. A `client` holds configuration and is responsible for sending data to Sentry by means of a `transport`, while a `scope` holds contextual data that gets appended to outgoing events, such as tags and breadcrumbs.
38
+
39
+
Every `hub` knows what the current scope is. It is always the scope on top of the stack. The difficult part is having a `hub` “per unit of concurrency”.
40
+
41
+
JavaScript, for example, is single-threaded with an event loop and async code execution. There is no standard way to carry contextual data that works across async calls. So for JavaScript browser applications, there is only one global `hub` shared for sync and async code.
42
+
43
+
A similar situation appears on Mobile SDKs. There is an user expectation that contextual data like tags, what the current user is, breadcrumbs, and other information stored on the `scope` to be available and settable from any thread. Therefore, in those SDKs there is only one global `hub`.
44
+
45
+
In both cases, everything was relatively fine when the SDK had to deal with reporting errors. With the added responsibility to track transactions and spans, the `scope` became a poor fit to store the current `span`, because it limits the existence of concurrent spans.
46
+
47
+
For Browser JavaScript, a possible solution is the use of [Zone.js](https://github.com/angular/angular/blob/master/packages/zone.js/README.md), part of the Angular framework. The main challenge is that it increases bundle size and may inadvertendly impact end user apps as it monkey-patches key parts of the JavaScript runtime engine.
48
+
49
+
The scope propagation problem became specially apparent when we tried to create a simpler API for manual instrumentation. The idea was to expose a `Sentry.trace` function that would implicitly propagate tracing and scope data, and support deep nesting with sync and async code.
50
+
51
+
As an example, let’s say someone wanted to measure how long searching through a DOM tree took, tracing this operation would look something like this:
52
+
53
+
```js
54
+
awaitSentry.trace(
55
+
{
56
+
op:'dom',
57
+
description:'Walk DOM Tree',
58
+
},
59
+
async () =>awaitwalkDomTree()
60
+
);
61
+
```
62
+
63
+
With the `Sentry.trace` function, users wouldn’t have to worry about keeping the reference to the correct transaction or span when adding timing data. Users are free to create child spans within the `walkDomTree` function and spans would be ordered in the correct hierarchy.
64
+
65
+
The implementation of the actual `trace` function is relatively simple (see [a PR which has an example implementation](https://github.com/getsentry/sentry-javascript/pull/3697/files#diff-f5bf6e0cdf7709e5675fcdc3b4ff254dd68f3c9d1a399c8751e0fa1846fa85dbR158)), however, knowing what is the current span in async code and global integrations is a challenge yet to be overcome.
66
+
67
+
The following two examples synthesize the scope propagation issues.
68
+
69
+
### 1. Cannot Determine Current Span
70
+
71
+
Consider some auto-instrumentation code that needs to get a reference to the current `span`, a case in which manual scope propagation is not available.
Promise.all([f1(), f2()]); // run f1 and f2 concurrently
116
+
```
117
+
118
+
In the example above, several concurrent `fetch` requests trigger the execution of the `fetchWrapper` helper. Line `<1>` must be able to observe a different span depending on the current flow of execution, leading to two span trees as below:
119
+
120
+
```
121
+
t1
122
+
\
123
+
|- http.client GET https://example.com/f1
124
+
t2
125
+
\
126
+
|- http.client GET https://example.com/f2
127
+
```
128
+
129
+
That means that, when `f1` is running, `parent` must refer to `t1` and, when `f2` is running, `parent` must be `t2`. Unfortunately, all code above is racing to update and read from a single `hub` instance, and thus the observed span trees are not deterministic. For example, the result could incorrectly be:
130
+
131
+
```
132
+
t1
133
+
t2
134
+
\
135
+
|- http.client GET https://example.com/f1
136
+
|- http.client GET https://example.com/f2
137
+
```
138
+
139
+
As a side-effect of not being able to correctly determine the current span, the present implementation of the `fetch` integration (and others) in [the JavaScript Browser SDK chooses to create flat transactions](https://github.com/getsentry/sentry-javascript/blob/61eda62ed5df5654f93e34a4848fc9ae3fcac0f7/packages/tracing/src/browser/request.ts#L169-L178), where all child spans are direct children of the transaction (instead of having a proper multi-level tree structure).
140
+
141
+
Note that other tracing libraries have the same kind of challenge. There are several (at the time open) issues in OpenTelemetry for JavaScript related to determining the parent span and proper context propagation (including async code):
142
+
143
+
-[Context leak if several TracerProvider instances are used #1932](https://github.com/open-telemetry/opentelemetry-js/issues/1932)
144
+
-[How to created nested spans without passing parents around #1963](https://github.com/open-telemetry/opentelemetry-js/issues/1963)
145
+
-[Nested Child spans are not getting parented correctly #1940](https://github.com/open-telemetry/opentelemetry-js/issues/1940)
-[Http Spans are not linked / does not set parent span #2333](https://github.com/open-telemetry/opentelemetry-js/issues/2333)
148
+
149
+
### 2. Conflicting Data Propagation Expectations
150
+
151
+
There is a conflict of expectations that appear whenever we add a `trace` function as discussed earlier, or simply try to address scope propagation with Zones.
152
+
153
+
The fact that the current `span` is stored in the `scope`, along with `tags`, `breadcrumbs` and more, makes data propagation messy as some parts of the `scope` are intended to propagate only into inner functions calls (for example, tags), while others are expected to propagate back into callers (for example, breadcrumbs), specially when there is an error.
154
+
155
+
Here is one example:
156
+
157
+
```js
158
+
functiona() {
159
+
trace((span, scope) => {
160
+
scope.setTag('func', 'a');
161
+
scope.setTag('id', '123');
162
+
scope.addBreadcrumb('was in a');
163
+
try {
164
+
b();
165
+
} catch(e) {
166
+
// How to report the SpanID from the span in b?
167
+
} finally {
168
+
captureMessage('hello from a');
169
+
// tags: {func: 'a', id: '123'}
170
+
// breadcrumbs: ['was in a', 'was in b']
171
+
}
172
+
})
173
+
}
174
+
175
+
functionb() {
176
+
trace((span, scope) => {
177
+
constfail=Math.random() >0.5;
178
+
scope.setTag('func', 'b');
179
+
scope.setTag('fail', fail.toString());
180
+
scope.addBreadcrumb('was in b');
181
+
captureMessage('hello from b');
182
+
// tags: {func: 'b', id: '123', fail: ?}
183
+
// breadcrumbs: ['was in a', 'was in b']
184
+
if (fail) {
185
+
throwError('b failed');
186
+
}
187
+
});
188
+
}
189
+
```
190
+
191
+
In the example above, if an error bubbles up the call stack we want to be able to report in which `span` (by referring to a `SpanID`) the error happened. We want to have breadcrumbs that describe everything that happened, no matter which Zones were executing, and we want a tag set in an inner Zone to override a tag with the same name from a parent Zone, while inherinting all other tags from the parent Zone. Every Zone has their own "current span".
192
+
193
+
All those different expectations makes it hard to reuse, in an understandable way, the current notion of `scope`, how breadcrumbs are recorded, and how those different concepts interact.
0 commit comments