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-636] initial document trace id #492

Merged
merged 10 commits into from Aug 24, 2020
Merged
5 changes: 3 additions & 2 deletions packages/core/src/cookie.ts
@@ -1,3 +1,5 @@
import { findCommaSeparatedValue } from './utils'

export const COOKIE_ACCESS_DELAY = 1000

export interface CookieCache {
Expand Down Expand Up @@ -43,8 +45,7 @@ export function setCookie(name: string, value: string, expireDelay: number) {
}

export function getCookie(name: string) {
const matches = document.cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)
return matches ? matches.pop() : undefined
return findCommaSeparatedValue(document.cookie, name)
}

export function areCookiesAuthorized(): boolean {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/utils.ts
Expand Up @@ -364,3 +364,8 @@ export function getLinkElementOrigin(element: Location | HTMLAnchorElement | URL
const sanitizedHost = element.host.replace(/(:80|:443)$/, '')
return `${element.protocol}//${sanitizedHost}`
}

export function findCommaSeparatedValue(rawString: string, name: string) {
const matches = rawString.match(`(?:^|;)\\s*${name}\\s*=\\s*([^;]+)`)
return matches ? matches[1] : undefined
bcaudan marked this conversation as resolved.
Show resolved Hide resolved
}
22 changes: 21 additions & 1 deletion packages/core/test/utils.spec.ts
@@ -1,4 +1,13 @@
import { deepMerge, jsonStringify, performDraw, round, throttle, toSnakeCase, withSnakeCaseKeys } from '../src/utils'
import {
deepMerge,
findCommaSeparatedValue,
jsonStringify,
performDraw,
round,
throttle,
toSnakeCase,
withSnakeCaseKeys,
} from '../src/utils'

describe('utils', () => {
describe('deepMerge', () => {
Expand Down Expand Up @@ -321,3 +330,14 @@ describe('utils', () => {
expect(round(10.12591, 3)).toEqual(10.126)
})
})

describe('findCommaSeparatedValue', () => {
it('returns the value from a comma separated hash', () => {
expect(findCommaSeparatedValue('foo=a;bar=b', 'foo')).toBe('a')
expect(findCommaSeparatedValue('foo=a;bar=b', 'bar')).toBe('b')
})

it('returns undefined if the value is not found', () => {
expect(findCommaSeparatedValue('foo=a;bar=b', 'baz')).toBe(undefined)
})
})
97 changes: 97 additions & 0 deletions packages/rum/src/getDocumentTraceId.ts
@@ -0,0 +1,97 @@
import { findCommaSeparatedValue, ONE_MINUTE } from '@datadog/browser-core'

interface DocumentTraceData {
traceId: string
traceTime: number
}

export const INITIAL_DOCUMENT_OUTDATED_TRACE_ID_THRESHOLD = 2 * ONE_MINUTE

export function getDocumentTraceId(document: Document): string | undefined {
const data = getDocumentTraceDataFromMeta(document) || getDocumentTraceDataFromComment(document)

if (!data || data.traceTime <= Date.now() - INITIAL_DOCUMENT_OUTDATED_TRACE_ID_THRESHOLD) {
return undefined
}

return data.traceId
}

export function getDocumentTraceDataFromMeta(document: Document): DocumentTraceData | undefined {
const traceIdMeta = document.querySelector<HTMLMetaElement>('meta[name=dd-trace-id]')
const traceTimeMeta = document.querySelector<HTMLMetaElement>('meta[name=dd-trace-time]')
return createDocumentTraceData(traceIdMeta && traceIdMeta.content, traceTimeMeta && traceTimeMeta.content)
}

export function getDocumentTraceDataFromComment(document: Document): DocumentTraceData | undefined {
const comment = findTraceComment(document)
if (!comment) {
return undefined
}
return createDocumentTraceData(
findCommaSeparatedValue(comment, 'trace-id'),
findCommaSeparatedValue(comment, 'trace-time')
)
}

export function createDocumentTraceData(
traceId: string | undefined | null,
rawTraceTime: string | undefined | null
): DocumentTraceData | undefined {
const traceTime = rawTraceTime && Number(rawTraceTime)
if (!traceId || !traceTime) {
return undefined
}

return {
traceId,
traceTime,
}
}

export function findTraceComment(document: Document): string | undefined {
// 1. Try to find the comment as a direct child of the document
// Note: TSLint advises to use a 'for of', but TS doesn't allow to use 'for of' if the iterated
// value is not an array or string (here, a NodeList).
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < document.childNodes.length; i += 1) {
const comment = getTraceCommentFromNode(document.childNodes[i])
if (comment) {
return comment
}
}

// 2. If the comment is placed after the </html> tag, but have some space or new lines before or
// after, the DOM parser will lift it (and the surrounding text) at the end of the <body> tag.
// Try to look for the comment at the end of the <body> by by iterating over its child nodes in
// reverse order, stoping if we come accross a non-text node.
if (document.body) {
for (let i = document.body.childNodes.length - 1; i >= 0; i -= 1) {
const node = document.body.childNodes[i]
const comment = getTraceCommentFromNode(node)
if (comment) {
return comment
}
if (!isTextNode(node)) {
break
}
}
}
}

function getTraceCommentFromNode(node: Node | null) {
if (node && isCommentNode(node)) {
const match = node.data.match(/^\s*DATADOG;(.*?)\s*$/)
if (match) {
return match[1]
}
}
}

function isCommentNode(node: Node): node is Comment {
return node.nodeName === '#comment'
}

function isTextNode(node: Node): node is Text {
return node.nodeName === '#text'
}
5 changes: 3 additions & 2 deletions packages/rum/src/lifeCycle.ts
@@ -1,4 +1,5 @@
import { ErrorMessage } from '@datadog/browser-core'
import { RumPerformanceEntry } from './performanceCollection'
import { RequestCompleteEvent, RequestStartEvent } from './requestCollection'
import { AutoUserAction, CustomUserAction } from './userActionCollection'
import { View } from './viewCollection'
Expand Down Expand Up @@ -28,7 +29,7 @@ export class LifeCycle {
private callbacks: { [key in LifeCycleEventType]?: Array<(data: any) => void> } = {}

notify(eventType: LifeCycleEventType.ERROR_COLLECTED, data: ErrorMessage): void
notify(eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, data: PerformanceEntry): void
notify(eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, data: RumPerformanceEntry): void
notify(eventType: LifeCycleEventType.REQUEST_STARTED, data: RequestStartEvent): void
notify(eventType: LifeCycleEventType.REQUEST_COMPLETED, data: RequestCompleteEvent): void
notify(eventType: LifeCycleEventType.AUTO_ACTION_COMPLETED, data: AutoUserAction): void
Expand All @@ -54,7 +55,7 @@ export class LifeCycle {
subscribe(eventType: LifeCycleEventType.ERROR_COLLECTED, callback: (data: ErrorMessage) => void): Subscription
subscribe(
eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
callback: (data: PerformanceEntry) => void
callback: (data: RumPerformanceEntry) => void
): Subscription
subscribe(eventType: LifeCycleEventType.REQUEST_STARTED, callback: (data: RequestStartEvent) => void): Subscription
subscribe(
Expand Down
9 changes: 6 additions & 3 deletions packages/rum/src/matchRequestTiming.ts
@@ -1,3 +1,4 @@
import { RumPerformanceResourceTiming } from './performanceCollection'
import { RequestCompleteEvent } from './requestCollection'

interface Timing {
Expand All @@ -21,9 +22,11 @@ export function matchRequestTiming(request: RequestCompleteEvent) {
if (!performance || !('getEntriesByName' in performance)) {
return
}
const candidates = performance
const candidates = (performance
.getEntriesByName(request.url, 'resource')
.filter((entry) => isBetween(entry, request.startTime, endTime(request))) as PerformanceResourceTiming[]
.filter((entry) =>
isBetween(entry, request.startTime, endTime(request))
) as unknown) as RumPerformanceResourceTiming[]

if (candidates.length === 1) {
return candidates[0]
Expand All @@ -36,7 +39,7 @@ export function matchRequestTiming(request: RequestCompleteEvent) {
return
}

function firstCanBeOptionRequest(correspondingEntries: PerformanceResourceTiming[]) {
function firstCanBeOptionRequest(correspondingEntries: RumPerformanceResourceTiming[]) {
return endTime(correspondingEntries[0]) <= correspondingEntries[1].startTime
}

Expand Down