Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions sdk/highlight-run/src/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
} from './listeners/jank-listener/jank-listener'
import { HighlightFetchWindow } from './listeners/network-listener/utils/fetch-listener'
import { RequestResponsePair } from './listeners/network-listener/utils/models'
import { sanitizeUrl } from './listeners/network-listener/utils/network-sanitizer'
import { PageVisibilityListener } from './listeners/page-visibility-listener'
import {
PerformanceListener,
Expand Down Expand Up @@ -1122,11 +1123,53 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`,
this.listeners.push(
WebVitalsListener((data) => {
const { name, value } = data
const tags: { name: string; value: string }[] = []
const addTag = (n: string, v: string | undefined) => {
if (v) tags.push({ name: n, value: v })
}
switch (data.name) {
case 'LCP': {
const a = data.attribution
addTag('web_vital.element', a.element)
addTag(
'web_vital.attribution.url',
a.url ? sanitizeUrl(a.url) : undefined,
)
break
}
case 'CLS': {
const a = data.attribution
addTag('web_vital.element', a.largestShiftTarget)
addTag('web_vital.load_state', a.loadState)
break
}
case 'INP': {
const a = data.attribution
addTag('web_vital.element', a.eventTarget)
addTag('web_vital.event_type', a.eventType)
addTag('web_vital.load_state', a.loadState)
break
}
case 'FID': {
const a = data.attribution
addTag('web_vital.element', a.eventTarget)
addTag('web_vital.event_type', a.eventType)
break
}
case 'FCP': {
const a = data.attribution
addTag('web_vital.load_state', a.loadState)
break
}
case 'TTFB':
break
}
this.recordGauge({
name,
value,
group: window.location.href,
category: MetricCategory.WebVital,
tags: tags.length ? tags : undefined,
})
}),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { Metric, onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals'
import {
CLSMetricWithAttribution,
FCPMetricWithAttribution,
FIDMetricWithAttribution,
INPMetricWithAttribution,
LCPMetricWithAttribution,
onCLS,
onFCP,
onFID,
onINP,
onLCP,
onTTFB,
TTFBMetricWithAttribution,
} from 'web-vitals/attribution'

export const WebVitalsListener = (callback: (metric: Metric) => void) => {
/**
* Discriminated union of all web-vitals metrics with their per-metric
* attribution shape populated. Use `switch (metric.name)` in consumers to
* narrow to the specific attribution type.
*/
export type WebVitalMetric =
| CLSMetricWithAttribution
| FCPMetricWithAttribution
| FIDMetricWithAttribution
| INPMetricWithAttribution
| LCPMetricWithAttribution
| TTFBMetricWithAttribution

export const WebVitalsListener = (
callback: (metric: WebVitalMetric) => void,
) => {
onCLS(callback)
onFCP(callback)
onFID(callback)
Expand Down
62 changes: 55 additions & 7 deletions sdk/highlight-run/src/sdk/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,16 +648,64 @@ export class ObserveSDK implements Observe {
WebVitalsListener((data) => {
const { name, value } = data
const { hostname, pathname, href } = window.location
const attributes: Attributes = {
group: window.location.pathname,
category: MetricCategory.WebVital,
[SemanticAttributes.ATTR_URL_FULL]: sanitizeUrl(href),
[SemanticAttributes.ATTR_URL_PATH]: pathname,
[SemanticAttributes.ATTR_SERVER_ADDRESS]: hostname,
}
switch (data.name) {
case 'LCP': {
const a = data.attribution
if (a.element) attributes['web_vital.element'] = a.element
if (a.url)
attributes['web_vital.attribution.url'] = sanitizeUrl(
a.url,
)
break
}
case 'CLS': {
const a = data.attribution
if (a.largestShiftTarget)
attributes['web_vital.element'] = a.largestShiftTarget
if (a.loadState)
attributes['web_vital.load_state'] = a.loadState
break
}
case 'INP': {
const a = data.attribution
if (a.eventTarget)
attributes['web_vital.element'] = a.eventTarget
if (a.eventType)
attributes['web_vital.event_type'] = a.eventType
if (a.loadState)
attributes['web_vital.load_state'] = a.loadState
break
}
case 'FID': {
const a = data.attribution
if (a.eventTarget)
attributes['web_vital.element'] = a.eventTarget
if (a.eventType)
attributes['web_vital.event_type'] = a.eventType
break
}
case 'FCP': {
const a = data.attribution
if (a.loadState)
attributes['web_vital.load_state'] = a.loadState
break
}
case 'TTFB':
// No high-signal selector to attribute; timing breakdown
// is already captured by the metric value itself.
break
}
this.recordGauge({
name,
value,
attributes: {
group: window.location.pathname,
category: MetricCategory.WebVital,
[SemanticAttributes.ATTR_URL_FULL]: sanitizeUrl(href),
[SemanticAttributes.ATTR_URL_PATH]: pathname,
[SemanticAttributes.ATTR_SERVER_ADDRESS]: hostname,
},
attributes,
})
})
ViewportResizeListener((viewport: ViewportResizeListenerArgs) => {
Expand Down
Loading