Skip to content

Commit

Permalink
✨ [RUMF-945] allow users to customize the attribute used to define th…
Browse files Browse the repository at this point in the history
…e action name (#919)

* ✨ allow users to customize the attribute used to define the action name

* fix lint/format issues

* style tweaks

* 🐛 fix userConfig -> config

* ✨ allow to have both data-dd-action-name et user configured attribute

Co-authored-by: Bazyli Brzóska <bazyli.brzoska@gmail.com>
  • Loading branch information
bcaudan and niieani authored Jul 1, 2021
1 parent 90573dc commit 3000fc5
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 18 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/domain/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface UserConfiguration {
publicApiKey?: string // deprecated
clientToken: string
applicationId?: string
actionNameAttribute?: string
internalMonitoringApiKey?: string
allowedTracingOrigins?: Array<string | RegExp>
sampleRate?: number
Expand Down Expand Up @@ -82,6 +83,8 @@ export type Configuration = typeof DEFAULT_CONFIGURATION &
service?: string
beforeSend?: BeforeSendCallback

actionNameAttribute?: string

isEnabled: (feature: string) => boolean
}

Expand Down Expand Up @@ -139,6 +142,10 @@ export function buildConfiguration(userConfiguration: UserConfiguration, buildEn
configuration.trackViewsManually = !!userConfiguration.trackViewsManually
}

if ('actionNameAttribute' in userConfiguration) {
configuration.actionNameAttribute = userConfiguration.actionNameAttribute
}

return configuration
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function startActionCollection(
)

if (configuration.trackInteractions) {
trackActions(lifeCycle, domMutationObservable)
trackActions(lifeCycle, domMutationObservable, configuration)
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,27 @@ describe('getActionNameFromElement', () => {
`)
).toBe('foo')
})

it('extracts the name from a user-configured attribute', () => {
expect(
getActionNameFromElement(
element`
<div data-test-id="foo">ignored</div>
`,
'data-test-id'
)
).toBe('foo')
})

it('favors data-dd-action-name over user-configured attribute', () => {
expect(
getActionNameFromElement(
element`
<div data-test-id="foo" data-dd-action-name="bar">ignored</div>
`,
'data-test-id'
)
).toBe('bar')
})
})
})
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
import { safeTruncate } from '@datadog/browser-core'

export function getActionNameFromElement(element: Element): string {
/**
* Get the action name from the attribute 'data-dd-action-name' on the element or any of its parent.
* It can also be retrieved from a user defined attribute.
*/
const DEFAULT_PROGRAMMATIC_ATTRIBUTE = 'data-dd-action-name'

export function getActionNameFromElement(element: Element, userProgrammaticAttribute?: string): string {
// Proceed to get the action name in two steps:
// * first, get the name programmatically, explicitly defined by the user.
// * then, use strategies that are known to return good results. Those strategies will be used on
// the element and a few parents, but it's likely that they won't succeed at all.
// * if no name is found this way, use strategies returning less accurate names as a fallback.
// Those are much likely to succeed.
return (
getActionNameFromElementProgrammatically(element) ||
getActionNameFromElementProgrammatically(element, DEFAULT_PROGRAMMATIC_ATTRIBUTE) ||
(userProgrammaticAttribute && getActionNameFromElementProgrammatically(element, userProgrammaticAttribute)) ||
getActionNameFromElementForStrategies(element, priorityStrategies) ||
getActionNameFromElementForStrategies(element, fallbackStrategies) ||
''
)
}

/**
* Get the action name from the attribute 'data-dd-action-name' on the element or any of its parent.
*/
const PROGRAMMATIC_ATTRIBUTE = 'data-dd-action-name'
function getActionNameFromElementProgrammatically(targetElement: Element) {
function getActionNameFromElementProgrammatically(targetElement: Element, programmaticAttribute: string) {
let elementWithAttribute
// We don't use getActionNameFromElementForStrategies here, because we want to consider all parents,
// without limit. It is up to the user to declare a relevant naming strategy.
// If available, use element.closest() to match get the attribute from the element or any of its
// parent. Else fallback to a more traditional implementation.
if (supportsElementClosest()) {
elementWithAttribute = targetElement.closest(`[${PROGRAMMATIC_ATTRIBUTE}]`)
elementWithAttribute = targetElement.closest(`[${programmaticAttribute}]`)
} else {
let element: Element | null = targetElement
while (element) {
if (element.hasAttribute(PROGRAMMATIC_ATTRIBUTE)) {
if (element.hasAttribute(programmaticAttribute)) {
elementWithAttribute = element
break
}
Expand All @@ -41,7 +44,7 @@ function getActionNameFromElementProgrammatically(targetElement: Element) {
if (!elementWithAttribute) {
return
}
const name = elementWithAttribute.getAttribute(PROGRAMMATIC_ATTRIBUTE)!
const name = elementWithAttribute.getAttribute(programmaticAttribute)!
return truncate(normalizeWhitespace(name.trim()))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ describe('trackActions', () => {

setupBuilder = setup()
.withFakeClock()
.beforeBuild(({ lifeCycle, domMutationObservable }) => {
.beforeBuild(({ lifeCycle, domMutationObservable, configuration }) => {
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_CREATED, createSpy)
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, pushEvent)
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_DISCARDED, discardSpy)
return trackActions(lifeCycle, domMutationObservable)
return trackActions(lifeCycle, domMutationObservable, configuration)
})
})

Expand Down Expand Up @@ -137,9 +137,9 @@ describe('newAction', () => {
let setupBuilder: TestSetupBuilder
const { events, pushEvent } = eventsCollector<AutoAction>()

function newClick(name: string) {
function newClick(name: string, attribute = 'title') {
const button = document.createElement('button')
button.setAttribute('title', name)
button.setAttribute(attribute, name)
document.getElementById('root')!.appendChild(button)
button.click()
}
Expand All @@ -150,7 +150,9 @@ describe('newAction', () => {
document.body.appendChild(root)
setupBuilder = setup()
.withFakeClock()
.beforeBuild(({ lifeCycle, domMutationObservable }) => trackActions(lifeCycle, domMutationObservable))
.beforeBuild(({ lifeCycle, domMutationObservable, configuration }) =>
trackActions(lifeCycle, domMutationObservable, configuration)
)
})

afterEach(() => {
Expand Down Expand Up @@ -197,4 +199,20 @@ describe('newAction', () => {
resourceCount: 0,
})
})

it('should take the name from user-configured attribute', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder
.withConfiguration({ actionNameAttribute: 'data-my-custom-attribute' })
.build()
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, pushEvent)

newClick('test-1', 'data-my-custom-attribute')

clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].name).toBe('test-1')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ClocksState,
clocksNow,
TimeStamp,
Configuration,
} from '@datadog/browser-core'
import { ActionType } from '../../../rawRumEvent.types'
import { LifeCycle, LifeCycleEventType } from '../../lifeCycle'
Expand Down Expand Up @@ -46,7 +47,11 @@ export interface AutoActionCreatedEvent {
startClocks: ClocksState
}

export function trackActions(lifeCycle: LifeCycle, domMutationObservable: DOMMutationObservable) {
export function trackActions(
lifeCycle: LifeCycle,
domMutationObservable: DOMMutationObservable,
{ actionNameAttribute }: Configuration
) {
const action = startActionManagement(lifeCycle, domMutationObservable)

// New views trigger the discard of the current pending Action
Expand All @@ -61,7 +66,7 @@ export function trackActions(lifeCycle: LifeCycle, domMutationObservable: DOMMut
if (!(event.target instanceof Element)) {
return
}
const name = getActionNameFromElement(event.target)
const name = getActionNameFromElement(event.target, actionNameAttribute)
if (!name) {
return
}
Expand Down

0 comments on commit 3000fc5

Please sign in to comment.