|
| 1 | +--- |
| 2 | +title: Span Links |
| 3 | +--- |
| 4 | + |
| 5 | +Span links associate one span with one or more other spans. The most important application of linking traces are frontend applications. |
| 6 | +It can also be useful in settings with producer/consumer patterns or batch operations. |
| 7 | +By using span links, we are able to display a user journey in our tracing views to make debugging of issues easier as developers get more context on what happened before a specific issue. |
| 8 | + |
| 9 | +A span link can store the context of (a.k.a. link to) any other span. When necessary, a special link type can be added (see [Link Types](#link-types)). |
| 10 | + |
| 11 | +Learn more about Span Links in [the RFC 141](https://github.com/getsentry/rfcs/blob/main/text/0141-linking-traces.md) or in the [OpenTelemetry docs](https://opentelemetry.io/docs/concepts/signals/traces/#span-links). |
| 12 | + |
| 13 | +## SDK Implementation Guideline |
| 14 | + |
| 15 | +- Links are added on a span level, as defined and specified by [OpenTelemetry](https://opentelemetry.io/docs/specs/otel/trace/api/#link). |
| 16 | +- In addition to linking to span IDs, a span link also holds meta information about the link, collected via attributes. |
| 17 | +- Span links MUST only link to other spans. We do not support linking a span to an error or other Sentry events. |
| 18 | +- For SDKs still having public APIs around transactions, their respective Transaction interface and `startTransaction` function(s) should support the same APIs. |
| 19 | + |
| 20 | +### Type Definitions |
| 21 | + |
| 22 | +SDKs should follow the OpenTelemetry spec for the Link interface as defined by the platform. |
| 23 | +Non-OTel SDKs should orient themselves on OTel, resulting in the interface below, or a related version that applies to the terminology and philosophy of the respective SDK: |
| 24 | + |
| 25 | +```ts {tabTitle:Types} |
| 26 | +// see https://github.com/open-telemetry/opentelemetry-js/blob/main/api/src/trace/link.ts |
| 27 | +// or interface of respective platform |
| 28 | +interface Link { |
| 29 | + // contains the SpanContext of the span to link to |
| 30 | + context: SpanContext; |
| 31 | + // key-value pair with primitive values |
| 32 | + attributes?: Attributes; |
| 33 | +} |
| 34 | + |
| 35 | + |
| 36 | +// see https://github.com/open-telemetry/opentelemetry-js/blob/main/api/src/trace/span_context.ts |
| 37 | +// or interface of respective platform |
| 38 | +interface SpanContext { |
| 39 | + traceId: string, |
| 40 | + spanId: string, |
| 41 | + traceFlags: number, |
| 42 | +} |
| 43 | + |
| 44 | +type Attributes = Record<string, AttributeValues> |
| 45 | +type AttributeValues = string | boolean | number | Array<string> | Array<boolean> | Array<number> |
| 46 | +``` |
| 47 | +
|
| 48 | +
|
| 49 | +### Required Span API |
| 50 | +
|
| 51 | +Ideally, the link is added when starting a span. An optional `links` attribute is added to the `startSpan` options. |
| 52 | +SDKs that don't offer span-centric APIs but e.g. transaction APIs, should ensure that their respective `start...` APIs (e.g. `startTransaction`) also offer a possibility to add links. |
| 53 | +
|
| 54 | +```ts {tabTitle:Span Options} |
| 55 | +function startSpan(options: StartSpanOptions); |
| 56 | + |
| 57 | +interface StartSpanOptions: { |
| 58 | + //... other options (name, attributes, etc) |
| 59 | + links?: Link[]; |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +Furthermore, the SDKs need to expose at least an `addLink` method on their respective Span interface. For completeness with OpenTelemetry, ideally they also expose `addLinks`: |
| 64 | + |
| 65 | +```ts {tabTitle:Span API} |
| 66 | +interface Span { |
| 67 | + // return value can differ depending on platform |
| 68 | + addLink(link: Link): this; |
| 69 | + addLinks(links: Link[]): this; |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +## Envelope Item Payload |
| 74 | + |
| 75 | +Span trees are serialized to transaction event envelopes in all Sentry SDKs. Therefore, the envelope item needs to accommodate span links |
| 76 | +in its payload. If the `links` entry is an empty array, it can be omitted from the envelope. |
| 77 | + |
| 78 | +The serialized `links` objects should always contain: |
| 79 | + |
| 80 | +- `span_id: string` - id of the span to link to |
| 81 | +- `trace_id: string` - trace id of the span to link to |
| 82 | +- `sampled: boolean` - required if sampling decision of the span to link to (corresponds to `traceFlags` in Otel span context converted to `boolean`) is available |
| 83 | +- `attributes:` - required if attributes were added to the link |
| 84 | + |
| 85 | +Optionally, the serialized link object can contain further fields from OTel like `traceState`, `isRemote` or `droppedAttributesCount` which will be just forwarded unless we find a use case for them. |
| 86 | + |
| 87 | +The OTel fields `spanId`, `traceId`, and `traceFlags` should be excluded from the links objects in the envelope to avoid duplicate data (e.g. `trace_id` vs. `traceId`). |
| 88 | + |
| 89 | +If the root span (previously known as transaction) has span links, the links are stored in the trace context. |
| 90 | + |
| 91 | +```ts {tabTitle:Example Trace Context} |
| 92 | +// event envelope item |
| 93 | +{ |
| 94 | + type: "transaction"; |
| 95 | + transaction: string; |
| 96 | + contexts: { |
| 97 | + trace: { |
| 98 | + span_id: string; |
| 99 | + parent_span_id: string; |
| 100 | + trace_id: string; |
| 101 | + // new field for links: |
| 102 | + links?: Array<{ |
| 103 | + "span_id": string, |
| 104 | + "trace_id": string, |
| 105 | + sampled?: boolean, // traceFlags from Otel converted to boolean |
| 106 | + attributes?: Record<string, AttributeValue>, |
| 107 | + // + potentially more fields 1:1 from Otel. e.g. (traceState, droppedAttributesCount, isRemote) |
| 108 | + }> |
| 109 | + // ... |
| 110 | + } |
| 111 | + } |
| 112 | + // ... |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +Links that are stored in child spans are serialized in `spans[i].links`. |
| 117 | + |
| 118 | +```ts {tabTitle:Example Span Link Data} |
| 119 | +// event envelope item |
| 120 | +{ |
| 121 | + type: "transaction"; |
| 122 | + transaction: string; |
| 123 | + spans: Array<{ |
| 124 | + span_id: string; |
| 125 | + parent_span_id: string; |
| 126 | + trace_id: string; |
| 127 | + // new field for links: |
| 128 | + links?: Array<{ |
| 129 | + "span_id": string, |
| 130 | + "trace_id": string, |
| 131 | + sampled?: boolean, |
| 132 | + attributes?: Record<string, AttributeValue>, |
| 133 | + }> |
| 134 | + // ... |
| 135 | + }> |
| 136 | + // ... |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +## Link Types |
| 141 | + |
| 142 | +Links don't require a special meaning or type but if necessary (e.g. for identifying special links in the product), set the `sentry.link.type` attribute on the link to define the link type. |
| 143 | +Any string can be used, but these types have predefined meanings: |
| 144 | + |
| 145 | +| `sentry.link.type` | Usage | UI Implications | |
| 146 | +|--------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------| |
| 147 | +| "previous_trace" | Linking e.g. a navigation span to the previous page load span. | Used to query linked traces. Shows a button to go to linked previous trace in the trace explorer. | |
| 148 | +| "next_trace" | Linking e.g. a page load span to the next navigation span. | Used to query linked traces. Shows a button to go to linked next trace in the trace explorer. | |
| 149 | + |
| 150 | +### `previous_trace` |
| 151 | + |
| 152 | +Frontend traces can be linked by adding span links between root spans. For example, a navigation span includes a link to the previous page load span. |
| 153 | + |
| 154 | +The span context inside the link object should be stored in a storage mechanism of choice (e.g. in-memory or `sessionStorage` in the browser). |
| 155 | + |
| 156 | +Therefore, on root span start: |
| 157 | +- Check if there is a previous span context stored. If yes, add the span link with the `'sentry.link.type': 'previous_trace'` attribute |
| 158 | +- Store the root span context as the previous root span in a storage mechanism of choice (e.g. in-memory or `sessionStorage` in the browser) |
| 159 | + |
| 160 | +SDKs are free to implement heuristics around how long a previous trace span context should be considered (max time) and store additional necessary data. |
| 161 | + |
| 162 | + |
| 163 | + |
| 164 | +#### Negatively Sampled Traces |
| 165 | + |
| 166 | +In many cases, with lower sample rates, we will not be able to provide a full trace link chain, due to some or many traces being negatively sampled. |
| 167 | + |
| 168 | +- Sampled root spans should still include a link to the previous, negatively sampled root span (`traceFlags` on the spanContext() carry |
| 169 | + the information that the previous trace root span was negatively sampled). |
| 170 | + |
| 171 | +This helps our product to hint that there would have been a previous trace, but it was negatively sampled. |
| 172 | + |
| 173 | +We will not link to the previous positively sampled trace if a negatively sampled trace is in-between (see Traces 2-4 in the diagram below). |
| 174 | +Furthermore, we will not show how many traces were negatively sampled in between two trace chains; only that there was at least one trace in between (see Trace 5-8). |
| 175 | + |
| 176 | + |
| 177 | + |
| 178 | +## Usage Example |
| 179 | + |
| 180 | +Adding span links should be possible at span start time, as well as when holding a reference to the span. |
| 181 | + |
| 182 | +In the example below, by adding span links, we can link from the last navigation trace all the way back to the initial pageload trace. |
| 183 | +By passing the `'sentry.link.type': 'previous_trace'` attribute, we can identify the link as a previous trace link in Sentry and display the spans accordingly. |
| 184 | + |
| 185 | +```ts {tabTitle:TypeScript} |
| 186 | +// 1st trace starts |
| 187 | +const pageloadSpan = startInactiveSpan(...) |
| 188 | + |
| 189 | +// 2nd trace starts |
| 190 | +const navigation1Span = startInactiveSpan({ |
| 191 | + name: '/users', |
| 192 | + links: [{ |
| 193 | + context: pageloadSpan.spanContext(), |
| 194 | + attributes: { |
| 195 | + 'sentry.link.type': 'previous_trace' |
| 196 | + } |
| 197 | + }] |
| 198 | +}); |
| 199 | + |
| 200 | +// 3rd trace starts |
| 201 | +const navigation2Span = startSpan({name: '/users/:id'}, (span) => { |
| 202 | + span.addLink({ |
| 203 | + context: navigation1Span.spanContext(), |
| 204 | + attributes: { |
| 205 | + 'sentry.link.type': 'previous_trace' |
| 206 | + } |
| 207 | + }) |
| 208 | +}) |
| 209 | +``` |
| 210 | + |
| 211 | +## Ingest/Relay |
| 212 | + |
| 213 | +Relay forwards the span links in the format that is required for further processing and storage. |
| 214 | +Importantly, Relay doesn't require span links to be defined. They are completely optional. |
| 215 | +Relay handles passing span links in the root span as well as in any child span (see [envelope item payload](#envelope-item-payload)). |
| 216 | + |
| 217 | +The expected type and structure of the links array and its contents is [specified in Relay](https://github.com/getsentry/relay/blob/master/relay-event-schema/src/protocol/span.rs#L753). |
0 commit comments