diff --git a/packages/rum-core/src/common/context.js b/packages/rum-core/src/common/context.js index 5b617a2f7..801854505 100644 --- a/packages/rum-core/src/common/context.js +++ b/packages/rum-core/src/common/context.js @@ -23,7 +23,7 @@ * */ -import Url from './url' +import { Url } from './url' import { PAGE_LOAD, NAVIGATION } from './constants' import { getServerTimingInfo, PERF, isPerfTimelineSupported } from './utils' diff --git a/packages/rum-core/src/common/url.js b/packages/rum-core/src/common/url.js index efd5ce8b7..b277cc794 100644 --- a/packages/rum-core/src/common/url.js +++ b/packages/rum-core/src/common/url.js @@ -78,7 +78,7 @@ const RULES = [ ] const PROTOCOL_REGEX = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i -class Url { +export class Url { constructor(url) { let { protocol, address, slashes } = this.extractProtocol(url || '') const relative = !protocol && !slashes @@ -216,4 +216,74 @@ class Url { } } -export default Url +/** + * Converts URL path tree in to slug based on tree depth + */ +export function slugifyUrl(urlStr, depth = 2) { + const parsedUrl = new Url(urlStr) + const { query, path } = parsedUrl + const pathParts = path.substring(1).split('/') + + const redactString = ':id' + const wildcard = '*' + const specialCharsRegex = /\W|_/g + const digitsRegex = /[0-9]/g + const lowerCaseRegex = /[a-z]/g + const upperCaseRegex = /[A-Z]/g + + const redactedParts = [] + let redactedBefore = false + + for (let index = 0; index < pathParts.length; index++) { + const part = pathParts[index] + + if (redactedBefore || index > depth - 1) { + if (part) { + redactedParts.push(wildcard) + } + break + } + + const numberOfSpecialChars = (part.match(specialCharsRegex) || []).length + if (numberOfSpecialChars >= 2) { + redactedParts.push(redactString) + redactedBefore = true + continue + } + + const numberOfDigits = (part.match(digitsRegex) || []).length + if ( + numberOfDigits > 3 || + (part.length > 3 && numberOfDigits / part.length >= 0.3) + ) { + redactedParts.push(redactString) + redactedBefore = true + continue + } + + const numberofUpperCase = (part.match(upperCaseRegex) || []).length + const numberofLowerCase = (part.match(lowerCaseRegex) || []).length + const lowerCaseRate = numberofLowerCase / part.length + const upperCaseRate = numberofUpperCase / part.length + if ( + part.length > 5 && + ((upperCaseRate > 0.3 && upperCaseRate < 0.6) || + (lowerCaseRate > 0.3 && lowerCaseRate < 0.6)) + ) { + redactedParts.push(redactString) + redactedBefore = true + continue + } + + part && redactedParts.push(part) + } + + const redacted = + '/' + + (redactedParts.length >= 2 + ? redactedParts.join('/') + : redactedParts.join('')) + + (query ? '?{query}' : '') + + return redacted +} diff --git a/packages/rum-core/src/performance-monitoring/performance-monitoring.js b/packages/rum-core/src/performance-monitoring/performance-monitoring.js index ded33772a..7a9810462 100644 --- a/packages/rum-core/src/performance-monitoring/performance-monitoring.js +++ b/packages/rum-core/src/performance-monitoring/performance-monitoring.js @@ -30,7 +30,7 @@ import { stripQueryStringFromUrl, getDtHeaderValue } from '../common/utils' -import Url from '../common/url' +import { Url } from '../common/url' import { patchEventHandler } from '../common/patching' import { globalState } from '../common/patching/patch-utils' import { diff --git a/packages/rum-core/src/performance-monitoring/transaction-service.js b/packages/rum-core/src/performance-monitoring/transaction-service.js index 9359fe310..c5047f9a3 100644 --- a/packages/rum-core/src/performance-monitoring/transaction-service.js +++ b/packages/rum-core/src/performance-monitoring/transaction-service.js @@ -48,6 +48,7 @@ import { } from '../common/constants' import { addTransactionContext } from '../common/context' import { __DEV__, state } from '../state' +import { slugifyUrl } from '../common/url' class TransactionService { constructor(logger, config) { @@ -232,6 +233,12 @@ class TransactionService { */ this.recorder.stop() + /** + * Capturing it here before scheduling the transaction end + * as to avoid capture different location when routed + */ + const currentUrl = window.location.href + return Promise.resolve().then( () => { const { name, type } = tr @@ -240,7 +247,7 @@ class TransactionService { if (lastHiddenStart >= tr._start) { if (__DEV__) { this._logger.debug( - `transaction(${tr.id}, ${tr.name}, ${tr.type}) was discarded! The page was hidden during the transaction!` + `transaction(${tr.id}, ${name}, ${type}) was discarded! The page was hidden during the transaction!` ) } return @@ -274,6 +281,13 @@ class TransactionService { tr.spans.push(createTotalBlockingTimeSpan(metrics.tbt)) } } + /** + * Categorize the transaction based on the current location + */ + if (tr.name === NAME_UNKNOWN) { + tr.name = slugifyUrl(currentUrl) + } + captureNavigation(tr) /** diff --git a/packages/rum-core/test/common/url.spec.js b/packages/rum-core/test/common/url.spec.js index 90f4e9e94..7fbb4c3dd 100644 --- a/packages/rum-core/test/common/url.spec.js +++ b/packages/rum-core/test/common/url.spec.js @@ -23,7 +23,7 @@ * */ -import Url from '../../src/common/url' +import { Url, slugifyUrl } from '../../src/common/url' describe('Url parser', function() { it('should parse relative url', function() { @@ -363,3 +363,45 @@ if (window.URL.toString().indexOf('native code') !== -1) { } }) } + +describe('Slug URL', () => { + const validate = (before, after, depth) => { + expect(slugifyUrl(before, depth)).toBe(after) + } + + it('accept depth parameter', () => { + validate('/a/b/c/d', '/a/b/*') + validate('/a/b/c/d', '/a/*', 1) + }) + + it('handle trailing slash', () => { + validate('/a/', '/a') + validate('/a', '/a') + validate('/', '/') + }) + + it('handle query param', () => { + validate('/a/b/?id=hello', '/a/b?{query}') + validate('/?foo=bar&bar=baz', '/?{query}') + }) + + it('redact digits', () => { + validate('/a/123', '/a/123') + validate('/a/12312bcdd23', '/a/:id') + validate('/a/B00I8BIC9E', '/a/:id') + // uuid + validate('/a/786c1883-dc0b-495f-a16b-6c53fb20b272', '/a/:id') + }) + + it('redact special characters', () => { + validate('/a-b-c-d', '/:id') + validate('/a~b-c', '/:id') + validate('/a~b-c/d/e/f', '/:id/*') + validate('/b%c%d-ef', '/:id') + }) + + it('react mix of lower-uppercase characters', () => { + validate('/abcDEF', '/:id') + validate('/a1W9FtW5DnkyP3Bucng4aLvqT/edit', '/:id/*') + }) +}) diff --git a/packages/rum-core/test/common/utils.spec.js b/packages/rum-core/test/common/utils.spec.js index 0f9a61096..3fc7a2798 100644 --- a/packages/rum-core/test/common/utils.spec.js +++ b/packages/rum-core/test/common/utils.spec.js @@ -25,7 +25,7 @@ import * as utils from '../../src/common/utils' import Span from '../../src/performance-monitoring/span' -import Url from '../../src/common/url' +import { Url } from '../../src/common/url' describe('lib/utils', function() { it('should merge objects', function() {