Skip to content

Commit

Permalink
🐛 [RUMF-670] wait for the DOM to be ready before getting the trace id (
Browse files Browse the repository at this point in the history
…#525)

Because the RUM SDK can be initialized synchronously when the document
is loading, the DOM may not be fully accessible when getting the trace
id set by APM. In particular, if meta tags are written after the SDK
snippet, the SDK will not have access to them during init.
  • Loading branch information
BenoitZugmeyer authored Sep 9, 2020
1 parent d169349 commit 0951a91
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum DOM_EVENT {
SCROLL = 'scroll',
TOUCH_START = 'touchstart',
VISIBILITY_CHANGE = 'visibilitychange',
DOM_CONTENT_LOADED = 'DOMContentLoaded',
}

export enum ResourceKind {
Expand Down
71 changes: 41 additions & 30 deletions packages/rum/src/performanceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ function supportPerformanceNavigationTimingEvent() {
}

export function startPerformanceCollection(lifeCycle: LifeCycle) {
handleRumPerformanceEntry(lifeCycle, retrieveInitialDocumentResourceTiming())
retrieveInitialDocumentResourceTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, timing)
})

if (supportPerformanceObject()) {
handlePerformanceEntries(lifeCycle, performance.getEntries())
Expand All @@ -93,55 +95,64 @@ export function startPerformanceCollection(lifeCycle: LifeCycle) {
}
}
if (!supportPerformanceNavigationTimingEvent()) {
retrieveNavigationTimingWhenLoaded((timing) => {
retrieveNavigationTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, timing)
})
}
}

export function retrieveInitialDocumentResourceTiming() {
let timing: RumPerformanceResourceTiming
export function retrieveInitialDocumentResourceTiming(callback: (timing: RumPerformanceResourceTiming) => void) {
runOnReadyState('interactive', () => {
let timing: RumPerformanceResourceTiming

const forcedAttributes = {
entryType: 'resource' as const,
initiatorType: FAKE_INITIAL_DOCUMENT,
traceId: getDocumentTraceId(document),
}
if (supportPerformanceNavigationTimingEvent() && performance.getEntriesByType('navigation').length > 0) {
const navigationEntry = performance.getEntriesByType('navigation')[0]
timing = { ...navigationEntry.toJSON(), ...forcedAttributes }
} else {
const relativePerformanceTiming = computeRelativePerformanceTiming()
timing = {
...relativePerformanceTiming,
decodedBodySize: 0,
duration: relativePerformanceTiming.responseEnd,
name: window.location.href,
startTime: 0,
...forcedAttributes,
const forcedAttributes = {
entryType: 'resource' as const,
initiatorType: FAKE_INITIAL_DOCUMENT,
traceId: getDocumentTraceId(document),
}
}
return timing
if (supportPerformanceNavigationTimingEvent() && performance.getEntriesByType('navigation').length > 0) {
const navigationEntry = performance.getEntriesByType('navigation')[0]
timing = { ...navigationEntry.toJSON(), ...forcedAttributes }
} else {
const relativePerformanceTiming = computeRelativePerformanceTiming()
timing = {
...relativePerformanceTiming,
decodedBodySize: 0,
duration: relativePerformanceTiming.responseEnd,
name: window.location.href,
startTime: 0,
...forcedAttributes,
}
}
callback(timing)
})
}

function retrieveNavigationTimingWhenLoaded(callback: (timing: RumPerformanceNavigationTiming) => void) {
function retrieveNavigationTiming(callback: (timing: RumPerformanceNavigationTiming) => void) {
function sendFakeTiming() {
callback({
...computeRelativePerformanceTiming(),
entryType: 'navigation',
})
}

if (document.readyState === 'complete') {
sendFakeTiming()
runOnReadyState('complete', () => {
// Send it a bit after the actual load event, so the "loadEventEnd" timing is accurate
setTimeout(monitor(sendFakeTiming))
})
}

function runOnReadyState(expectedReadyState: 'complete' | 'interactive', callback: () => void) {
if (document.readyState === expectedReadyState || document.readyState === 'complete') {
callback()
} else {
const eventName = expectedReadyState === 'complete' ? DOM_EVENT.LOAD : DOM_EVENT.DOM_CONTENT_LOADED
const listener = monitor(() => {
window.removeEventListener(DOM_EVENT.LOAD, listener)
// Send it a bit after the actual load event, so the "loadEventEnd" timing is accurate
setTimeout(monitor(sendFakeTiming))
window.removeEventListener(eventName, listener)
callback()
})

window.addEventListener(DOM_EVENT.LOAD, listener)
window.addEventListener(eventName, listener)
}
}

Expand Down
10 changes: 6 additions & 4 deletions packages/rum/test/performanceCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ describe('rum initial document resource', () => {
setupBuilder.cleanup()
})

it('creates a resource timing for the initial document', () => {
const timing = retrieveInitialDocumentResourceTiming()
expect(timing.entryType).toBe('resource')
expect(timing.duration).toBeGreaterThan(0)
it('creates a resource timing for the initial document', (done) => {
retrieveInitialDocumentResourceTiming((timing) => {
expect(timing.entryType).toBe('resource')
expect(timing.duration).toBeGreaterThan(0)
done()
})
})
})

0 comments on commit 0951a91

Please sign in to comment.