@newrelic/browser-agent 1.252.0 → 1.253.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +6 -6
- package/dist/cjs/cdn/experimental.js +6 -2
- package/dist/cjs/cdn/spa.js +5 -3
- package/dist/cjs/common/aggregate/aggregator.js +1 -8
- package/dist/cjs/common/config/state/init.js +7 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/context/observation-context-manager.js +56 -0
- package/dist/cjs/common/event-emitter/contextual-ee.js +12 -9
- package/dist/cjs/common/session/constants.js +2 -1
- package/dist/cjs/common/session/session-entity.js +3 -1
- package/dist/cjs/common/timing/nav-timing.js +8 -3
- package/dist/cjs/common/timing/now.js +1 -1
- package/dist/cjs/common/util/feature-flags.js +1 -1
- package/dist/cjs/common/wrap/index.js +0 -7
- package/dist/cjs/common/wrap/wrap-events.js +2 -2
- package/dist/cjs/common/wrap/wrap-fetch.js +2 -1
- package/dist/cjs/common/wrap/wrap-function.js +5 -7
- package/dist/cjs/common/wrap/wrap-promise.js +2 -1
- package/dist/cjs/features/ajax/aggregate/index.js +34 -16
- package/dist/cjs/features/jserrors/aggregate/index.js +77 -66
- package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
- package/dist/cjs/features/page_view_event/aggregate/initialized-features.js +1 -0
- package/dist/cjs/features/session_replay/aggregate/index.js +96 -94
- package/dist/cjs/features/session_replay/constants.js +5 -1
- package/dist/cjs/features/session_replay/instrument/index.js +24 -8
- package/dist/cjs/features/session_replay/shared/recorder.js +5 -4
- package/dist/cjs/features/session_replay/shared/stylesheet-evaluator.js +8 -7
- package/dist/cjs/features/session_replay/shared/utils.js +26 -0
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +50 -0
- package/dist/cjs/features/soft_navigations/aggregate/bel-node.js +29 -0
- package/dist/cjs/features/soft_navigations/aggregate/index.js +263 -0
- package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +62 -0
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +146 -0
- package/dist/cjs/features/soft_navigations/constants.js +31 -0
- package/dist/cjs/features/soft_navigations/index.js +12 -0
- package/dist/cjs/features/soft_navigations/instrument/index.js +79 -0
- package/dist/cjs/features/spa/aggregate/index.js +4 -4
- package/dist/cjs/features/utils/agent-session.js +2 -1
- package/dist/cjs/features/utils/instrument-base.js +6 -9
- package/dist/cjs/features/utils/lazy-feature-loader.js +2 -0
- package/dist/cjs/loaders/agent-base.js +18 -3
- package/dist/cjs/loaders/agent.js +15 -18
- package/dist/cjs/loaders/api/api-methods.js +9 -0
- package/dist/cjs/loaders/api/api.js +17 -18
- package/dist/cjs/loaders/configure/configure.js +5 -2
- package/dist/cjs/loaders/features/enabled-features.js +1 -1
- package/dist/cjs/loaders/features/features.js +3 -1
- package/dist/esm/cdn/experimental.js +5 -2
- package/dist/esm/cdn/spa.js +3 -1
- package/dist/esm/common/aggregate/aggregator.js +1 -8
- package/dist/esm/common/config/state/init.js +7 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/context/observation-context-manager.js +49 -0
- package/dist/esm/common/event-emitter/contextual-ee.js +12 -9
- package/dist/esm/common/session/constants.js +1 -0
- package/dist/esm/common/session/session-entity.js +3 -1
- package/dist/esm/common/timing/nav-timing.js +8 -3
- package/dist/esm/common/timing/now.js +1 -1
- package/dist/esm/common/util/feature-flags.js +1 -1
- package/dist/esm/common/wrap/index.js +1 -2
- package/dist/esm/common/wrap/wrap-events.js +3 -3
- package/dist/esm/common/wrap/wrap-fetch.js +3 -2
- package/dist/esm/common/wrap/wrap-function.js +4 -5
- package/dist/esm/common/wrap/wrap-promise.js +3 -2
- package/dist/esm/features/ajax/aggregate/index.js +36 -18
- package/dist/esm/features/jserrors/aggregate/index.js +77 -66
- package/dist/esm/features/page_view_event/aggregate/index.js +1 -1
- package/dist/esm/features/page_view_event/aggregate/initialized-features.js +1 -0
- package/dist/esm/features/session_replay/aggregate/index.js +97 -95
- package/dist/esm/features/session_replay/constants.js +4 -0
- package/dist/esm/features/session_replay/instrument/index.js +25 -9
- package/dist/esm/features/session_replay/shared/recorder.js +5 -4
- package/dist/esm/features/session_replay/shared/stylesheet-evaluator.js +8 -7
- package/dist/esm/features/session_replay/shared/utils.js +17 -0
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +43 -0
- package/dist/esm/features/soft_navigations/aggregate/bel-node.js +22 -0
- package/dist/esm/features/soft_navigations/aggregate/index.js +256 -0
- package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +55 -0
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +140 -0
- package/dist/esm/features/soft_navigations/constants.js +25 -0
- package/dist/esm/features/soft_navigations/index.js +1 -0
- package/dist/esm/features/soft_navigations/instrument/index.js +73 -0
- package/dist/esm/features/spa/aggregate/index.js +4 -4
- package/dist/esm/features/utils/agent-session.js +2 -1
- package/dist/esm/features/utils/instrument-base.js +7 -10
- package/dist/esm/features/utils/lazy-feature-loader.js +2 -0
- package/dist/esm/loaders/agent-base.js +18 -3
- package/dist/esm/loaders/agent.js +15 -18
- package/dist/esm/loaders/api/api-methods.js +3 -0
- package/dist/esm/loaders/api/api.js +17 -17
- package/dist/esm/loaders/configure/configure.js +5 -2
- package/dist/esm/loaders/features/enabled-features.js +1 -1
- package/dist/esm/loaders/features/features.js +3 -1
- package/dist/types/common/aggregate/aggregator.d.ts.map +1 -1
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/context/event-context.d.ts.map +1 -0
- package/dist/types/common/context/observation-context-manager.d.ts +28 -0
- package/dist/types/common/context/observation-context-manager.d.ts.map +1 -0
- package/dist/types/common/event-emitter/contextual-ee.d.ts +2 -2
- package/dist/types/common/event-emitter/contextual-ee.d.ts.map +1 -1
- package/dist/types/common/session/constants.d.ts +1 -0
- package/dist/types/common/session/constants.d.ts.map +1 -1
- package/dist/types/common/session/session-entity.d.ts +0 -1
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/timing/nav-timing.d.ts.map +1 -1
- package/dist/types/common/wrap/index.d.ts +1 -2
- package/dist/types/common/wrap/index.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-fetch.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-function.d.ts +0 -1
- package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-promise.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +4 -3
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/initialized-features.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +4 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +4 -0
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +19 -0
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts +16 -0
- package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +36 -0
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts +12 -0
- package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +50 -0
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/constants.d.ts +20 -0
- package/dist/types/features/soft_navigations/constants.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/index.d.ts +2 -0
- package/dist/types/features/soft_navigations/index.d.ts.map +1 -0
- package/dist/types/features/soft_navigations/instrument/index.d.ts +7 -0
- package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -0
- package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/utils/agent-session.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts +1 -7
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/features/utils/lazy-feature-loader.d.ts.map +1 -1
- package/dist/types/loaders/agent-base.d.ts +5 -1
- package/dist/types/loaders/agent-base.d.ts.map +1 -1
- package/dist/types/loaders/agent.d.ts +2 -2
- package/dist/types/loaders/agent.d.ts.map +1 -1
- package/dist/types/loaders/api/api-methods.d.ts +3 -0
- package/dist/types/loaders/api/api-methods.d.ts.map +1 -0
- package/dist/types/loaders/api/api.d.ts +3 -6
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/dist/types/loaders/features/features.d.ts +1 -0
- package/dist/types/loaders/features/features.d.ts.map +1 -1
- package/dist/types/loaders/micro-agent.d.ts +0 -1
- package/dist/types/loaders/micro-agent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cdn/experimental.js +4 -2
- package/src/cdn/spa.js +3 -1
- package/src/common/aggregate/aggregator.js +2 -11
- package/src/common/config/state/init.js +3 -1
- package/src/common/context/observation-context-manager.js +55 -0
- package/src/common/event-emitter/contextual-ee.js +20 -10
- package/src/common/session/constants.js +1 -0
- package/src/common/session/session-entity.js +3 -1
- package/src/common/timing/nav-timing.js +7 -3
- package/src/common/timing/now.js +1 -1
- package/src/common/util/feature-flags.js +1 -1
- package/src/common/wrap/index.js +1 -2
- package/src/common/wrap/wrap-events.js +3 -3
- package/src/common/wrap/wrap-fetch.js +3 -2
- package/src/common/wrap/wrap-function.js +4 -6
- package/src/common/wrap/wrap-promise.js +3 -2
- package/src/features/ajax/aggregate/index.js +36 -18
- package/src/features/jserrors/aggregate/index.js +70 -73
- package/src/features/page_view_event/aggregate/index.js +1 -1
- package/src/features/page_view_event/aggregate/initialized-features.js +1 -0
- package/src/features/session_replay/aggregate/index.js +92 -95
- package/src/features/session_replay/constants.js +5 -0
- package/src/features/session_replay/instrument/index.js +24 -9
- package/src/features/session_replay/shared/recorder.js +5 -4
- package/src/features/session_replay/shared/stylesheet-evaluator.js +8 -7
- package/src/features/session_replay/shared/utils.js +19 -0
- package/src/features/soft_navigations/aggregate/ajax-node.js +57 -0
- package/src/features/soft_navigations/aggregate/bel-node.js +26 -0
- package/src/features/soft_navigations/aggregate/index.js +254 -0
- package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +53 -0
- package/src/features/soft_navigations/aggregate/interaction.js +159 -0
- package/src/features/soft_navigations/constants.js +29 -0
- package/src/features/soft_navigations/index.js +1 -0
- package/src/features/soft_navigations/instrument/index.js +67 -0
- package/src/features/spa/aggregate/index.js +5 -4
- package/src/features/utils/agent-session.js +2 -1
- package/src/features/utils/instrument-base.js +7 -10
- package/src/features/utils/lazy-feature-loader.js +2 -0
- package/src/loaders/agent-base.js +18 -3
- package/src/loaders/agent.js +18 -17
- package/src/loaders/api/api-methods.js +12 -0
- package/src/loaders/api/api.js +17 -28
- package/src/loaders/configure/configure.js +4 -1
- package/src/loaders/features/enabled-features.js +1 -1
- package/src/loaders/features/features.js +3 -1
- package/dist/cjs/common/wrap/wrap-raf.js +0 -55
- package/dist/esm/common/wrap/wrap-raf.js +0 -48
- package/dist/types/common/event-emitter/event-context.d.ts.map +0 -1
- package/dist/types/common/wrap/wrap-raf.d.ts +0 -16
- package/dist/types/common/wrap/wrap-raf.d.ts.map +0 -1
- package/src/common/wrap/wrap-raf.js +0 -52
- /package/dist/cjs/common/{event-emitter → context}/event-context.js +0 -0
- /package/dist/esm/common/{event-emitter → context}/event-context.js +0 -0
- /package/dist/types/common/{event-emitter → context}/event-context.d.ts +0 -0
- /package/src/common/{event-emitter → context}/event-context.js +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { getConfigurationValue } from '../../../common/config/config';
|
|
2
|
+
import { handle } from '../../../common/event-emitter/handle';
|
|
3
|
+
import { registerHandler } from '../../../common/event-emitter/register-handler';
|
|
4
|
+
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler';
|
|
5
|
+
import { single } from '../../../common/util/invoke';
|
|
6
|
+
import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte';
|
|
7
|
+
import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
8
|
+
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
|
|
9
|
+
import { AggregateBase } from '../../utils/aggregate-base';
|
|
10
|
+
import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS } from '../constants';
|
|
11
|
+
import { AjaxNode } from './ajax-node';
|
|
12
|
+
import { InitialPageLoadInteraction } from './initial-page-load-interaction';
|
|
13
|
+
import { Interaction } from './interaction';
|
|
14
|
+
export class Aggregate extends AggregateBase {
|
|
15
|
+
static featureName = FEATURE_NAME;
|
|
16
|
+
constructor(agentIdentifier, aggregator, _ref) {
|
|
17
|
+
let {
|
|
18
|
+
domObserver
|
|
19
|
+
} = _ref;
|
|
20
|
+
super(agentIdentifier, aggregator, FEATURE_NAME);
|
|
21
|
+
const harvestTimeSeconds = getConfigurationValue(agentIdentifier, 'soft_navigations.harvestTimeSeconds') || 10;
|
|
22
|
+
this.interactionsToHarvest = [];
|
|
23
|
+
this.interactionsAwaitingRetry = [];
|
|
24
|
+
this.domObserver = domObserver;
|
|
25
|
+
this.scheduler = new HarvestScheduler('events', {
|
|
26
|
+
onFinished: this.onHarvestFinished.bind(this),
|
|
27
|
+
retryDelay: harvestTimeSeconds,
|
|
28
|
+
onUnload: () => this.interactionInProgress?.done() // return any held ajax or jserr events so they can be sent with EoL harvest
|
|
29
|
+
}, {
|
|
30
|
+
agentIdentifier,
|
|
31
|
+
ee: this.ee
|
|
32
|
+
});
|
|
33
|
+
this.scheduler.harvest.on('events', this.onHarvestStarted.bind(this));
|
|
34
|
+
this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentIdentifier);
|
|
35
|
+
timeToFirstByte.subscribe(_ref2 => {
|
|
36
|
+
let {
|
|
37
|
+
entries
|
|
38
|
+
} = _ref2;
|
|
39
|
+
const loadEventTime = entries[0].loadEventEnd;
|
|
40
|
+
this.initialPageLoadInteraction.forceSave = true;
|
|
41
|
+
this.initialPageLoadInteraction.done(loadEventTime);
|
|
42
|
+
this.interactionsToHarvest.push(this.initialPageLoadInteraction);
|
|
43
|
+
this.initialPageLoadInteraction = null;
|
|
44
|
+
// Report metric on the initial page load time
|
|
45
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/InitialPageLoad/Duration/Ms', Math.round(loadEventTime)], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
46
|
+
});
|
|
47
|
+
this.latestRouteSetByApi = null;
|
|
48
|
+
this.interactionInProgress = null; // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
|
|
49
|
+
|
|
50
|
+
this.blocked = false;
|
|
51
|
+
this.waitForFlags(['spa']).then(_ref3 => {
|
|
52
|
+
let [spaOn] = _ref3;
|
|
53
|
+
if (spaOn) this.scheduler.startTimer(harvestTimeSeconds, 0);else this.blocked = true; // if rum response determines that customer lacks entitlements for spa endpoint, this feature shouldn't harvest
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
|
|
57
|
+
registerHandler('newUIEvent', event => this.startUIInteraction(event.type, event.timeStamp, event.target), this.featureName, this.ee);
|
|
58
|
+
registerHandler('newURL', (timestamp, url) => this.interactionInProgress?.updateHistory(timestamp, url), this.featureName, this.ee);
|
|
59
|
+
registerHandler('newDom', timestamp => {
|
|
60
|
+
this.interactionInProgress?.updateDom(timestamp);
|
|
61
|
+
if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done();
|
|
62
|
+
}, this.featureName, this.ee);
|
|
63
|
+
this.#registerApiHandlers();
|
|
64
|
+
registerHandler('ajax', this.#handleAjaxEvent.bind(this), this.featureName, this.ee);
|
|
65
|
+
registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee);
|
|
66
|
+
this.drain();
|
|
67
|
+
}
|
|
68
|
+
onHarvestStarted(options) {
|
|
69
|
+
if (this.interactionsToHarvest.length === 0 || this.blocked) return;
|
|
70
|
+
|
|
71
|
+
// The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
|
|
72
|
+
// In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
|
|
73
|
+
let firstIxnStartTime = 0; // the very 1st ixn does not require any offsetting
|
|
74
|
+
const serializedIxnList = [];
|
|
75
|
+
for (const interaction of this.interactionsToHarvest) {
|
|
76
|
+
serializedIxnList.push(interaction.serialize(firstIxnStartTime));
|
|
77
|
+
if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start);
|
|
78
|
+
}
|
|
79
|
+
const payload = "bel.7;".concat(serializedIxnList.join(';'));
|
|
80
|
+
if (options.retry) this.interactionsAwaitingRetry.push(...this.interactionsToHarvest);
|
|
81
|
+
this.interactionsToHarvest = [];
|
|
82
|
+
return {
|
|
83
|
+
body: {
|
|
84
|
+
e: payload
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
onHarvestFinished(result) {
|
|
89
|
+
if (result.sent && result.retry && this.interactionsAwaitingRetry.length > 0) {
|
|
90
|
+
this.interactionsToHarvest = [...this.interactionsAwaitingRetry, ...this.interactionsToHarvest];
|
|
91
|
+
this.interactionsAwaitingRetry = [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
startUIInteraction(eventName, startedAt, sourceElem) {
|
|
95
|
+
// this is throttled by instrumentation so that it isn't excessively called
|
|
96
|
+
if (this.interactionInProgress?.createdByApi) return; // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
|
|
97
|
+
if (this.interactionInProgress?.done() === false) return;
|
|
98
|
+
this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi);
|
|
99
|
+
if (eventName === 'click') {
|
|
100
|
+
const sourceElemText = getActionText(sourceElem);
|
|
101
|
+
if (sourceElemText) this.interactionInProgress.customAttributes.actionText = sourceElemText;
|
|
102
|
+
}
|
|
103
|
+
this.interactionInProgress.cancellationTimer = setTimeout(() => {
|
|
104
|
+
this.interactionInProgress.done();
|
|
105
|
+
// Report metric on frequency of cancellation due to timeout for UI ixn
|
|
106
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/TimeOut'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
107
|
+
}, 30000); // UI ixn are disregarded after 30 seconds if it's not completed by then
|
|
108
|
+
this.setClosureHandlers();
|
|
109
|
+
}
|
|
110
|
+
setClosureHandlers() {
|
|
111
|
+
this.interactionInProgress.on('finished', () => {
|
|
112
|
+
const ref = this.interactionInProgress;
|
|
113
|
+
this.interactionsToHarvest.push(this.interactionInProgress);
|
|
114
|
+
this.interactionInProgress = null;
|
|
115
|
+
this.domObserver.disconnect(); // can stop observing whenever our interaction logic completes a cycle
|
|
116
|
+
|
|
117
|
+
// Report metric on the ixn duration
|
|
118
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ["SoftNav/Interaction/".concat(ref.newURL !== ref.oldURL ? 'RouteChange' : 'Custom', "/Duration/Ms"), Math.round(ref.end - ref.start)], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
119
|
+
});
|
|
120
|
+
this.interactionInProgress.on('cancelled', () => {
|
|
121
|
+
this.interactionInProgress = null;
|
|
122
|
+
this.domObserver.disconnect();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find the active interaction (current or past) for a given timestamp. Note that historic lookups mostly only go as far back as the last harvest for this feature.
|
|
128
|
+
* Also, the caller should check the status of the interaction returned if found via {@link Interaction.status}, if that's pertinent.
|
|
129
|
+
* TIP: Cancelled (status) interactions are NOT returned!
|
|
130
|
+
* IMPORTANT: Finished interactions are in queue for next harvest! It's highly recommended that consumer logic be synchronous for safe reference.
|
|
131
|
+
* @param {DOMHighResTimeStamp} timestamp
|
|
132
|
+
* @returns An {@link Interaction} or undefined, if no active interaction was found.
|
|
133
|
+
*/
|
|
134
|
+
getInteractionFor(timestamp) {
|
|
135
|
+
/* In the sole case wherein there can be two "interactions" overlapping (initialPageLoad + regular route-change),
|
|
136
|
+
the regular interaction should get precedence in being assigned the "active" interaction in regards to our one-at-a-time model.
|
|
137
|
+
*/
|
|
138
|
+
if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress;
|
|
139
|
+
let saveIxn;
|
|
140
|
+
for (let idx = this.interactionsToHarvest.length - 1; idx >= 0; idx--) {
|
|
141
|
+
// reverse search for the latest completed interaction for efficiency
|
|
142
|
+
const finishedInteraction = this.interactionsToHarvest[idx];
|
|
143
|
+
if (finishedInteraction.isActiveDuring(timestamp)) {
|
|
144
|
+
if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction;
|
|
145
|
+
// It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
|
|
146
|
+
else saveIxn = finishedInteraction;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (saveIxn) return saveIxn; // if an iPL was determined to be active and no route-change was found active for the same time, then iPL is deemed the one
|
|
150
|
+
if (this.initialPageLoadInteraction?.isActiveDuring(timestamp)) return this.initialPageLoadInteraction; // lowest precedence and also only if it's still in-progress
|
|
151
|
+
// Time must be when no interaction is happening, so return undefined.
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handles or redirect ajax event based on the interaction, if any, that it's tied to.
|
|
156
|
+
* @param {Object} event see Ajax feature's storeXhr function for object definition
|
|
157
|
+
*/
|
|
158
|
+
#handleAjaxEvent(event) {
|
|
159
|
+
const associatedInteraction = this.getInteractionFor(event.startTime);
|
|
160
|
+
if (!associatedInteraction) {
|
|
161
|
+
// no interaction was happening when this ajax started, so give it back to Ajax feature for processing
|
|
162
|
+
handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee);
|
|
163
|
+
} else {
|
|
164
|
+
if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(this.agentIdentifier, event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
|
|
165
|
+
else {
|
|
166
|
+
// same thing as above, just at a later time -- if the interaction in progress is cancelled, just send the event back to ajax feat unmodified
|
|
167
|
+
associatedInteraction.on('finished', () => processAjax(this.agentIdentifier, event, associatedInteraction));
|
|
168
|
+
associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function processAjax(agentId, event, parentInteraction) {
|
|
172
|
+
const newNode = new AjaxNode(agentId, event);
|
|
173
|
+
parentInteraction.addChild(newNode);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Decorate the passed-in params obj with properties relating to any associated interaction at the time of the timestamp.
|
|
179
|
+
* @param {Object} params reference to the local var instance in Jserrors feature's storeError
|
|
180
|
+
* @param {DOMHighResTimeStamp} timestamp time the jserror occurred
|
|
181
|
+
*/
|
|
182
|
+
#handleJserror(params, timestamp) {
|
|
183
|
+
const associatedInteraction = this.getInteractionFor(timestamp);
|
|
184
|
+
if (!associatedInteraction) return; // do not need to decorate this jserror params
|
|
185
|
+
|
|
186
|
+
// Whether the interaction is in-progress or already finished, the id will let jserror buffer it under its index, until it gets the next step instruction.
|
|
187
|
+
params.browserInteractionId = associatedInteraction.id;
|
|
188
|
+
if (associatedInteraction.status === INTERACTION_STATUS.FIN) {
|
|
189
|
+
// This information cannot be relayed back via handle() that flushes buffered errs because this is being called by a jserror's handle() per se and before the err is buffered.
|
|
190
|
+
params._softNavFinished = true; // instead, signal that this err can be processed right away without needing to be buffered aka wait for an in-progress ixn
|
|
191
|
+
params._softNavAttributes = associatedInteraction.customAttributes;
|
|
192
|
+
} else {
|
|
193
|
+
// These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
|
|
194
|
+
// As such, be cautious not to use the params object since that's tied to one specific jserror and won't affect the rest of them.
|
|
195
|
+
associatedInteraction.on('finished', single(() => handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes], undefined, FEATURE_NAMES.jserrors, this.ee)));
|
|
196
|
+
associatedInteraction.on('cancelled', single(() => handle('softNavFlush', [associatedInteraction.id, false, undefined], undefined, FEATURE_NAMES.jserrors, this.ee))); // don't take custom attrs from cancelled ixns
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
#registerApiHandlers() {
|
|
200
|
+
const INTERACTION_API = 'api-ixn-';
|
|
201
|
+
const thisClass = this;
|
|
202
|
+
registerHandler(INTERACTION_API + 'get', function (time) {
|
|
203
|
+
let {
|
|
204
|
+
waitForEnd
|
|
205
|
+
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
206
|
+
// In here, 'this' refers to the EventContext specific to per InteractionHandle instance spawned by each .interaction() api call.
|
|
207
|
+
// Each api call aka IH instance would therefore retain a reference to either the in-progress interaction *at the time of the call* OR a new api-started interaction.
|
|
208
|
+
this.associatedInteraction = thisClass.getInteractionFor(time);
|
|
209
|
+
if (!this.associatedInteraction) {
|
|
210
|
+
// This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
|
|
211
|
+
this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentIdentifier, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
|
|
212
|
+
thisClass.setClosureHandlers();
|
|
213
|
+
}
|
|
214
|
+
if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true;
|
|
215
|
+
}, thisClass.featureName, thisClass.ee);
|
|
216
|
+
registerHandler(INTERACTION_API + 'end', function (timeNow) {
|
|
217
|
+
this.associatedInteraction.done(timeNow);
|
|
218
|
+
}, thisClass.featureName, thisClass.ee);
|
|
219
|
+
registerHandler(INTERACTION_API + 'save', function () {
|
|
220
|
+
this.associatedInteraction.forceSave = true;
|
|
221
|
+
}, thisClass.featureName, thisClass.ee);
|
|
222
|
+
registerHandler(INTERACTION_API + 'ignore', function () {
|
|
223
|
+
this.associatedInteraction.forceIgnore = true;
|
|
224
|
+
}, thisClass.featureName, thisClass.ee);
|
|
225
|
+
registerHandler(INTERACTION_API + 'getContext', function (time, callback) {
|
|
226
|
+
if (typeof callback !== 'function') return;
|
|
227
|
+
setTimeout(() => callback(this.associatedInteraction.customDataByApi), 0);
|
|
228
|
+
}, thisClass.featureName, thisClass.ee);
|
|
229
|
+
registerHandler(INTERACTION_API + 'onEnd', function (time, callback) {
|
|
230
|
+
if (typeof callback !== 'function') return;
|
|
231
|
+
this.associatedInteraction.onDone.push(callback);
|
|
232
|
+
}, thisClass.featureName, thisClass.ee);
|
|
233
|
+
registerHandler(INTERACTION_API + 'actionText', function (time, newActionText) {
|
|
234
|
+
if (newActionText) this.associatedInteraction.customAttributes.actionText = newActionText;
|
|
235
|
+
}, thisClass.featureName, thisClass.ee);
|
|
236
|
+
registerHandler(INTERACTION_API + 'setName', function (time, name, trigger) {
|
|
237
|
+
if (name) this.associatedInteraction.customName = name;
|
|
238
|
+
if (trigger) this.associatedInteraction.trigger = trigger;
|
|
239
|
+
}, thisClass.featureName, thisClass.ee);
|
|
240
|
+
registerHandler(INTERACTION_API + 'setAttribute', function (time, key, value) {
|
|
241
|
+
this.associatedInteraction.customAttributes[key] = value;
|
|
242
|
+
}, thisClass.featureName, thisClass.ee);
|
|
243
|
+
registerHandler(INTERACTION_API + 'routeName', function (time, newRouteName) {
|
|
244
|
+
// notice that this fn tampers with the ixn IP, not with the linked ixn
|
|
245
|
+
thisClass.latestRouteSetByApi = newRouteName;
|
|
246
|
+
if (thisClass.interactionInProgress) thisClass.interactionInProgress.newRoute = newRouteName;
|
|
247
|
+
}, thisClass.featureName, thisClass.ee);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function getActionText(elem) {
|
|
251
|
+
const tagName = elem.tagName.toLowerCase();
|
|
252
|
+
const elementsOfInterest = ['a', 'button', 'input'];
|
|
253
|
+
if (elementsOfInterest.includes(tagName)) {
|
|
254
|
+
return elem.title || elem.value || elem.innerText;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { navTimingValues } from '../../../common/timing/nav-timing';
|
|
2
|
+
import { Interaction } from './interaction';
|
|
3
|
+
import { numeric } from '../../../common/serialize/bel-serializer';
|
|
4
|
+
import { firstPaint } from '../../../common/vitals/first-paint';
|
|
5
|
+
import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint';
|
|
6
|
+
import { getInfo } from '../../../common/config/config';
|
|
7
|
+
export class InitialPageLoadInteraction extends Interaction {
|
|
8
|
+
constructor(agentIdentifier) {
|
|
9
|
+
super(agentIdentifier, 'initialPageLoad', 0, null);
|
|
10
|
+
const agentInfo = getInfo(agentIdentifier);
|
|
11
|
+
this.queueTime = agentInfo.queueTime;
|
|
12
|
+
this.appTime = agentInfo.applicationTime;
|
|
13
|
+
}
|
|
14
|
+
get firstPaint() {
|
|
15
|
+
return firstPaint.current.value;
|
|
16
|
+
}
|
|
17
|
+
get firstContentfulPaint() {
|
|
18
|
+
return firstContentfulPaint.current.value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the navTiming node. This assumes the navTimingValues array in nav-timing.js has already been filled with values via the PageViewEvent feature having
|
|
23
|
+
* executed the addPT function first and foremost.
|
|
24
|
+
*/
|
|
25
|
+
get navTiming() {
|
|
26
|
+
if (!navTimingValues.length) return;
|
|
27
|
+
/*
|
|
28
|
+
1. we initialize the seperator to ',' (seperates the nodeType id from the first value)
|
|
29
|
+
2. we initialize the navTiming node to 'b' (the nodeType id)
|
|
30
|
+
3. if the value is present, we add the seperator followed by the value;
|
|
31
|
+
otherwise:
|
|
32
|
+
- we add null seperator ('!') to the navTimingNode
|
|
33
|
+
- we set the seperator to an empty string since we already wrote it above
|
|
34
|
+
the reason for writing the null seperator instead of setting the seperator
|
|
35
|
+
is to ensure we still write it if the null is the last navTiming value.
|
|
36
|
+
*/
|
|
37
|
+
let seperator = ',';
|
|
38
|
+
let navTimingNode = 'b';
|
|
39
|
+
let prev = 0;
|
|
40
|
+
|
|
41
|
+
// Get all navTiming values except offset aka timeOrigin since we just consider that (this.start) 0.
|
|
42
|
+
// These are the latter 20 of the 21 timings appended by addPT:
|
|
43
|
+
navTimingValues.slice(1, 21).forEach(v => {
|
|
44
|
+
if (v !== undefined) {
|
|
45
|
+
navTimingNode += seperator + numeric(v - prev);
|
|
46
|
+
seperator = ',';
|
|
47
|
+
prev = v;
|
|
48
|
+
} else {
|
|
49
|
+
navTimingNode += seperator + '!';
|
|
50
|
+
seperator = '';
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return navTimingNode;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { getInfo } from '../../../common/config/config';
|
|
2
|
+
import { globalScope, initialLocation } from '../../../common/constants/runtime';
|
|
3
|
+
import { generateUuid } from '../../../common/ids/unique-id';
|
|
4
|
+
import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer';
|
|
5
|
+
import { cleanURL } from '../../../common/url/clean-url';
|
|
6
|
+
import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME } from '../constants';
|
|
7
|
+
import { BelNode } from './bel-node';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* link https://github.com/newrelic/nr-querypack/blob/main/schemas/bel/7.qpschema
|
|
11
|
+
**/
|
|
12
|
+
export class Interaction extends BelNode {
|
|
13
|
+
id = generateUuid(); // unique id that is serialized and used to link interactions with errors
|
|
14
|
+
initialPageURL = initialLocation;
|
|
15
|
+
oldURL = '' + globalScope?.location;
|
|
16
|
+
newURL = '' + globalScope?.location;
|
|
17
|
+
customName;
|
|
18
|
+
customAttributes = {};
|
|
19
|
+
customDataByApi = {};
|
|
20
|
+
queueTime; // only used by initialPageLoad interactions
|
|
21
|
+
appTime; // only used by initialPageLoad interactions
|
|
22
|
+
newRoute;
|
|
23
|
+
/** Internal state of this interaction: in-progress, finished, or cancelled. */
|
|
24
|
+
status = INTERACTION_STATUS.IP;
|
|
25
|
+
domTimestamp = 0;
|
|
26
|
+
historyTimestamp = 0;
|
|
27
|
+
createdByApi = false;
|
|
28
|
+
keepOpenUntilEndApi = false;
|
|
29
|
+
onDone = [];
|
|
30
|
+
cancellationTimer;
|
|
31
|
+
constructor(agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown) {
|
|
32
|
+
super(agentIdentifier);
|
|
33
|
+
this.belType = NODE_TYPE.INTERACTION;
|
|
34
|
+
this.trigger = uiEvent;
|
|
35
|
+
this.start = uiEventTimestamp;
|
|
36
|
+
this.oldRoute = currentRouteKnown;
|
|
37
|
+
this.eventSubscription = new Map([['finished', []], ['cancelled', []]]);
|
|
38
|
+
this.forceSave = this.forceIgnore = false;
|
|
39
|
+
if (this.trigger === API_TRIGGER_NAME) this.createdByApi = true;
|
|
40
|
+
}
|
|
41
|
+
updateDom(timestamp) {
|
|
42
|
+
this.domTimestamp = timestamp || performance.now(); // default timestamp should be precise for accurate isActiveDuring calculations
|
|
43
|
+
}
|
|
44
|
+
updateHistory(timestamp, newUrl) {
|
|
45
|
+
this.newURL = newUrl || '' + globalScope?.location;
|
|
46
|
+
this.historyTimestamp = timestamp || performance.now();
|
|
47
|
+
}
|
|
48
|
+
seenHistoryAndDomChange() {
|
|
49
|
+
return this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp; // URL must change before DOM does
|
|
50
|
+
}
|
|
51
|
+
on(event, cb) {
|
|
52
|
+
if (!this.eventSubscription.has(event)) throw new Error('Cannot subscribe to non pre-defined events.');
|
|
53
|
+
if (typeof cb !== 'function') throw new Error('Must supply function as callback.');
|
|
54
|
+
this.eventSubscription.get(event).push(cb);
|
|
55
|
+
}
|
|
56
|
+
done(customEndTime) {
|
|
57
|
+
// User could've mark this interaction--regardless UI or api started--as "don't close until .end() is called on it". Only .end provides a timestamp; the default flows do not.
|
|
58
|
+
if (this.keepOpenUntilEndApi && customEndTime === undefined) return false;
|
|
59
|
+
this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)); // this interaction's .save or .ignore can still be set by these user provided callbacks for example
|
|
60
|
+
|
|
61
|
+
if (this.forceIgnore) this.#cancel(); // .ignore() always has precedence over save actions
|
|
62
|
+
else if (this.seenHistoryAndDomChange()) this.#finish(customEndTime); // then this should've already finished while it was the interactionInProgress, with a natural end time
|
|
63
|
+
else if (this.forceSave) this.#finish(customEndTime || performance.now()); // a manually saved ixn (did not fulfill conditions) must have a specified end time, if one wasn't provided
|
|
64
|
+
else this.#cancel();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
#finish() {
|
|
68
|
+
let customEndTime = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
|
69
|
+
if (this.status !== INTERACTION_STATUS.IP) return; // disallow this call if the ixn is already done aka not in-progress
|
|
70
|
+
clearTimeout(this.cancellationTimer);
|
|
71
|
+
this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime);
|
|
72
|
+
this.customAttributes = {
|
|
73
|
+
...getInfo(this.agentIdentifier).jsAttributes,
|
|
74
|
+
...this.customAttributes
|
|
75
|
+
}; // attrs specific to this interaction should have precedence over the general custom attrs
|
|
76
|
+
this.status = INTERACTION_STATUS.FIN;
|
|
77
|
+
|
|
78
|
+
// Run all the callbacks awaiting this interaction to finish.
|
|
79
|
+
const callbacks = this.eventSubscription.get('finished');
|
|
80
|
+
callbacks.forEach(fn => fn());
|
|
81
|
+
}
|
|
82
|
+
#cancel() {
|
|
83
|
+
if (this.status !== INTERACTION_STATUS.IP) return; // disallow this call if the ixn is already done aka not in-progress
|
|
84
|
+
clearTimeout(this.cancellationTimer);
|
|
85
|
+
this.status = INTERACTION_STATUS.CAN;
|
|
86
|
+
|
|
87
|
+
// Run all the callbacks listening to this interaction's potential cancellation.
|
|
88
|
+
const callbacks = this.eventSubscription.get('cancelled');
|
|
89
|
+
callbacks.forEach(fn => fn());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Given a timestamp, determine if it falls within this interaction's span, i.e. if this was the active interaction during that time.
|
|
94
|
+
* For in-progress interactions, this only compares the time with the start of span. Cancelled interactions are not considered active at all.
|
|
95
|
+
* @param {DOMHighResTimeStamp} timestamp
|
|
96
|
+
* @returns True or false boolean.
|
|
97
|
+
*/
|
|
98
|
+
isActiveDuring(timestamp) {
|
|
99
|
+
if (this.status === INTERACTION_STATUS.IP) return this.start <= timestamp;
|
|
100
|
+
return this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end >= timestamp;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Following are virtual properties overridden by a subclass:
|
|
104
|
+
get firstPaint() {}
|
|
105
|
+
get firstContentfulPaint() {}
|
|
106
|
+
get navTiming() {}
|
|
107
|
+
serialize(firstStartTimeOfPayload) {
|
|
108
|
+
const addString = getAddStringContext(this.agentIdentifier);
|
|
109
|
+
const nodeList = [];
|
|
110
|
+
let ixnType;
|
|
111
|
+
if (this.trigger === 'initialPageLoad') ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = INTERACTION_TYPE.UNSPECIFIED;
|
|
112
|
+
|
|
113
|
+
// IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
|
|
114
|
+
const fields = [numeric(this.belType), 0,
|
|
115
|
+
// this will be overwritten below with number of attached nodes
|
|
116
|
+
numeric(Math.floor(this.start - firstStartTimeOfPayload)),
|
|
117
|
+
// relative to first node
|
|
118
|
+
numeric(Math.floor(this.end - this.start)),
|
|
119
|
+
// end -- relative to start
|
|
120
|
+
numeric(this.callbackEnd),
|
|
121
|
+
// cbEnd -- relative to start; not used by BrowserInteraction events
|
|
122
|
+
numeric(this.callbackDuration),
|
|
123
|
+
// not relative
|
|
124
|
+
addString(this.trigger), addString(cleanURL(this.initialPageURL, true)), addString(cleanURL(this.oldURL, true)), addString(cleanURL(this.newURL, true)), addString(this.customName), ixnType, nullable(this.queueTime, numeric, true) + nullable(this.appTime, numeric, true) + nullable(this.oldRoute, addString, true) + nullable(this.newRoute, addString, true) + addString(this.id), addString(this.nodeId), nullable(this.firstPaint, numeric, true) + nullable(this.firstContentfulPaint, numeric)];
|
|
125
|
+
const allAttachedNodes = addCustomAttributes(this.customAttributes || {}, addString); // start with all custom attributes
|
|
126
|
+
if (getInfo(this.agentIdentifier).atts) allAttachedNodes.push('a,' + addString(getInfo(this.agentIdentifier).atts)); // add apm provided attributes
|
|
127
|
+
/* Querypack encoder+decoder quirkiness:
|
|
128
|
+
- If first ixn node of payload is being processed, we use this node's start to offset. (firstStartTime should be 0--or undefined.)
|
|
129
|
+
- Else for subsequent ixn nodes, we use the first ixn node's start to offset. */
|
|
130
|
+
this.children.forEach(node => allAttachedNodes.push(node.serialize(firstStartTimeOfPayload || this.start))); // recursively add the serialized string of every child of this (ixn) bel node
|
|
131
|
+
|
|
132
|
+
fields[1] = numeric(allAttachedNodes.length);
|
|
133
|
+
nodeList.push(fields);
|
|
134
|
+
if (allAttachedNodes.length) nodeList.push(allAttachedNodes.join(';'));
|
|
135
|
+
if (this.navTiming) nodeList.push(this.navTiming);else nodeList.push('');
|
|
136
|
+
// nodeList = [<fields array>, <serialized string of all attributes and children>, <serialized nav timing info> || '']
|
|
137
|
+
|
|
138
|
+
return nodeList.join(';');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FEATURE_NAMES } from '../../loaders/features/features';
|
|
2
|
+
export const INTERACTION_TRIGGERS = ['click',
|
|
3
|
+
// e.g. user clicks link or the page back/forward buttons
|
|
4
|
+
'keydown',
|
|
5
|
+
// e.g. user presses left and right arrow key to switch between displayed photo gallery
|
|
6
|
+
'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
|
|
7
|
+
];
|
|
8
|
+
export const API_TRIGGER_NAME = 'api';
|
|
9
|
+
export const FEATURE_NAME = FEATURE_NAMES.softNav;
|
|
10
|
+
export const INTERACTION_TYPE = {
|
|
11
|
+
INITIAL_PAGE_LOAD: '',
|
|
12
|
+
ROUTE_CHANGE: 1,
|
|
13
|
+
UNSPECIFIED: 2
|
|
14
|
+
};
|
|
15
|
+
export const NODE_TYPE = {
|
|
16
|
+
INTERACTION: 1,
|
|
17
|
+
AJAX: 2,
|
|
18
|
+
CUSTOM_END: 3,
|
|
19
|
+
CUSTOM_TRACER: 4
|
|
20
|
+
};
|
|
21
|
+
export const INTERACTION_STATUS = {
|
|
22
|
+
IP: 'in progress',
|
|
23
|
+
FIN: 'finished',
|
|
24
|
+
CAN: 'cancelled'
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Instrument as SoftNav } from './instrument/index';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { originals } from '../../../common/config/config';
|
|
2
|
+
import { isBrowserScope } from '../../../common/constants/runtime';
|
|
3
|
+
import { handle } from '../../../common/event-emitter/handle';
|
|
4
|
+
import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts';
|
|
5
|
+
import { now } from '../../../common/timing/now';
|
|
6
|
+
import { debounce } from '../../../common/util/invoke';
|
|
7
|
+
import { wrapEvents, wrapHistory } from '../../../common/wrap';
|
|
8
|
+
import { InstrumentBase } from '../../utils/instrument-base';
|
|
9
|
+
import { FEATURE_NAME, INTERACTION_TRIGGERS } from '../constants';
|
|
10
|
+
|
|
11
|
+
/** The minimal time after a UI event for which no further events will be processed - i.e. a throttling rate to reduce spam.
|
|
12
|
+
* This also give some time for the new interaction to complete without being discarded by a subsequent UI event and wrongly attributed.
|
|
13
|
+
* This value is still subject to change and critique, as it is derived from beyond worst case time to next frame of a page.
|
|
14
|
+
*/
|
|
15
|
+
const UI_WAIT_INTERVAL = 1 / 10 * 1000; // assume 10 fps
|
|
16
|
+
|
|
17
|
+
export class Instrument extends InstrumentBase {
|
|
18
|
+
static featureName = FEATURE_NAME;
|
|
19
|
+
constructor(agentIdentifier, aggregator) {
|
|
20
|
+
let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
21
|
+
super(agentIdentifier, aggregator, FEATURE_NAME, auto);
|
|
22
|
+
if (!isBrowserScope || !originals.MO) return; // soft navigations is not supported outside web env or browsers without the mutation observer API
|
|
23
|
+
|
|
24
|
+
const historyEE = wrapHistory(this.ee);
|
|
25
|
+
const eventsEE = wrapEvents(this.ee);
|
|
26
|
+
const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee);
|
|
27
|
+
historyEE.on('pushState-end', trackURLChange);
|
|
28
|
+
historyEE.on('replaceState-end', trackURLChange);
|
|
29
|
+
try {
|
|
30
|
+
this.removeOnAbort = new AbortController();
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
const trackURLChangeEvent = evt => handle('newURL', [evt.timeStamp, '' + window.location], undefined, this.featureName, this.ee);
|
|
33
|
+
windowAddEventListener('popstate', trackURLChangeEvent, true, this.removeOnAbort?.signal);
|
|
34
|
+
let oncePerFrame = false; // attempt to reduce dom noice since the observer runs very frequently with below options
|
|
35
|
+
const domObserver = new originals.MO((domChanges, observer) => {
|
|
36
|
+
if (oncePerFrame) return;
|
|
37
|
+
oncePerFrame = true;
|
|
38
|
+
requestAnimationFrame(() => {
|
|
39
|
+
// waiting for next frame to time when any visuals are supposedly updated
|
|
40
|
+
handle('newDom', [now()], undefined, this.featureName, this.ee);
|
|
41
|
+
oncePerFrame = false;
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
const processUserInteraction = debounce(event => {
|
|
45
|
+
handle('newUIEvent', [event], undefined, this.featureName, this.ee);
|
|
46
|
+
domObserver.observe(document.body, {
|
|
47
|
+
attributes: true,
|
|
48
|
+
childList: true,
|
|
49
|
+
subtree: true,
|
|
50
|
+
characterData: true
|
|
51
|
+
});
|
|
52
|
+
}, UI_WAIT_INTERVAL, {
|
|
53
|
+
leading: true
|
|
54
|
+
});
|
|
55
|
+
eventsEE.on('fn-start', _ref => {
|
|
56
|
+
let [evt] = _ref;
|
|
57
|
+
// set up a new user ixn before the callback for the triggering event executes
|
|
58
|
+
if (INTERACTION_TRIGGERS.includes(evt?.type)) {
|
|
59
|
+
processUserInteraction(evt);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
for (let eventType of INTERACTION_TRIGGERS) document.addEventListener(eventType, () => {/* no-op, this ensures the UI events are monitored by our callback above */});
|
|
63
|
+
this.abortHandler = abort;
|
|
64
|
+
this.importAggregator({
|
|
65
|
+
domObserver
|
|
66
|
+
});
|
|
67
|
+
function abort() {
|
|
68
|
+
this.removeOnAbort?.abort();
|
|
69
|
+
domObserver.disconnect();
|
|
70
|
+
this.abortHandler = undefined; // weakly allow this abort op to run only once
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -647,7 +647,7 @@ export class Aggregate extends AggregateBase {
|
|
|
647
647
|
}
|
|
648
648
|
function saveInteraction(interaction) {
|
|
649
649
|
if (interaction.ignored || !interaction.save && !interaction.routeChange) {
|
|
650
|
-
baseEE.emit('
|
|
650
|
+
baseEE.emit('interactionDone', [interaction, false]);
|
|
651
651
|
return;
|
|
652
652
|
}
|
|
653
653
|
if (state.prevInteraction === interaction) {
|
|
@@ -663,10 +663,10 @@ export class Aggregate extends AggregateBase {
|
|
|
663
663
|
interaction.root.attrs.firstPaint = firstPaint.current.value;
|
|
664
664
|
interaction.root.attrs.firstContentfulPaint = firstContentfulPaint.current.value;
|
|
665
665
|
}
|
|
666
|
-
baseEE.emit('
|
|
666
|
+
baseEE.emit('interactionDone', [interaction, true]);
|
|
667
667
|
state.interactionsToHarvest.push(interaction);
|
|
668
|
-
let smCategory
|
|
669
|
-
if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad';else if (interaction.
|
|
668
|
+
let smCategory;
|
|
669
|
+
if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad';else if (interaction.routeChange) smCategory = 'RouteChange';else smCategory = 'Custom';
|
|
670
670
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ["Spa/Interaction/".concat(smCategory, "/Duration/Ms"), Math.max((interaction.root?.end || 0) - (interaction.root?.start || 0), 0)], undefined, FEATURE_NAMES.metrics, baseEE);
|
|
671
671
|
scheduler.scheduleHarvest(0);
|
|
672
672
|
}
|
|
@@ -5,6 +5,7 @@ import { registerHandler } from '../../common/event-emitter/register-handler';
|
|
|
5
5
|
import { SessionEntity } from '../../common/session/session-entity';
|
|
6
6
|
import { LocalStorage } from '../../common/storage/local-storage.js';
|
|
7
7
|
import { FirstPartyCookies } from '../../common/storage/first-party-cookies';
|
|
8
|
+
import { DEFAULT_KEY } from '../../common/session/constants';
|
|
8
9
|
let ranOnce = 0;
|
|
9
10
|
export function setupAgentSession(agentIdentifier) {
|
|
10
11
|
const agentRuntime = getRuntime(agentIdentifier);
|
|
@@ -15,7 +16,7 @@ export function setupAgentSession(agentIdentifier) {
|
|
|
15
16
|
const storageTypeInst = sessionInit?.domain ? new FirstPartyCookies(sessionInit.domain) : new LocalStorage();
|
|
16
17
|
agentRuntime.session = new SessionEntity({
|
|
17
18
|
agentIdentifier,
|
|
18
|
-
key:
|
|
19
|
+
key: DEFAULT_KEY,
|
|
19
20
|
storage: storageTypeInst,
|
|
20
21
|
expiresMs: sessionInit?.expiresMs,
|
|
21
22
|
inactiveMs: sessionInit?.inactiveMs
|
|
@@ -10,7 +10,8 @@ import { onWindowLoad } from '../../common/window/load';
|
|
|
10
10
|
import { isBrowserScope } from '../../common/constants/runtime';
|
|
11
11
|
import { warn } from '../../common/util/console';
|
|
12
12
|
import { FEATURE_NAMES } from '../../loaders/features/features';
|
|
13
|
-
import { getConfigurationValue
|
|
13
|
+
import { getConfigurationValue } from '../../common/config/config';
|
|
14
|
+
import { canImportReplayAgg, enableSessionTracking } from '../session_replay/shared/utils';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Base class for instrumenting a feature.
|
|
@@ -73,7 +74,6 @@ export class InstrumentBase extends FeatureBase {
|
|
|
73
74
|
});
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
|
-
const enableSessionTracking = isBrowserScope && getConfigurationValue(this.agentIdentifier, 'privacy.cookies_enabled') === true;
|
|
77
77
|
let loadedSuccessfully;
|
|
78
78
|
this.onAggregateImported = new Promise(resolve => {
|
|
79
79
|
loadedSuccessfully = resolve;
|
|
@@ -81,7 +81,7 @@ export class InstrumentBase extends FeatureBase {
|
|
|
81
81
|
const importLater = async () => {
|
|
82
82
|
let session;
|
|
83
83
|
try {
|
|
84
|
-
if (enableSessionTracking) {
|
|
84
|
+
if (enableSessionTracking(this.agentIdentifier)) {
|
|
85
85
|
// would require some setup before certain features start
|
|
86
86
|
const {
|
|
87
87
|
setupAgentSession
|
|
@@ -90,6 +90,7 @@ export class InstrumentBase extends FeatureBase {
|
|
|
90
90
|
}
|
|
91
91
|
} catch (e) {
|
|
92
92
|
warn('A problem occurred when starting up session manager. This page will not start or extend any session.', e);
|
|
93
|
+
if (this.featureName === FEATURE_NAMES.sessionReplay) this.abortHandler?.(); // SR should stop recording if session DNE
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/**
|
|
@@ -97,7 +98,7 @@ export class InstrumentBase extends FeatureBase {
|
|
|
97
98
|
* it's only responsible for aborting its one specific feature, rather than all.
|
|
98
99
|
*/
|
|
99
100
|
try {
|
|
100
|
-
if (!this
|
|
101
|
+
if (!this.#shouldImportAgg(this.featureName, session)) {
|
|
101
102
|
drain(this.agentIdentifier, this.featureName);
|
|
102
103
|
loadedSuccessfully(false); // aggregate module isn't loaded at all
|
|
103
104
|
return;
|
|
@@ -130,12 +131,8 @@ export class InstrumentBase extends FeatureBase {
|
|
|
130
131
|
* @param {import('../../common/session/session-entity').SessionEntity} session
|
|
131
132
|
* @returns
|
|
132
133
|
*/
|
|
133
|
-
shouldImportAgg(featureName, session) {
|
|
134
|
-
if (featureName === FEATURE_NAMES.sessionReplay)
|
|
135
|
-
if (!originals.MO) return false; // Session Replay cannot work without Mutation Observer
|
|
136
|
-
if (getConfigurationValue(this.agentIdentifier, 'session_trace.enabled') === false) return false; // Session Replay as of now is tightly coupled with Session Trace in the UI
|
|
137
|
-
return !!session?.isNew || !!session?.state.sessionReplayMode; // Session Replay should only try to run if already running from a previous page, or at the beginning of a session
|
|
138
|
-
}
|
|
134
|
+
#shouldImportAgg(featureName, session) {
|
|
135
|
+
if (featureName === FEATURE_NAMES.sessionReplay) return canImportReplayAgg(this.agentIdentifier, session);
|
|
139
136
|
return true;
|
|
140
137
|
}
|
|
141
138
|
}
|