|
| 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