Skip to content

feat: Calculate New Relic time in the agent #911

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

Merged
merged 4 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdn/polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ import 'core-js/stable/object/get-own-property-descriptors'
import 'core-js/stable/url'
import 'core-js/stable/url-search-params'
import 'core-js/stable/string/starts-with'
import 'core-js/stable/number/is-nan'
55 changes: 0 additions & 55 deletions src/common/context/observation-context-manager.js

This file was deleted.

30 changes: 10 additions & 20 deletions src/common/event-emitter/contextual-ee.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import { gosNREUM } from '../window/nreum'
import { getOrSet } from '../util/get-or-set'
import { getRuntime } from '../config/config'
import { EventContext } from '../context/event-context'
import { ObservationContextManager } from '../context/observation-context-manager'
import { EventContext } from './event-context'
import { bundleId } from '../ids/bundle-id'

// create a unique id to store event context data for the current agent bundle
const contextId = `nr@context:${bundleId}`
// create global emitter instance that can be shared among bundles
const globalInstance = ee(undefined, 'globalEE')

Expand All @@ -18,7 +20,7 @@ if (!nr.ee) {
nr.ee = globalInstance
}

export { globalInstance as ee }
export { globalInstance as ee, contextId }

function ee (old, debugId) {
var handlers = {}
Expand Down Expand Up @@ -50,8 +52,8 @@ function ee (old, debugId) {
aborted: false,
isBuffering,
debugId,
backlog: isolatedBacklog ? {} : old && typeof old.backlog === 'object' ? old.backlog : {},
observationContextManager: null
backlog: isolatedBacklog ? {} : old && typeof old.backlog === 'object' ? old.backlog : {}

}

return emitter
Expand All @@ -60,15 +62,9 @@ function ee (old, debugId) {
if (contextOrStore && contextOrStore instanceof EventContext) {
return contextOrStore
} else if (contextOrStore) {
return getOrSet(contextOrStore, ObservationContextManager.contextId, () =>
emitter.observationContextManager
? emitter.observationContextManager.getCreateContext(contextOrStore)
: new EventContext(ObservationContextManager.contextId)
)
return getOrSet(contextOrStore, contextId, () => new EventContext(contextId))
} else {
return emitter.observationContextManager
? emitter.observationContextManager.getCreateContext({})
: new EventContext(ObservationContextManager.contextId)
return new EventContext(contextId)
}
}

Expand Down Expand Up @@ -116,13 +112,7 @@ function ee (old, debugId) {
}

function getOrCreate (name) {
const newEventEmitter = (emitters[name] = emitters[name] || ee(emitter, name))

if (!newEventEmitter.observationContextManager && emitter.observationContextManager) {
newEventEmitter.observationContextManager = emitter.observationContextManager
}

return newEventEmitter
return (emitters[name] = emitters[name] || ee(emitter, name))
}

function bufferEventsByGroup (types, group) {
Expand Down
2 changes: 1 addition & 1 deletion src/common/harvest/harvest.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class Harvest extends SharedContext {
result.addEventListener('loadend', function () {
// `this` refers to the XHR object in this scope, do not change this to a fat arrow
// status 0 refers to a local error, such as CORS or network failure, or a blocked request by the browser (e.g. adblocker)
const cbResult = { sent: this.status !== 0, status: this.status }
const cbResult = { sent: this.status !== 0, status: this.status, xhr: this, fullUrl }
if (this.status === 429) {
cbResult.retry = true
cbResult.delay = harvestScope.tooManyRequestsDelay
Expand Down
22 changes: 16 additions & 6 deletions src/common/harvest/harvest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe('_send', () => {
expect(xhrAddEventListener).toHaveBeenCalledWith('loadend', expect.any(Function), expect.any(Object))
expect(result).toEqual(jest.mocked(submitDataModule.xhr).mock.results[0].value)
expect(submitMethod).not.toHaveBeenCalled()
expect(spec.cbFinished).toHaveBeenCalledWith({ ...xhrState, sent: true })
expect(spec.cbFinished).toHaveBeenCalledWith({ ...xhrState, sent: true, xhr: xhrState, fullUrl: expect.any(String) })
})

test('should set cbFinished state retry to true with delay when xhr has 429 status', () => {
Expand All @@ -392,7 +392,9 @@ describe('_send', () => {
...xhrState,
sent: true,
retry: true,
delay: harvestInstance.tooManyRequestsDelay
delay: harvestInstance.tooManyRequestsDelay,
xhr: xhrState,
fullUrl: expect.any(String)
})
})

Expand All @@ -417,7 +419,9 @@ describe('_send', () => {
expect(spec.cbFinished).toHaveBeenCalledWith({
...xhrState,
sent: true,
retry: true
retry: true,
xhr: xhrState,
fullUrl: expect.any(String)
})
})

Expand All @@ -441,7 +445,9 @@ describe('_send', () => {
expect(submitMethod).not.toHaveBeenCalled()
expect(spec.cbFinished).toHaveBeenCalledWith({
...xhrState,
sent: true
sent: true,
xhr: xhrState,
fullUrl: expect.any(String)
})
})

Expand All @@ -466,7 +472,9 @@ describe('_send', () => {
expect(spec.cbFinished).toHaveBeenCalledWith({
...xhrState,
responseText: undefined,
sent: true
sent: true,
xhr: xhrState,
fullUrl: expect.any(String)
})
})

Expand All @@ -488,7 +496,9 @@ describe('_send', () => {
expect(submitMethod).not.toHaveBeenCalled()
expect(spec.cbFinished).toHaveBeenCalledWith({
...xhrState,
sent: false
sent: false,
xhr: xhrState,
fullUrl: expect.any(String)
})
})
})
Expand Down
96 changes: 96 additions & 0 deletions src/common/timing/time-keeper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { gosNREUM } from '../window/nreum'
import { globalScope } from '../constants/runtime'
import { getRuntime } from '../config/config'

/**
* Class used to adjust the timestamp of harvested data to New Relic server time. This
* is done by tracking the performance timings of the RUM call and applying a calculation
* to the harvested data event offset time.
*/
export class TimeKeeper {
#agent

/**
* Represents the browser origin time corrected to NR server time.
* @type {number}
*/
#correctedOriginTime

/**
* Represents the difference in milliseconds between the calculated NR server time and
* the local time.
* @type {number}
*/
#localTimeDiff

constructor (agent) {
this.#agent = agent
}

static getTimeKeeperByAgentIdentifier (agentIdentifier) {
const nr = gosNREUM()
return Object.keys(nr?.initializedAgents || {}).indexOf(agentIdentifier) > -1
? nr.initializedAgents[agentIdentifier].timeKeeper
: undefined
}

get correctedPageOriginTime () {
return this.#correctedOriginTime
}

/**
* Process a rum request to calculate NR server time.
* @param rumRequest {XMLHttpRequest} The xhr for the rum request
* @param rumRequestUrl {string} The full url of the rum request
*/
processRumRequest (rumRequest, rumRequestUrl) {
const responseDateHeader = rumRequest.getResponseHeader('Date')
if (!responseDateHeader) {
throw new Error('Missing date header on rum response.')
}

const resourceEntries = globalScope.performance.getEntriesByName(rumRequestUrl, 'resource')
if (!Array.isArray((resourceEntries)) || resourceEntries.length === 0) {
throw new Error('Missing rum request performance entry.')
}

let medianRumOffset = 0
let serverOffset = 0
if (typeof resourceEntries[0].responseStart === 'number' && resourceEntries[0].responseStart !== 0) {
// Cors is enabled and we can make a more accurate calculation of NR server time
medianRumOffset = (resourceEntries[0].responseStart - resourceEntries[0].requestStart) / 2
serverOffset = Math.floor(resourceEntries[0].requestStart + medianRumOffset)
} else {
// Cors is disabled or erred, we need to use a less accurate calculation
medianRumOffset = (resourceEntries[0].responseEnd - resourceEntries[0].fetchStart) / 2
serverOffset = Math.floor(resourceEntries[0].fetchStart + medianRumOffset)
}

// Corrected page origin time
this.#correctedOriginTime = Math.floor(Date.parse(responseDateHeader) - serverOffset)
this.#localTimeDiff = getRuntime(this.#agent.agentIdentifier).offset - this.#correctedOriginTime

if (Number.isNaN(this.#correctedOriginTime)) {
throw new Error('Date header invalid format.')
}
}

/**
* Converts a page origin relative time to an absolute timestamp
* corrected to NR server time.
* @param relativeTime {number} The relative time of the event in milliseconds
* @returns {number} The correct timestamp as a unix/epoch timestamp value
*/
convertRelativeTimestamp (relativeTime) {
return this.#correctedOriginTime + relativeTime
}

/**
* Corrects an event timestamp to NR server time.
* @param timestamp {number} The unix/epoch timestamp of the event with milliseconds
* @return {number} Corrected unix/epoch timestamp
*/
correctAbsoluteTimestamp (timestamp) {
return Math.floor(timestamp - this.#localTimeDiff)
}
}
Loading