Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(browser): Fix ElementTiming span timestamps and attribute names#19261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| import type { SpanAttributes } from '@sentry/core'; | ||
| import { | ||
| browserPerformanceTimeOrigin, | ||
| getActiveSpan, | ||
| @@ -9,13 +8,12 @@ import { | ||
| SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, | ||
| spanToJSON, | ||
| startSpan, | ||
| timestampInSeconds, | ||
| } from '@sentry/core'; | ||
| import { addPerformanceInstrumentationHandler } from './instrument'; | ||
| import { getBrowserPerformanceAPI, msToSec } from './utils'; | ||
| // ElementTiming interface based on the W3C spec | ||
| interface PerformanceElementTiming extends PerformanceEntry { | ||
| export interface PerformanceElementTiming extends PerformanceEntry { | ||
| renderTime: number; | ||
| loadTime: number; | ||
| intersectionRect: DOMRectReadOnly; | ||
| @@ -49,72 +47,58 @@ export const _onElementTiming = ({ entries }: { entries: PerformanceEntry[] }): | ||
| ? spanToJSON(rootSpan).description | ||
| : getCurrentScope().getScopeData().transactionName; | ||
| const timeOrigin = browserPerformanceTimeOrigin(); | ||
| if (!timeOrigin) { | ||
| // If there's no reliable time origin, we might as well not record the spans here | ||
| // as their data will be unreliable. | ||
| return; | ||
| } | ||
| entries.forEach(entry => { | ||
| const elementEntry = entry as PerformanceElementTiming; | ||
| const { naturalWidth, naturalHeight, url, identifier, name, renderTime, loadTime, startTime, id, element } = | ||
| entry as PerformanceElementTiming; | ||
| // Skip entries without identifier (elementtiming attribute) | ||
| if (!elementEntry.identifier) { | ||
| // Skip: | ||
| // - entries without identifier (elementtiming attribute) | ||
| // - entries without startTime (e.g. 3rd party Image nodes w/o Timing-Allow-Origin header returned instantly from cache) | ||
| if (!identifier || !startTime) { | ||
| return; | ||
| } | ||
| // `name` contains the type of the element paint. Can be `'image-paint'` or `'text-paint'`. | ||
| // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming#instance_properties | ||
| const paintType = elementEntry.name as 'image-paint' | 'text-paint' | undefined; | ||
| const renderTime = elementEntry.renderTime; | ||
| const loadTime = elementEntry.loadTime; | ||
| // starting the span at: | ||
| // - `loadTime` if available (should be available for all "image-paint" entries, 0 otherwise) | ||
| // - `renderTime` if available (available for all entries, except 3rd party images, but these should be covered by `loadTime`, 0 otherwise) | ||
| // - `timestampInSeconds()` as a safeguard | ||
| // see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming/renderTime#cross-origin_image_render_time | ||
| const [spanStartTime, spanStartTimeSource] = loadTime | ||
| ? [msToSec(loadTime), 'load-time'] | ||
| : renderTime | ||
| ? [msToSec(renderTime), 'render-time'] | ||
| : [timestampInSeconds(), 'entry-emission']; | ||
| const duration = | ||
| paintType === 'image-paint' | ||
| ? // for image paints, we can acually get a duration because image-paint entries also have a `loadTime` | ||
| // and `renderTime`. `loadTime` is the time when the image finished loading and `renderTime` is the | ||
| // time when the image finished rendering. | ||
| msToSec(Math.max(0, (renderTime ?? 0) - (loadTime ?? 0))) | ||
| : // for `'text-paint'` entries, we can't get a duration because the `loadTime` is always zero. | ||
| 0; | ||
| const attributes: SpanAttributes = { | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.elementtiming', | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.elementtiming', | ||
| // name must be user-entered, so we can assume low cardinality | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', | ||
| // recording the source of the span start time, as it varies depending on available data | ||
| 'sentry.span_start_time_source': spanStartTimeSource, | ||
| 'sentry.transaction_name': transactionName, | ||
| 'element.id': elementEntry.id, | ||
| 'element.type': elementEntry.element?.tagName?.toLowerCase() || 'unknown', | ||
| 'element.size': | ||
| elementEntry.naturalWidth && elementEntry.naturalHeight | ||
| ? `${elementEntry.naturalWidth}x${elementEntry.naturalHeight}` | ||
| : undefined, | ||
| 'element.render_time': renderTime, | ||
| 'element.load_time': loadTime, | ||
| // `url` is `0`(number) for text paints (hence we fall back to undefined) | ||
| 'element.url': elementEntry.url || undefined, | ||
| 'element.identifier': elementEntry.identifier, | ||
| 'element.paint_type': paintType, | ||
| }; | ||
| // Span durations | ||
| // Case 1: Text nodes: point-in-time spans at `renderTime` | ||
| // Case 2: Image nodes: spans from `loadTime` to `renderTime` (i.e. "effective render time") | ||
| // Case 3: 3rd party Image nodes w/o Timing-Allow-Origin header: point-in-time spans at `loadTime` | ||
| // Case 4: Both times are 0 is already covered by the `startTime` check above | ||
| const relativeStartTime = loadTime > 0 ? loadTime : renderTime; | ||
| const relativeEndTime = renderTime > 0 ? renderTime : loadTime; | ||
| startSpan( | ||
| { | ||
| name: `element[${elementEntry.identifier}]`, | ||
| attributes, | ||
| startTime: spanStartTime, | ||
| name: `element[${identifier}]`, | ||
| attributes: { | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.elementtiming', | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.elementtiming', | ||
| // name must be user-entered, so we can assume low cardinality | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', | ||
| 'sentry.transaction_name': transactionName, | ||
| 'ui.element.id': id, | ||
| 'ui.element.type': element?.tagName?.toLowerCase() || 'unknown', | ||
| 'ui.element.width': naturalWidth, | ||
| 'ui.element.height': naturalHeight, | ||
| 'ui.element.render_time': renderTime, | ||
| 'ui.element.load_time': loadTime, | ||
| // `url` is `0`(number) for text paints (hence we fall back to undefined) | ||
| 'ui.element.url': url || undefined, | ||
| 'ui.element.identifier': identifier, | ||
| // `name` contains the type of the element paint. Can be `'image-paint'` or `'text-paint'`. | ||
| 'ui.element.paint_type': name, | ||
| }, | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| startTime: msToSec(timeOrigin + relativeStartTime), | ||
| onlyIfParent: true, | ||
| }, | ||
| span => { | ||
| span.end(spanStartTime + duration); | ||
| span.end(msToSec(timeOrigin + relativeEndTime)); | ||
| }, | ||
| ); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: I checked the
startTimeproperty and I think I'm getting confused on how the span duration is being determined, it seems like it represents the opposite of what we are doing here for the start time?I think what you have here make sense, but was wondering if it is consistently correct.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand the definition of
startTimecorrectly, the prop always returns the "longest" time available ( i.e.renderTime if !== 0, otherwiseloadTime. My idea for using the shorter of the two is that we can show a timespan for images representing the relative rendering time. Does this make sense? Happy to switch tostartTime, but unless I'm missing something, we'd always generate point-in-time spans. WDYT?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never been so confused by a spec lol, okay so here is what I understand:
For text: we don't know the
startTimeof the span, we know the render time tho. So in this caserelativeStartTime = renderTimeandrelativeEndTime = renderTime. Which makes them equal.For images: Each value tells a different story because they aren't related, load time is the timestamp it took to be loaded and attached to the element, while rendering should happen after. So kinda each represent an "endTime" of two different spans, a load span and a render span, in case of text we don't have a "load" span.
The question here is if
renderTimeandloadTimeare both end times for each respective span, what's the start time? I don't think we have that information here. Which makes me think point-in-time spans here make sense for these cases, or a metric even.There could be a long winded way to guess the start time with resource timing and try to match the resource with the image element but ehhh, I don't know if it is worth it.
Does that make any sense or did I confuse myself 😂 I will approve anyways to not drag this any longer, but just wanted to see what you think first.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, correct, text element timing will always be duration 0 (for better or worse)
Hmm I just assumed that rendering would happen right after loading completed, so my thinking was, instead of having yet again no duration at all for the span, we use the relative rendering time (i.e. renderTime - loadTime) for the span duration. But maybe that assumption is wrong?
So you mean we should create two spans? We could do that (tho both would have duration 0 then) but it would increase quota usage/billing accordingly.
I think theoretically, it should be
performance.timeOrigin, right? but if we let the span start from there, it would mess up the trace waterfall if an elementTiming span was added to e.g. a navigation span, rather than the pageload. I think the root issue is that ElementTiming, like classic web vitals, just doesn't play well withSPAs :(
Yeah, the SPA limitation basically was my reasoning for rather having point-in-time spans than potentially super long ones.
Interesting idea! We could give it a try though I believe the timing would be weird again, simply because ElementTiming measures its values from
performance.timeOrigin. So if we find out when we actually started loading the image via the ResourceTiming, and let the span start from this time on, the semantics around the span duration would somehow not tell us much either (?). Also, assuming we get aResourceTimingentry, our SDK should be able to collect aresource.imgspan anyway.Hmm that's a good point! Radical idea: What if we just delete all of the element timing span logic and create a
browserMetricsIntegration(or a more specific one)? Tracking Element timing spans wasn't documented, it never worked and the only thing we'd need to leave is theenableElementTimingoption.Though, before we do this, we need to think which metrics we'd actually collect.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in favor of this actually, for one we start breaking up the tracing integration, secondly, does that mean we could re-work into a metric instead? Happy to take that on if you have too much on your plate.
I think you are right on the other points, the span timing no matter where we assign its start and end doesn't make full sense, and then matching the same resource in two event pipelines is just asking for non-ending spans or spans with no start time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@logaretm if you could take a look at this and think about how we could track ET as metrics instead of spans that would be greatly appreciated! I'll hold off from merging this for the moment.