Skip to content
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

[RUMF-407] improve resource timings collection #315

Merged
merged 8 commits into from
Mar 25, 2020
148 changes: 101 additions & 47 deletions packages/rum/src/resourceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,68 +48,122 @@ export function computeResourceKind(timing: PerformanceResourceTiming) {
return ResourceKind.OTHER
}

function areInOrder(...numbers: number[]) {
for (let i = 1; i < numbers.length; i += 1) {
if (numbers[i - 1] > numbers[i]) {
return false
}
}
return true
}

export function computePerformanceResourceDuration(entry: PerformanceResourceTiming): number {
const { duration, startTime, responseEnd } = entry

// Safari duration is always 0 on timings blocked by cross origin policies.
if (duration === 0 && startTime < responseEnd) {
return msToNs(responseEnd - startTime)
}

return msToNs(duration)
}

export function computePerformanceResourceDetails(
entry?: PerformanceResourceTiming
entry: PerformanceResourceTiming
): PerformanceResourceDetails | undefined {
if (!entry || !hasTimingAllowedAttributes(entry) || isCached(entry)) {
return undefined
}
const {
startTime,
fetchStart,
domainLookupStart,
domainLookupEnd,
connectStart,
secureConnectionStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
} = entry
let { redirectStart, redirectEnd } = entry

// Ensure timings are in the right order. On top of filtering out potential invalid
// PerformanceResourceTiming, it will ignore entries from requests where timings cannot be
// collected, for example cross origin requests without a "Timing-Allow-Origin" header allowing
// it.
if (
!isValidTiming(entry.connectStart, entry.connectEnd) ||
!isValidTiming(entry.domainLookupStart, entry.domainLookupEnd) ||
!isValidTiming(entry.responseStart, entry.responseEnd) ||
!isValidTiming(entry.requestStart, entry.responseStart) ||
!isValidTiming(entry.redirectStart, entry.redirectEnd) ||
!isValidTiming(entry.secureConnectionStart, entry.connectEnd)
!areInOrder(
startTime,
fetchStart,
domainLookupStart,
domainLookupEnd,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd
)
) {
return undefined
}
return {
connect: isRelevantTiming(entry.connectStart, entry.connectEnd, entry.fetchStart)
? formatTiming(entry.connectStart, entry.connectEnd)
: undefined,
dns: isRelevantTiming(entry.domainLookupStart, entry.domainLookupEnd, entry.fetchStart)
? formatTiming(entry.domainLookupStart, entry.domainLookupEnd)
: undefined,
download: formatTiming(entry.responseStart, entry.responseEnd),
firstByte: formatTiming(entry.requestStart, entry.responseStart),
redirect: isRelevantTiming(entry.redirectStart, entry.redirectEnd, 0)
? formatTiming(entry.redirectStart, entry.redirectEnd)
: undefined,
ssl:
entry.secureConnectionStart !== 0 &&
isRelevantTiming(entry.secureConnectionStart, entry.connectEnd, entry.fetchStart)
? formatTiming(entry.secureConnectionStart, entry.connectEnd)
: undefined,

// The only time fetchStart is different than startTime is if a redirection occured.
const hasRedirectionOccured = fetchStart !== startTime

if (hasRedirectionOccured) {
// Firefox doesn't provide redirect timings on cross origin requests. Provide a default for
// those.
if (redirectStart < startTime) {
redirectStart = startTime
}
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
if (redirectEnd < startTime) {
redirectEnd = fetchStart
}

// Make sure redirect timings are in order
if (!areInOrder(startTime, redirectStart, redirectEnd, fetchStart)) {
return undefined
}
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
}
}

function hasTimingAllowedAttributes(timing: PerformanceResourceTiming) {
return timing.responseStart > 0
}
const details: PerformanceResourceDetails = {
download: formatTiming(startTime, responseStart, responseEnd),
firstByte: formatTiming(startTime, requestStart, responseStart),
}

function isCached(timing: PerformanceResourceTiming) {
return timing.duration === 0
}
// Make sure a connection occured
if (connectEnd !== fetchStart) {
details.connect = formatTiming(startTime, connectStart, connectEnd)

function isValidTiming(start: number, end: number) {
return start >= 0 && end >= 0 && end >= start
}
// Make sure a secure connection occured
if (areInOrder(connectStart, secureConnectionStart, connectEnd)) {
details.ssl = formatTiming(startTime, secureConnectionStart, connectEnd)
}
}

// Make sure a domain lookup occured
if (domainLookupEnd !== fetchStart) {
details.dns = formatTiming(startTime, domainLookupStart, domainLookupEnd)
}

if (hasRedirectionOccured) {
details.redirect = formatTiming(startTime, redirectStart, redirectEnd)
}

/**
* Do not collect timing when persistent connection, cache, ...
* https://developer.mozilla.org/en-US/docs/Web/Performance/Navigation_and_resource_timings
*/
function isRelevantTiming(start: number, end: number, reference: number) {
return start !== reference || end !== reference
return details
}

function formatTiming(start: number, end: number) {
return { duration: msToNs(end - start), start: msToNs(start) }
function formatTiming(origin: number, start: number, end: number) {
return {
duration: msToNs(end - start),
start: msToNs(start - origin),
}
}

export function computeSize(entry?: PerformanceResourceTiming) {
return entry && hasTimingAllowedAttributes(entry) ? entry.decodedBodySize : undefined
export function computeSize(entry: PerformanceResourceTiming) {
// Make sure a request actually occured
if (entry.startTime < entry.responseStart) {
return entry.decodedBodySize
}
return undefined
}

export function isValidResource(url: string, configuration: Configuration) {
Expand Down
16 changes: 11 additions & 5 deletions packages/rum/src/rum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import lodashMerge from 'lodash.merge'

import { LifeCycle, LifeCycleEventType } from './lifeCycle'
import { matchRequestTiming } from './matchRequestTiming'
import { computePerformanceResourceDetails, computeResourceKind, computeSize, isValidResource } from './resourceUtils'
import {
computePerformanceResourceDetails,
computePerformanceResourceDuration,
computeResourceKind,
computeSize,
isValidResource,
} from './resourceUtils'
import { RumGlobal } from './rum.entry'
import { RumSession } from './rumSession'
import { trackView, viewContext, ViewMeasures } from './viewTracker'
Expand Down Expand Up @@ -266,18 +272,18 @@ export function trackRequests(
const kind = requestDetails.type === RequestType.XHR ? ResourceKind.XHR : ResourceKind.FETCH
addRumEvent({
date: getTimestamp(timing ? timing.startTime : requestDetails.startTime),
duration: msToNs(timing ? timing.duration : requestDetails.duration),
duration: timing ? computePerformanceResourceDuration(timing) : msToNs(requestDetails.duration),
evt: {
category: RumEventCategory.RESOURCE,
},
http: {
method: requestDetails.method,
performance: computePerformanceResourceDetails(timing),
performance: timing ? computePerformanceResourceDetails(timing) : undefined,
statusCode: requestDetails.status,
url: requestDetails.url,
},
network: {
bytesWritten: computeSize(timing),
bytesWritten: timing ? computeSize(timing) : undefined,
},
resource: {
kind,
Expand Down Expand Up @@ -322,7 +328,7 @@ export function handleResourceEntry(
}
addRumEvent({
date: getTimestamp(entry.startTime),
duration: msToNs(entry.duration),
duration: computePerformanceResourceDuration(entry),
evt: {
category: RumEventCategory.RESOURCE,
},
Expand Down
Loading