Skip to content

Commit b1e03e2

Browse files
feat: Evaluate the accuracy of cross-feature attribution (#1573)
1 parent 8b04fb4 commit b1e03e2

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed

docs/supportability-metrics.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,4 +390,32 @@ EventBuffer/soft_navigations/Dropped/Bytes
390390
<!--- Soft nav harvest was sent before the interval elapsed (bytes captured) --->
391391
* soft_navigations/Harvest/Early/Seen
392392
<!--- Logging harvest was sent before the interval elapsed (bytes captured) --->
393-
* spa/Harvest/Early/Seen
393+
* spa/Harvest/Early/Seen
394+
395+
### Audit
396+
<!--- Page view event had hasReplay true but no session replay harvest (false positive) --->
397+
* audit/page_view/hasReplay/false/positive
398+
<!--- Page view event had hasReplay false but a session replay harvest occurred (false negative) --->
399+
* audit/page_view/hasReplay/false/negative
400+
<!--- Page view event had hasReplay true and a session replay harvest occurred (true positive) --->
401+
* audit/page_view/hasReplay/true/positive
402+
<!--- Page view event had hasReplay false and no session replay harvest occurred (true negative) --->
403+
* audit/page_view/hasReplay/true/negative
404+
405+
<!--- Page view event had hasTrace true but no session trace harvest (false positive) --->
406+
* audit/page_view/hasTrace/false/positive
407+
<!--- Page view event had hasTrace false but a session trace harvest occurred (false negative) --->
408+
* audit/page_view/hasTrace/false/negative
409+
<!--- Page view event had hasTrace true and a session trace harvest occurred (true positive) --->
410+
* audit/page_view/hasTrace/true/positive
411+
<!--- Page view event had hasTrace false and no session trace harvest occurred (true negative) --->
412+
* audit/page_view/hasTrace/true/negative
413+
414+
<!--- Session replay had hasError true but no js error harvest occurred (false positive) --->
415+
* audit/session_replay/hasError/false/positive
416+
<!--- Session replay had hasError false but a js error harvest occurred (false negative) --->
417+
* audit/session_replay/hasError/false/negative
418+
<!--- Session replay had hasError true and a js error harvest occurred (true positive) --->
419+
* audit/session_replay/hasError/true/positive
420+
<!--- Session replay had hasError false and no js error harvest occurred (true negative) --->
421+
* audit/session_replay/hasError/true/negative

src/common/harvest/harvester.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,34 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
160160
const cbResult = { sent: this.status !== 0, status: this.status, retry: shouldRetry(this.status), fullUrl, xhr: this, targetApp }
161161
if (localOpts.needResponse) cbResult.responseText = this.responseText
162162
cbFinished(cbResult)
163+
164+
/** temporary audit of consistency of harvest metadata flags */
165+
if (!shouldRetry(this.status)) trackHarvestMetadata()
163166
}, eventListenerOpts(false))
164167
} else if (submitMethod === fetchMethod) {
165168
result.then(async function (response) {
166169
const status = response.status
167170
const cbResult = { sent: true, status, retry: shouldRetry(status), fullUrl, fetchResponse: response, targetApp }
168171
if (localOpts.needResponse) cbResult.responseText = await response.text()
169172
cbFinished(cbResult)
173+
/** temporary audit of consistency of harvest metadata flags */
174+
if (!shouldRetry(status)) trackHarvestMetadata()
170175
})
171176
}
177+
178+
function trackHarvestMetadata () {
179+
const hasReplay = baseParams.includes('hr=1')
180+
const hasTrace = baseParams.includes('ht=1')
181+
const hasError = qs?.attributes?.includes('hasError=true')
182+
183+
handle('harvest-metadata', [{
184+
[featureName]: {
185+
...(hasReplay && { hasReplay }),
186+
...(hasTrace && { hasTrace }),
187+
...(hasError && { hasError })
188+
}
189+
}], undefined, FEATURE_NAMES.metrics, agentRef.ee)
190+
}
172191
}
173192

174193
dispatchGlobalEvent({
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export function evaluateHarvestMetadata (pageMetadata) {
7+
try {
8+
const supportabilityTags = []
9+
10+
// Report SM like... audit/<feature_name>/<hasReplay|hasTrace|hasError>/<true|false>/<negative|positive>
11+
const formTag = (...strings) => strings.join('/')
12+
13+
// Track if replay/trace/error harvests actually occurred (key only exists when harvested)
14+
function evaluateTag (feature, flag, hasFlag, hasHarvest) {
15+
const AUDIT = 'audit'
16+
if (hasFlag) {
17+
// False positive: flag true, but no harvest
18+
if (!hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'positive'))
19+
// True positive (correct)
20+
else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'positive'))
21+
} else {
22+
// False negative: flag false, but harvest occurred
23+
if (hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'negative'))
24+
// True negative (correct)
25+
else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'negative'))
26+
}
27+
}
28+
29+
if (pageMetadata.page_view_event) {
30+
evaluateTag('page_view', 'hasReplay', pageMetadata.page_view_event.hasReplay, !!pageMetadata.session_replay)
31+
evaluateTag('page_view', 'hasTrace', pageMetadata.page_view_event.hasTrace, !!pageMetadata.session_trace)
32+
}
33+
34+
if (pageMetadata.session_replay) {
35+
evaluateTag('session_replay', 'hasError', pageMetadata.session_replay.hasError, !!pageMetadata.jserrors)
36+
}
37+
38+
return supportabilityTags
39+
} catch (err) {
40+
return []
41+
}
42+
}

src/features/metrics/aggregate/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { windowAddEventListener } from '../../../common/event-listener/event-lis
1111
import { isBrowserScope, isWorkerScope } from '../../../common/constants/runtime'
1212
import { AggregateBase } from '../../utils/aggregate-base'
1313
import { isIFrameWindow } from '../../../common/dom/iframe'
14+
import { evaluateHarvestMetadata } from './harvest-metadata'
1415
// import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
1516
// import { handleWebsocketEvents } from './websocket-detection'
1617

@@ -19,6 +20,15 @@ export class Aggregate extends AggregateBase {
1920
constructor (agentRef) {
2021
super(agentRef, FEATURE_NAME)
2122
this.harvestOpts.aggregatorTypes = ['cm', 'sm'] // the types in EventAggregator this feature cares about
23+
24+
/** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
25+
this.harvestMetadata = {}
26+
this.harvestOpts.beforeUnload = () => {
27+
evaluateHarvestMetadata(this.harvestMetadata).forEach(smTag => {
28+
this.storeSupportabilityMetrics(smTag)
29+
})
30+
}
31+
2232
// This feature only harvests once per potential EoL of the page, which is handled by the central harvester.
2333

2434
// this must be read/stored synchronously, as the currentScript is removed from the DOM after this script is executed and this lookup will be void
@@ -126,6 +136,17 @@ export class Aggregate extends AggregateBase {
126136
// handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
127137
// }, this.featureName, this.ee)
128138
// })
139+
140+
/** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
141+
registerHandler('harvest-metadata', (harvestMetadataObject = {}) => {
142+
try {
143+
Object.keys(harvestMetadataObject).forEach(key => {
144+
Object.assign(this.harvestMetadata[key] ??= {}, harvestMetadataObject[key])
145+
})
146+
} catch (e) {
147+
// failed to merge harvest metadata... ignore
148+
}
149+
}, this.featureName, this.ee)
129150
}
130151

131152
eachSessionChecks () {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { evaluateHarvestMetadata } from '../../src/features/metrics/aggregate/harvest-metadata'
2+
3+
describe('evaluateHarvestMetadata', () => {
4+
it('returns SR false positive when hasReplay true but no replay harvest', () => {
5+
const meta = { page_view_event: { hasReplay: true } }
6+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining(['audit/page_view/hasReplay/false/positive']))
7+
})
8+
9+
it('returns SR false negative when hasReplay false but replay harvest occurred', () => {
10+
const meta = { page_view_event: { hasReplay: false }, session_replay: {} }
11+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining(['audit/page_view/hasReplay/false/negative']))
12+
})
13+
14+
it('returns ST false positive when hasTrace true but no trace harvest', () => {
15+
const meta = { page_view_event: { hasTrace: true } }
16+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining(['audit/page_view/hasTrace/false/positive']))
17+
})
18+
19+
it('returns ST false negative when hasTrace false but trace harvest occurred', () => {
20+
const meta = { page_view_event: { hasTrace: false }, session_trace: {} }
21+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining(['audit/page_view/hasTrace/false/negative']))
22+
})
23+
24+
it('returns SR event error false positive when SR harvest hasError true but no error harvest', () => {
25+
const meta = { session_replay: { hasError: true } }
26+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining(['audit/session_replay/hasError/false/positive']))
27+
})
28+
29+
it('returns SR event error false negative when error harvested during active SR but no SR harvest with hasError true', () => {
30+
const meta = { session_replay: { hasError: false }, jserrors: { } }
31+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining(['audit/session_replay/hasError/false/negative']))
32+
})
33+
34+
it('returns positive tags when all conditions are normal', () => {
35+
const meta = { page_view_event: { hasReplay: true, hasTrace: true }, session_replay: { }, session_trace: { } }
36+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining([
37+
'audit/page_view/hasReplay/true/positive',
38+
'audit/page_view/hasTrace/true/positive',
39+
'audit/session_replay/hasError/true/negative'
40+
]))
41+
})
42+
43+
it('returns true negative tags when all features are correctly absent', () => {
44+
const meta = { page_view_event: { hasReplay: false, hasTrace: false } }
45+
expect(evaluateHarvestMetadata(meta)).toEqual(expect.arrayContaining([
46+
'audit/page_view/hasReplay/true/negative',
47+
'audit/page_view/hasTrace/true/negative'
48+
]))
49+
})
50+
51+
it('returns multiple tags if multiple conditions are met', () => {
52+
const meta = { page_view_event: { hasReplay: true, hasTrace: false }, session_trace: { } }
53+
const tags = evaluateHarvestMetadata(meta)
54+
expect(tags).toEqual(expect.arrayContaining([
55+
'audit/page_view/hasReplay/false/positive',
56+
'audit/page_view/hasTrace/false/negative'
57+
]))
58+
})
59+
60+
it('handles errors', () => {
61+
const meta = {}
62+
Object.defineProperty(meta, 'page_view_event', {
63+
get () {
64+
throw new Error('Test error accessing page_view_event')
65+
}
66+
})
67+
expect(evaluateHarvestMetadata(meta)).toEqual([])
68+
})
69+
})

0 commit comments

Comments
 (0)