Skip to content

Commit

Permalink
Add useractions
Browse files Browse the repository at this point in the history
  • Loading branch information
cwli24 committed Sep 17, 2024
1 parent b12131c commit 324fa70
Show file tree
Hide file tree
Showing 20 changed files with 184 additions and 89 deletions.
1 change: 1 addition & 0 deletions src/common/config/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const model = () => {
metrics: { enabled: true, autoStart: true },
obfuscate: undefined,
page_action: { enabled: true },
user_actions: { enabled: true },
page_view_event: { enabled: true, autoStart: true },
page_view_timing: { enabled: true, harvestTimeSeconds: 30, long_task: false, autoStart: true },
privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true
Expand Down
35 changes: 5 additions & 30 deletions src/common/event-listener/event-listener-opts.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,9 @@
import { globalScope } from '../constants/runtime'

/*
* See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#safely_detecting_option_support
*/
let passiveSupported = false
let signalSupported = false
try {
const options = {
get passive () { // this function will be called when the browser attempts to access the passive property
passiveSupported = true
return false
},
get signal () {
signalSupported = true
return false
}
}

globalScope.addEventListener('test', null, options)
globalScope.removeEventListener('test', null, options)
} catch (err) {}

export function eventListenerOpts (useCapture, abortSignal) {
return (passiveSupported || signalSupported)
? {
capture: !!useCapture,
passive: passiveSupported, // passive defaults to false
signal: abortSignal
}
: !!useCapture // mainly just IE11 doesn't support third param options under EventTarget API
return {
capture: useCapture,
passive: false,
signal: abortSignal
}
}

/** Do not use this within the worker context. */
Expand Down
1 change: 1 addition & 0 deletions src/common/timing/__mocks__/time-keeper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const TimeKeeper = jest.fn(function () {
this.convertRelativeTimestamp = jest.fn()
this.convertAbsoluteTimestamp = jest.fn()
this.correctAbsoluteTimestamp = jest.fn()
this.correctRelativeTimestamp = jest.fn()
})
9 changes: 9 additions & 0 deletions src/common/timing/time-keeper.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ export class TimeKeeper {
return timestamp - this.#localTimeDiff
}

/**
* Corrects relative timestamp to NR server time (epoch).
* @param {DOMHighResTimeStamp} relativeTime
* @returns {number}
*/
correctRelativeTimestamp (relativeTime) {
return this.correctAbsoluteTimestamp(this.convertRelativeTimestamp(relativeTime))
}

/** Process the session entity and use the info to set the main time calculations if present */
processStoredDiff () {
if (this.#ready) return // Time diff has already been calculated
Expand Down
29 changes: 20 additions & 9 deletions src/features/generic_events/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { stringify } from '../../../common/util/stringify'
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
import { cleanURL } from '../../../common/url/clean-url'
import { getInfo } from '../../../common/config/info'
import { getConfigurationValue } from '../../../common/config/init'
import { getConfiguration } from '../../../common/config/init'
import { getRuntime } from '../../../common/config/runtime'
import { FEATURE_NAME } from '../constants'
import { isBrowserScope } from '../../../common/constants/runtime'
Expand All @@ -25,9 +25,10 @@ export class Aggregate extends AggregateBase {
static featureName = FEATURE_NAME
constructor (agentIdentifier, aggregator) {
super(agentIdentifier, aggregator, FEATURE_NAME)
const agentInit = getConfiguration(this.agentIdentifier)

this.eventsPerHarvest = 1000
this.harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'generic_events.harvestTimeSeconds')
this.harvestTimeSeconds = agentInit.generic_events.harvestTimeSeconds

this.referrerUrl = (isBrowserScope && document.referrer) ? cleanURL(document.referrer) : undefined

Expand All @@ -42,14 +43,12 @@ export class Aggregate extends AggregateBase {
return
}

if (getConfigurationValue(this.agentIdentifier, 'page_action.enabled')) {
if (agentInit.page_action.enabled) {
registerHandler('api-addPageAction', (timestamp, name, attributes) => {
this.addEvent({
...attributes,
eventType: 'PageAction',
timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(
this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp)
)),
timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timestamp)),
timeSinceLoad: timestamp / 1000,
actionName: name,
referrerUrl: this.referrerUrl,
Expand All @@ -62,6 +61,20 @@ export class Aggregate extends AggregateBase {
}, this.featureName, this.ee)
}

if (agentInit.user_actions.enabled) {
registerHandler('ua', (evt) => {
this.addEvent({
eventType: 'UserAction',
timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(evt.timeStamp)),
action: evt.type,
targetId: evt.target?.id,
targetTag: evt.target?.tagName,
targetType: evt.target?.type,
targetClass: evt.target?.className
})
}, this.featureName, this.ee)
}

this.harvestScheduler = new HarvestScheduler('ins', { onFinished: (...args) => this.onHarvestFinished(...args) }, this)
this.harvestScheduler.harvest.on('ins', (...args) => this.onHarvestStarted(...args))
this.harvestScheduler.startTimer(this.harvestTimeSeconds, 0)
Expand All @@ -85,9 +98,7 @@ export class Aggregate extends AggregateBase {

const defaultEventAttributes = {
/** should be overridden by the event-specific attributes, but just in case -- set it to now() */
timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(
this.#agentRuntime.timeKeeper.convertRelativeTimestamp(now())
)),
timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(now())),
/** all generic events require a pageUrl */
pageUrl: cleanURL(getRuntime(this.agentIdentifier).origin)
}
Expand Down
3 changes: 3 additions & 0 deletions src/features/generic_events/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ import { FEATURE_NAMES } from '../../loaders/features/features'
export const FEATURE_NAME = FEATURE_NAMES.genericEvents
export const IDEAL_PAYLOAD_SIZE = 64000
export const MAX_PAYLOAD_SIZE = 1000000

export const OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'input', 'keydown', 'paste', 'scrollend']
export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur']
21 changes: 18 additions & 3 deletions src/features/generic_events/instrument/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { getConfigurationValue } from '../../../common/config/init'
import { getConfiguration } from '../../../common/config/init'
import { deregisterDrain } from '../../../common/drain/drain'
import { handle } from '../../../common/event-emitter/handle'
import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
import { InstrumentBase } from '../../utils/instrument-base'
import { FEATURE_NAME } from '../constants'
import { FEATURE_NAME, OBSERVED_EVENTS, OBSERVED_WINDOW_EVENTS } from '../constants'

export class Instrument extends InstrumentBase {
static featureName = FEATURE_NAME
constructor (agentIdentifier, aggregator, auto = true) {
super(agentIdentifier, aggregator, FEATURE_NAME, auto)
const agentInit = getConfiguration(this.agentIdentifier)
const genericEventSourceConfigs = [
getConfigurationValue(this.agentIdentifier, 'page_action.enabled')
agentInit.page_action.enabled,
agentInit.user_actions.enabled
// other future generic event source configs to go here, like M&Ms, PageResouce, etc.
]

if (agentInit.user_actions.enabled) {
OBSERVED_EVENTS.forEach(eventType =>
windowAddEventListener(eventType, (evt) => handle('ua', [evt], undefined, this.featureName, this.ee), true)
)
OBSERVED_WINDOW_EVENTS.forEach(eventType =>
windowAddEventListener(eventType, (evt) => handle('ua', [evt], undefined, this.featureName, this.ee))
// Capture is not used here so that we don't get element focus/blur events, only the window's as they do not bubble. They are also not cancellable, so no worries about being front of line.
)
}

/** If any of the sources are active, import the aggregator. otherwise deregister */
if (genericEventSourceConfigs.some(x => x)) this.importAggregator()
else deregisterDrain(this.agentIdentifier, this.featureName)
Expand Down
8 changes: 2 additions & 6 deletions src/features/jserrors/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ export class Aggregate extends AggregateBase {
if (!this.stackReported[bucketHash]) {
this.stackReported[bucketHash] = true
params.stack_trace = truncateSize(stackInfo.stackString)
this.observedAt[bucketHash] = Math.floor(agentRuntime.timeKeeper.correctAbsoluteTimestamp(
agentRuntime.timeKeeper.convertRelativeTimestamp(time)
))
this.observedAt[bucketHash] = Math.floor(agentRuntime.timeKeeper.correctRelativeTimestamp(time))

Check warning on line 187 in src/features/jserrors/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/jserrors/aggregate/index.js#L187

Added line #L187 was not covered by tests
} else {
params.browser_stack_hash = stringHashCode(stackInfo.stackString)
}
Expand All @@ -203,9 +201,7 @@ export class Aggregate extends AggregateBase {
}

params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
params.timestamp = Math.floor(agentRuntime.timeKeeper.correctAbsoluteTimestamp(
agentRuntime.timeKeeper.convertRelativeTimestamp(time)
))
params.timestamp = Math.floor(agentRuntime.timeKeeper.correctRelativeTimestamp(time))

Check warning on line 204 in src/features/jserrors/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/jserrors/aggregate/index.js#L204

Added line #L204 was not covered by tests

var type = internal ? 'ierr' : 'err'
var newMetrics = { time }
Expand Down
4 changes: 1 addition & 3 deletions src/features/logging/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ export class Aggregate extends AggregateBase {
if (typeof message !== 'string' || !message) return warn(32)

const log = new Log(
Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(
this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp)
)),
Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timestamp)),
message,
attributes,
level
Expand Down
4 changes: 1 addition & 3 deletions src/features/page_view_event/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ export class Aggregate extends AggregateBase {
queryParameters.fcp = firstContentfulPaint.current.value

if (this.timeKeeper?.ready) {
queryParameters.timestamp = Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
this.timeKeeper.convertRelativeTimestamp(now())
))
queryParameters.timestamp = Math.floor(this.timeKeeper.correctRelativeTimestamp(now()))

Check warning on line 109 in src/features/page_view_event/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/page_view_event/aggregate/index.js#L109

Added line #L109 was not covered by tests
}

const rumStartTime = now()
Expand Down
8 changes: 2 additions & 6 deletions src/features/session_replay/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,8 @@ export class Aggregate extends AggregateBase {
const firstEventTimestamp = this.getCorrectedTimestamp(events[0]) // from rrweb node
const lastEventTimestamp = this.getCorrectedTimestamp(events[events.length - 1]) // from rrweb node
// from rrweb node || from when the harvest cycle started
const firstTimestamp = firstEventTimestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp)
))
const lastTimestamp = lastEventTimestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
this.timeKeeper.convertRelativeTimestamp(relativeNow)
))
const firstTimestamp = firstEventTimestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
const lastTimestamp = lastEventTimestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))

const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {}

Expand Down
12 changes: 3 additions & 9 deletions src/features/session_trace/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,21 +154,15 @@ export class Aggregate extends AggregateBase {
type: 'BrowserSessionChunk',
app_id: this.agentInfo.applicationID,
protocol_version: '0',
timestamp: Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
this.timeKeeper.convertRelativeTimestamp(earliestTimeStamp)
)),
timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
attributes: encodeObj({
...(agentMetadata.entityGuid && { entityGuid: agentMetadata.entityGuid }),
harvestId: `${this.agentRuntime.session?.state.value}_${this.agentRuntime.ptid}_${this.agentRuntime.harvestCount}`,
// this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
// if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
// trace payload metadata
'trace.firstTimestamp': Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
this.timeKeeper.convertRelativeTimestamp(earliestTimeStamp)
)),
'trace.lastTimestamp': Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
this.timeKeeper.convertRelativeTimestamp(latestTimeStamp)
)),
'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
'trace.nodes': stns.length,
'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
// other payload metadata
Expand Down
24 changes: 24 additions & 0 deletions tests/assets/user-actions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<!--
Copyright 2020 New Relic Corporation.
PDX-License-Identifier: Apache-2.0
-->
<html>
<head>
<title>User Action Test</title>
{init} {config}
<!-- <script>
;window.NREUM||(NREUM={});NREUM.init={distributed_tracing:{enabled:true,cors_use_newrelic_header:false,cors_use_tracecontext_headers:false},privacy:{cookies_enabled:true},ajax:{deny_list:["staging-bam-cell.nr-data.net"]}};
;NREUM.loader_config={accountID:"550352",trustKey:"1",agentID:"174742265",licenseKey:"NRBR-c7d7aa44f6c74d8ed93",applicationID:"174742265"};
;NREUM.info={beacon:"staging-bam-cell.nr-data.net",errorBeacon:"staging-bam-cell.nr-data.net",licenseKey:"NRBR-c7d7aa44f6c74d8ed93",applicationID:"174742265",sa:1};
</script>
<script>NREUM.init.ssl = true</script> -->
{loader}
</head>
<body>
<button id="pay-btn" class="btn-cart-add flex-grow container" type="submit">Create click user action</button>
<input type="text" id="textbox"/>
</body>
</html>
16 changes: 15 additions & 1 deletion tests/components/generic_events/aggregate/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ test('should only buffer 64kb of events at a time', async () => {
expect(genericEventsAggregate.harvestScheduler.runHarvest).toHaveBeenCalled()
})

describe('page_actions', () => {
describe('sub-features', () => {
beforeEach(async () => {
genericEventsAggregate.ee.emit('rumresp', [{ ins: 1 }])
await new Promise(process.nextTick)
Expand Down Expand Up @@ -144,4 +144,18 @@ describe('page_actions', () => {
genericEventsAggregate.ee.emit('api-addPageAction', [relativeTimestamp, name, {}])
expect(genericEventsAggregate.events[0]).toBeUndefined()
})

test('should record user actions when enabled', () => {
getInfo(agentSetup.agentIdentifier).jsAttributes = { globalFoo: 'globalBar' }
genericEventsAggregate.ee.emit('ua', [{ timeStamp: 123456, type: 'click', target: { id: 'myBtn', tagName: 'button' } }])

expect(genericEventsAggregate.events.buffer[0]).toMatchObject({
eventType: 'UserAction',
timestamp: expect.any(Number),
action: 'click',
targetId: 'myBtn',
targetTag: 'button',
globalFoo: 'globalBar'
})
})
})
29 changes: 25 additions & 4 deletions tests/components/generic_events/instrument/index.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Instrument as GenericEvents } from '../../../../src/features/generic_events/instrument'
import * as handleModule from '../../../../src/common/event-emitter/handle'
import { setupAgent } from '../../setup-agent'
import { getConfiguration } from '../../../../src/common/config/init'
import { OBSERVED_EVENTS } from '../../../../src/features/generic_events/constants'

let agentSetup

Expand All @@ -9,21 +11,40 @@ beforeAll(() => {
})

describe('pageActions sub-feature', () => {
test('should import if event source is enabled', async () => {
getConfiguration(agentSetup.agentIdentifier).page_action = { enabled: true }
test('should import if at least one child feature is enabled', async () => {
const config = getConfiguration(agentSetup.agentIdentifier)
config.page_action.enabled = true
config.user_actions.enabled = false

const genericEventsInstrument = new GenericEvents(agentSetup.agentIdentifier, agentSetup.aggregator)
await new Promise(process.nextTick)

expect(genericEventsInstrument.featAggregate).toBeDefined()
})

test('should not import if event source is not enabled', async () => {
getConfiguration(agentSetup.agentIdentifier).page_action = { enabled: false }
test('should not import if no child features are enabled', async () => {
const config = getConfiguration(agentSetup.agentIdentifier)
config.page_action.enabled = false
config.user_actions.enabled = false

const genericEventsInstrument = new GenericEvents(agentSetup.agentIdentifier, agentSetup.aggregator)
await new Promise(process.nextTick)

expect(genericEventsInstrument.featAggregate).toBeUndefined()
})

test('user actions should be observed if enabled', () => {
const config = getConfiguration(agentSetup.agentIdentifier)
config.page_action.enabled = false
config.user_actions.enabled = true
const handleSpy = jest.spyOn(handleModule, 'handle')

const genericEventsInstrument = new GenericEvents(agentSetup.agentIdentifier, agentSetup.aggregator)
OBSERVED_EVENTS.forEach(eventType => {
const event = new Event(eventType)
window.dispatchEvent(event)

expect(handleSpy).toHaveBeenCalledWith('ua', [event], undefined, genericEventsInstrument.featureName, genericEventsInstrument.ee)
})
})
})
Loading

0 comments on commit 324fa70

Please sign in to comment.