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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)
})
})
89 changes: 89 additions & 0 deletions packages/rum/src/getAPMDocumentData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { findCommaSeparatedValue } from '@datadog/browser-core'

interface APMDocumentData {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
traceId: string
traceTime: number
}

export function getAPMDocumentData(document: Document): APMDocumentData | undefined {
return getAPMDocumentDataFromMeta(document) || getAPMDocumentDataFromComment(document)
}

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

export function getAPMDocumentDataFromComment(document: Document): APMDocumentData | undefined {
const comment = findAPMComment(document)
if (!comment) {
return undefined
}
return createAPMDocumentData(
findCommaSeparatedValue(comment, 'trace-id'),
findCommaSeparatedValue(comment, 'trace-time')
)
}

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

return {
traceId,
traceTime,
}
}

export function findAPMComment(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 = getAPMCommentFromNode(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 = getAPMCommentFromNode(node)
if (comment) {
return comment
}
if (!isTextNode(node)) {
break
}
}
}
}

function getAPMCommentFromNode(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'
}
144 changes: 144 additions & 0 deletions packages/rum/test/getAPMDocumentData.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {
createAPMDocumentData,
findAPMComment,
getAPMDocumentData,
getAPMDocumentDataFromMeta,
} from '../src/getAPMDocumentData'

const HTML_DOCTYPE = '\n<!DOCTYPE html>\n'
const HTML_CONTENT = '\n<html><head></head><body></body></html>\n'

describe('getAPMDocumentData', () => {
it('uses the meta strategy in priority', () => {
expect(
getAPMDocumentData(
createDocument(
`<!-- DATADOG;trace-id=comment;trace-time=123 -->
${HTML_DOCTYPE}
<html>
<head>
<meta name="dd-trace-id" content="meta" />
<meta name="dd-trace-time" content="456" />
</head>
<body>
</body>
</html>`
)
)
).toEqual({ traceId: 'meta', traceTime: 456 })
})

it('returns undefined if nothing is present', () => {
expect(getAPMDocumentData(createDocument(HTML_DOCTYPE + HTML_CONTENT))).toEqual(undefined)
})
})

describe('getAPMDocumentDataFromMeta', () => {
it('gets data from meta', () => {
expect(
getAPMDocumentDataFromMeta(
createDocument(
`${HTML_DOCTYPE}
<html>
<head>
<meta name="dd-trace-id" content="123" />
<meta name="dd-trace-time" content="456" />
</head>
<body>
</body>
</html>`
)
)
).toEqual({ traceId: '123', traceTime: 456 })
})

it('returns undefined if a meta is missing', () => {
expect(
getAPMDocumentDataFromMeta(
createDocument(
`${HTML_DOCTYPE}
<html>
<head>
<meta name="dd-trace-id" content="123" />
</head>
<body>
</body>
</html>`
)
)
).toEqual(undefined)
})
})

describe('findAPMComment', () => {
const DATADOG_COMMENT = '\n<!-- DATADOG;foo=bar -->\n'

it('returns undefined if no comment is present', () => {
expect(findAPMComment(createDocument(HTML_DOCTYPE + HTML_CONTENT))).toBe(undefined)
})

it('returns undefined if no body is present', () => {
expect(findAPMComment(createDocument(`${HTML_DOCTYPE}<html></html>`))).toBe(undefined)
})

it('finds a comment before the doctype', () => {
expect(findAPMComment(createDocument(DATADOG_COMMENT + HTML_DOCTYPE + HTML_CONTENT))).toBe('foo=bar')
})

it('finds a comment before the HTML content', () => {
expect(findAPMComment(createDocument(HTML_DOCTYPE + DATADOG_COMMENT + HTML_CONTENT))).toBe('foo=bar')
})

it('finds a comment after the HTML content', () => {
expect(findAPMComment(createDocument(HTML_DOCTYPE + HTML_CONTENT + DATADOG_COMMENT))).toBe('foo=bar')
})

it('finds a comment at the end of the body', () => {
expect(findAPMComment(createDocument(`${HTML_DOCTYPE}<html><body>${DATADOG_COMMENT}</body></html>`))).toBe(
'foo=bar'
)
})

it("doesn't match comments without the DATADOG; prefix", () => {
expect(findAPMComment(createDocument(`${HTML_DOCTYPE}${HTML_CONTENT}<!-- foo=bar -->`))).toBe(undefined)
})

it("doesn't look for comments nested below the body", () => {
expect(
findAPMComment(createDocument(`${HTML_DOCTYPE}<html><body><div>${DATADOG_COMMENT}</div></body></html>`))
).toBe(undefined)
})

it('finds a comment surrounded by newlines', () => {
expect(findAPMComment(createDocument(`<!--\nDATADOG;foo=bar\n-->${HTML_DOCTYPE}${HTML_CONTENT}`))).toBe('foo=bar')
})
})

describe('createAPMDocumentData', () => {
it('parses an APM comment', () => {
expect(createAPMDocumentData('123', '456')).toEqual({
traceId: '123',
traceTime: 456,
})
})

it('returns undefined if the time is not a number', () => {
expect(createAPMDocumentData('123', '4x6')).toBe(undefined)
})

it('returns undefined if the time is missing', () => {
expect(createAPMDocumentData('123', undefined)).toBe(undefined)
})

it('returns undefined if the trace id is missing', () => {
expect(createAPMDocumentData(undefined, '456')).toBe(undefined)
})

it('returns undefined if the trace id is empty', () => {
expect(createAPMDocumentData('', '456')).toBe(undefined)
})
})

function createDocument(content: string) {
return new DOMParser().parseFromString(content, 'text/html')
}