Skip to content

Commit 09e4ecc

Browse files
mydeaLms24
andauthored
docs: Add docs for new performance APIs (#10017)
This is a WIP document about the new performance APIs. I'm sure I forgot a bunch of things, but it's a start at least...! [View rendered](https://github.com/getsentry/sentry-javascript/blob/fn/docs-new-performance-apis/docs/v8-new-performance-apis.md) --------- Co-authored-by: Lukas Stracke <[email protected]>
1 parent 59ffba3 commit 09e4ecc

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

docs/v8-new-performance-apis.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# New Performance APIs in v8
2+
3+
> [!WARNING] This document is WIP. We are working on this while we are preparing v8.
4+
5+
In v8.0.0, we moved to new performance APIs. These APIs have been introduced in v7, so they can already be used there.
6+
However, in v8 we have removed the old performance APIs, so you have to update your manual instrumentation usage to the
7+
new APIs before updating to v8 of the JavaScript SDKs.
8+
9+
## Why?
10+
11+
In v8 of the JavaScript SDKs, we made the move to base the performance instrumentation of all Node-based SDKs to use
12+
[OpenTelemetry](https://opentelemetry.io/) under the hood. This has been done to better align with the broader
13+
ecosystem, and to allow to use common auto instrumentation packages to be able to cover more ground in the ever-changing
14+
JavaScript landscape.
15+
16+
Since the way that OpenTelemetry works differs from how the SDK used to work, this required some changes in order to be
17+
compatible.
18+
19+
Note that for Browser- or Edge-based SDKs, we are not (yet) using OpenTelemetry for auto instrumentation. However, in
20+
order to keep the SDKs isomorphic - especially for SDKs for Meta-Frameworks like Next.js, Sveltekit or Remix - we made
21+
the decision to align the performance APIs for all JavaScript-based SDKs.
22+
23+
## The "old" Way of Manual Performance Instrumentation
24+
25+
Previously, there where two key APIs for adding manual performance instrumentation to your applications:
26+
27+
- `startTransaction()`
28+
- `span.startChild()`
29+
30+
This showed the underlying data model that Sentry was originally based on, which is that there is a root **Transaction**
31+
which can have a nested tree of **Spans**.
32+
33+
## The new model: Goodbye Transactions, Hello Spans Everywhere!
34+
35+
In the new model, transactions are conceptually gone. Instead, you will _always_ operate on spans, no matter where in
36+
the tree you are. Note that in the background, spans _may_ still be grouped into a transaction for the Sentry UI.
37+
However, this happens transparently, and from an SDK perspective, all you have to think about are spans.
38+
39+
## The Span schema
40+
41+
Previously, spans & transactions had a bunch of properties and methods to be used. Most of these have been removed in
42+
favor of a slimmer, more straightforward API, which is also aligned with OpenTelemetry Spans. You can refer to the table
43+
below to see which things used to exist, and how they can/should be mapped going forward:
44+
45+
| Old name | Replace with |
46+
| --------------------- | ---------------------------------------------------- |
47+
| `traceId` | `spanContext().traceId` |
48+
| `spanId` | `spanContext().spanId` |
49+
| `parentSpanId` | Unchanged |
50+
| `status` | use utility method TODO |
51+
| `sampled` | `spanIsSampled(span)` |
52+
| `startTimestamp` | `startTime` - note that this has a different format! |
53+
| `tags` | `spanGetAttributes(span)`, or set tags on the scope |
54+
| `data` | `spanGetAttributes(span)` |
55+
| `transaction` | ??? Removed |
56+
| `instrumenter` | Removed |
57+
| `finish()` | `end()` |
58+
| `end()` | Same |
59+
| `setTag()` | `setAttribute()`, or set tags on the scope |
60+
| `setData()` | `setAttribute()` |
61+
| `setStatus()` | TODO: new signature |
62+
| `setHttpStatus()` | ??? TODO |
63+
| `setName()` | `updateName()` |
64+
| `startChild()` | Call `Sentry.startSpan()` independently |
65+
| `isSuccess()` | Removed (TODO) |
66+
| `toTraceparent()` | `spanToTraceHeader(span)` |
67+
| `toContext()` | Removed |
68+
| `updateWithContext()` | Removed |
69+
| `getTraceContext()` | `spanToTraceContext(span)` |
70+
71+
In addition, a transaction has this API:
72+
73+
| Old name | Replace with |
74+
| --------------------------- | ------------------------------------------------ |
75+
| `name` | `spanGetName(span)` (TODO) |
76+
| `trimEnd` | Removed |
77+
| `parentSampled` | `spanIsSampled(span)` & `spanContext().isRemote` |
78+
| `metadata` | `spanGetMetadata(span)` |
79+
| `setContext()` | Set context on scope instead |
80+
| `setMeasurement()` | ??? TODO |
81+
| `setMetadata()` | `spanSetMetadata(span, metadata)` |
82+
| `getDynamicSamplingContext` | ??? TODO |
83+
84+
### Attributes vs. Data vs. Tags vs. Context
85+
86+
In the old model, you had the concepts of **Data**, **Tags** and **Context** which could be used for different things.
87+
However, this has two main downsides: One, it is not always clear which of these should be used when. And two, not all
88+
of these are displayed the same way for transactions or spans.
89+
90+
Because of this, in the new model, there are only **Attributes** to be set on spans anymore. Broadly speaking, they map
91+
to what Data used to be.
92+
93+
If you still really _need_ to set tags or context, you can do so on the scope before starting a span:
94+
95+
```js
96+
Sentry.withScope(scope => {
97+
scope.setTag('my-tag', 'tag-value');
98+
Sentry.startSpan({ name: 'my-span' }, span => {
99+
// do something here
100+
// span will have the tags from the containing scope
101+
});
102+
});
103+
```
104+
105+
## Creating Spans
106+
107+
Instead of manually starting & ending transactions and spans, the new model does not differentiate between these two.
108+
Instead, you _always_ use the same APIs to start a new span, and it will automatically either create a new **Root Span**
109+
(which is just a regular span, only that it has no parent, and is thus conceptually roughly similar to a transaction) or
110+
a **Child Span** for whatever is the currently active span.
111+
112+
There are three key APIs available to start spans:
113+
114+
- `startSpan()`
115+
- `startSpanManual()`
116+
- `startInactiveSpan()`
117+
118+
All three span APIs take a `SpanContext` as a first argument, which has the following shape:
119+
120+
```ts
121+
interface SpanContext {
122+
// The only required field - the name of the span
123+
name: string;
124+
attributes?: SpanAttributes;
125+
op?: string;
126+
// TODO: Not yet implemented, but you should be able to pass a scope to base this off
127+
scope?: Scope;
128+
// TODO: The list below may change a bit...
129+
origin?: SpanOrigin;
130+
source?: SpanSource;
131+
metadata?: Partial<SpanMetadata>;
132+
}
133+
```
134+
135+
### `startSpan()`
136+
137+
This is the most common API that should be used in most circumstances. It will start a new span, make it the active span
138+
for the duration of a given callback, and automatically end it when the callback ends. You can use it like this:
139+
140+
```js
141+
Sentry.startSpan(
142+
{
143+
name: 'my-span',
144+
attributes: {
145+
attr1: 'my-attribute',
146+
attr2: 123,
147+
},
148+
},
149+
span => {
150+
// do something that you want to measure
151+
// once this is done, the span is automatically ended
152+
},
153+
);
154+
```
155+
156+
You can also pass an async function:
157+
158+
```js
159+
Sentry.startSpan(
160+
{
161+
name: 'my-span',
162+
attributes: {},
163+
},
164+
async span => {
165+
// do something that you want to measure
166+
await waitOnSomething();
167+
// once this is done, the span is automatically ended
168+
},
169+
);
170+
```
171+
172+
Since `startSpan()` will make the created span the active span, any automatic or manual instrumentation that creates
173+
spans inside of the callback will attach new spans as children of the span we just started.
174+
175+
Note that if an error is thrown inside of the callback, the span status will automatically be set to be errored.
176+
177+
### `startSpanManual()`
178+
179+
This is a variation of `startSpan()` with the only change that it does not automatically end the span when the callback
180+
ends, but you have to call `span.end()` yourself:
181+
182+
```js
183+
Sentry.startSpanManual(
184+
{
185+
name: 'my-span',
186+
},
187+
span => {
188+
// do something that you want to measure
189+
190+
// Now manually end the span ourselves
191+
span.end();
192+
},
193+
);
194+
```
195+
196+
In most cases, `startSpan()` should be all you need for manual instrumentation. But if you find yourself in a place
197+
where the automatic ending of spans, for whatever reason, does not work for you, you can use `startSpanManual()`
198+
instead.
199+
200+
This function will _also_ set the created span as the active span for the duration of the callback, and will _also_
201+
update the span status to be errored if there is an error thrown inside of the callback.
202+
203+
### `startInactiveSpan()`
204+
205+
In contrast to the other two methods, this does not take a callback and this does not make the created span the active
206+
span. You can use this method if you want to create loose spans that do not need to have any children:
207+
208+
```js
209+
Sentry.startSpan({ name: 'outer' }, () => {
210+
const inner1 = Sentry.startInactiveSpan({ name: 'inner1' });
211+
const inner2 = Sentry.startInactiveSpan({ name: 'inner2' });
212+
213+
// do something
214+
215+
// manually end the spans
216+
inner1.end();
217+
inner2.end();
218+
});
219+
```
220+
221+
No span will ever be created as a child span of an inactive span.
222+
223+
## Other Notable Changes
224+
225+
In addition to generally changing the performance APIs, there are also some smaller changes that this brings with it.
226+
227+
### Changed `SamplingContext` for `tracesSampler()`
228+
229+
Currently, `tracesSampler()` can receive an arbitrary `SamplingContext` passed as argument. While this is not defined
230+
anywhere in detail, the shape of this context will change in v8. Going forward, this will mostly receive the attributes
231+
of the span, as well as some other relevant data of the span. Some properties we used to (sometimes) pass there, like
232+
`req` for node-based SDKs or `location` for browser tracing, will not be passed anymore.
233+
234+
### No more `undefined` spans
235+
236+
In v7, the performance APIs `startSpan()` / `startInactiveSpan()` / `startSpanManual()` would receive an `undefined`
237+
span if tracing was disabled or the span was not sampled.
238+
239+
In v8, aligning with OpenTelemetry, these will _always_ return a span - _but_ the span may eb a Noop-Span, meaning a
240+
span that is never sent. This means you don't have to guard everywhere in your code anymore for the span to exist:
241+
242+
```ts
243+
Sentry.startSpan((span: Span | undefined) => {
244+
// previously, in order to be type safe, you had to do...
245+
span?.setAttribute('attr', 1);
246+
});
247+
248+
// In v8, the signature changes to:
249+
Sentry.startSpan((span: Span) => {
250+
// no need to guard anymore!
251+
span.setAttribute('attr', 1);
252+
});
253+
```

0 commit comments

Comments
 (0)